Mystery DigitalTalk to us
Database-per-service vs shared schema: what the migration actually costs
ArchitectureMigrations

Database-per-service vs shared schema: what the migration actually costs

Priya Vasan · 2026-07-02 · 4 min read

Every services migration hits the same slide: a diagram where each box has its own database cylinder underneath it, with an arrow explaining that this is how "real" microservices look. It's treated as a non-negotiable maturity marker, the thing you graduate to once you've split the monolith. We've shipped both patterns at scale, and the honest answer is that database-per-service is the more expensive option in almost every dimension that isn't coupling, and coupling isn't always the problem you have.

What isolation actually buys you

Strict per-service databases give you three things, and only three: independent schema evolution, independent scaling, and a hard blast-radius boundary. If a payments service migrates a column, no other team's deploy breaks. If the search service needs to scale reads ten times harder than everything else, it can, without dragging the rest of the fleet's connection pool with it. If one database gets compromised or corrupted, the others are untouched.

None of that is free. You pay for it in cross-service queries that used to be a JOIN and are now a fan-out of network calls with their own retry and timeout logic. You pay for it in data consistency: a transaction that used to be BEGIN; UPDATE orders; UPDATE inventory; COMMIT; becomes a saga, or an outbox pattern, or eventual consistency you have to explain to support fielding "why does the order say fulfilled but the warehouse still shows it in stock." You pay for it in operational surface: more instances to patch, back up, monitor, and provision credentials for. A team that ran one Postgres cluster now runs eleven, each with its own connection limits, slow-query alerts, and on-call runbook.

The shared-schema case nobody wants to say out loud

Shared schema, with services owning their tables by convention and foreign keys enforced at the database level, is not a failure to modernize. For a lot of migrations it's the correct terminal state, not a waypoint. If your services are extracted along seams that still share a lot of relational data (orders and line items, users and their permissions, inventory and reservations), forcing separate databases just moves the referential integrity work from the database, which is good at it, into application code, which is not. We've seen teams reimplement foreign key constraints as a distributed saga with a compensating transaction, and watched that saga silently drop the compensating step during an incident because nobody load-tested the failure path. The database was already doing that job correctly for six years before the migration.

Shared schema also keeps query latency where it belongs. A report joining four domains inside one database runs in milliseconds. The same report against four separately-owned databases means an aggregation service, a cache, and a nightly reconciliation job to catch the cases where the cache lied. We've built that aggregation layer for clients twice, and both times it became the most expensive piece of infrastructure in the migration, more code and more incidents than the services it was stitching together.

Where the isolation earns its cost

The calculus flips when a service has a genuinely different consistency, scaling, or compliance profile than its neighbors. Payments and PII-heavy services are the clearest case: regulatory boundaries (PCI scope, GDPR data residency) mean you want the blast radius contained regardless of coupling cost, because the alternative is failing an audit or scoping your entire monolith into PCI. A service with wildly different read/write ratios, like an analytics or search index rebuilding itself nightly off a firehose of events, also earns isolation, because sharing a database means its batch jobs compete for the same buffer pool and lock table as your checkout flow at 2am on Black Friday.

The pattern we recommend: isolate the two or three services where isolation pays for itself in the first year, and leave the rest on a shared schema with enforced table ownership and a linter that fails CI if service A's code touches service B's tables directly. That gets you most of the coupling discipline microservices promise without the distributed-transaction tax on domains that never needed it. Revisit per service as scaling or compliance pressure actually shows up, not on a schedule set by a conference talk.

The tell that you got it wrong

If, six months after a database-per-service migration, your team has built a shared read replica, a change-data-capture pipeline, and a denormalized reporting store that re-joins the data you split apart, you didn't gain isolation. You paid for it twice: once to split the database, once to rebuild the join you needed anyway. That pattern shows up often enough in postmortems we've reviewed that it belongs in planning, not just hindsight.

← Back to blog