Why does having multiple 'microservices' read and write the same shared database (or even the same tables) recreate the coupling problems of a monolith?
answer
- schema = hidden unversioned API
- database-per-service pattern
- DDL change breaks silent consumers
- lock contention starves unrelated service
- CQRS/events replace cross-service reads
basics
~10 sThe database schema becomes a hidden shared contract - if one service changes a table, every other service touching that table can break, so you can't change or deploy them independently anymore.
solid answer
~30 sA shared database means the schema - table names, column types, constraints, indexes - is an implicit, unversioned API consumed by every service that queries it. Unlike a documented REST/gRPC contract, schema changes aren't discoverable at compile time by consumers, so a column rename or type change in Service A's table can silently break Service B's queries at runtime. It also breaks encapsulation: services can bypass each other's business rules by writing directly to shared tables, and it forces synchronized migrations and shared locking/transaction semantics across team boundaries - exactly the coordination cost microservices are meant to remove.
go deeper
Should recognize that sharing a database means services aren't really independent, even if code-level names give the illusion of separation.
Should articulate schema-as-hidden-contract and name database-per-service as the target pattern.
Should describe concrete production failure modes (lock contention, silent column-drop breakage) and know at least one remediation pattern such as an API call or event-driven read model.
Should weigh the trade-off explicitly - real ACID joins/transactions versus independent deployability - and design a phased data-ownership migration plan, including enforcement mechanisms like DB grants and fitness functions, not just conventions.
## The rule a shared schema breaks In a properly decomposed microservices system, each service exclusively owns its own data store, following the **'database-per-service'** pattern: other services never touch that data directly with SQL, they only ask for it through the owning service's published API or consume events it publishes. When teams instead point several services at one shared schema — very common when a service is extracted from an existing monolith's database without splitting the data — the schema itself becomes a **de facto integration contract**. Any DDL change is consumed by every service holding a connection string to that database, not just the team that considers itself the 'owner': - adding a `NOT NULL` constraint - renaming a column - changing a foreign key - dropping an index another service silently relied on for performance ## Why teams end up here This usually happens for understandable reasons: extracting a service's code into its own deployable is technically easier and faster than also splitting its data, especially when the new service still needs joins or transactional consistency with data 'owned' by other parts of the system. Keeping one database avoids the harder distributed-data problems — cross-service joins, referential integrity, multi-service transactions — in the short term, so teams under delivery pressure ship the code split and defer the data split, sometimes indefinitely. ## The trade-off The trade-off is real on both sides. A single shared database gives you **cheap consistency**: real ACID transactions and joins across what are nominally separate services, without touching sagas or eventual consistency. The cost is what you lose: - **independent deployability** — any schema migration needs review and coordination from everyone touching that table - **failure isolation**, since connection-pool exhaustion or lock contention from one service's heavy query can starve an unrelated service sharing the same database instance - **the ability to reason about dependencies**, because no compiler or contract enforces the schema as an API; breakage is discovered in production or by a job quietly failing against a renamed column ## Production failure modes Production failure modes recur in a few characteristic shapes. 1. **One:** Service A runs a schema migration (an `ALTER TABLE` that takes a lock) and Service B's completely unrelated queries start timing out or the shared connection pool saturates, causing an outage for a service that made no code change that day. 2. **Two:** Service B silently depends on the semantics of a column 'owned' by Service A; A's team cleans up dead code, drops the column, and B breaks in production with no compile-time warning. 3. **Three:** an old, half-forgotten batch job in Service C does a nightly bulk update against tables it doesn't 'own,' and nobody on-call for A or B even knows that dependency exists until anomalous data changes show up. ## The remediation pattern The remediation pattern — **'database-per-service,'** catalogued as one of the foundational microservices data patterns — is to: 1. identify the true **bounded-context owner** of each table; 2. migrate write access so only that owner can write (temporarily via a compatibility API if needed); 3. replace other services' direct SQL reads with either synchronous calls to the owner's API or an **event-driven local read model** (a CQRS-style materialized projection kept current by consuming the owner's domain events); 4. and only then physically split the schema or database once no cross-service SQL remains. A concrete worked example: extracting an 'Orders' service from a monolith and giving it exclusive ownership of the `orders` table, while an 'Inventory' service that used to run a direct SQL `JOIN` against `orders.order_id` now instead calls the Orders API for what it needs, or maintains its own local copy of the relevant fields kept up to date by consuming an `OrderPlaced` event — removing the shared-schema coupling while preserving the functionality both services need.
- If two services need to join data that used to be a simple SQL JOIN in the monolith, what are the options once each service owns its own database?Common approaches are: (1) have the consuming service call the owning service's API and join in application code, fine for low volume, (2) maintain a local materialized/read-optimized copy of the needed fields via a CQRS-style projection kept in sync by consuming domain events, or (3) accept eventual consistency and design the workflow around it. Real-time synchronous joins across services at scale usually push teams toward option 2.
- Is it ever acceptable for two services to share a database?As a deliberate, temporary step during a strangler-fig migration, yes - you might extract a service's code before you've fully split its data, with a plan to migrate ownership next. The danger is when it becomes permanent because splitting the data is 'too hard,' at which point the org has paid the network/ops cost of microservices while keeping the monolith's coupling.
- How do you enforce that only the owning service writes to its tables once you've decided on data ownership?Technically: give each service separate database credentials so non-owners have no write, ideally no read, grants on the owner's tables, enforced at the database user/role level rather than by convention alone. Organizationally: code review plus architecture fitness functions, such as a CI check scanning for cross-service SQL or ORM entities, catch violations before they reach production.
Like several departments sharing one filing cabinet with no assigned owner - anyone can reorganize the folders or throw out what looks like an old form, and every other department's process silently breaks the next time they reach for it.
saying these in an interview costs you the question
- Thinks a shared database is fine as long as each service 'only touches its own tables' with no technical enforcement of that boundary
- Doesn't mention that schema changes are effectively unversioned API changes
- Proposes 'just add more indexes' as a fix for the coupling instead of addressing data ownership
- Can't explain what database-per-service means or why it exists
- Assumes distributed transactions across services solve this cleanly with no downside