The question this answers
Two services need the same data. Is sharing a database a shortcut or a mistake?
A shared database preserves, across the services that share it, exactly the guarantees the database provides: atomic multi-table transactions, referential integrity, and consistent joins at the isolation level configured. What it does not guarantee is that either service can change its schema, deploy, or scale without the other.
Everything below is bought to hold this sentence. "Strongly consistent" with no scope attached is a slogan, not a guarantee — read what it actually covers, and what it explicitly does not.
With a shared database each service knows the true current state directly, which is precisely what makes the arrangement attractive — no staleness, no replication lag, no reconciliation. What no service knows is who else depends on the shape of what it reads. A column is dropped, and the failure appears in a service the migrating team has never opened. Shared schema converts private implementation knowledge into an undocumented public contract.
A node knows its own state and the messages that arrived. Everything else is inference from evidence that was already stale. "B has not replied in five seconds" is knowledge; "B is down" is a decision — and usually the bug.
What you keep — and it is not nothing
The case against sharing is usually made without acknowledging what sharing preserves, which is why the argument fails to persuade the people actually doing the work.
Atomic transactions across the shared data. One BEGIN, several tables, one COMMIT. Replacing that across a boundary means a saga, compensating actions, visible intermediate states and a reconciliation path — a substantial engineering project per invariant. Joins. A query spanning two entities is one statement the planner optimises. Across services it becomes an N+1 fan-out, a client-side join, or a denormalised copy that must be kept fresh. Referential integrity. A foreign key enforced by the database becomes a convention nobody enforces. No staleness and no reconciliation. Everyone reads the same rows, so there is no derived copy to drift and nothing to repair.
That is a real list. A team that splits the database without a plan for those four things has not simplified its architecture; it has converted database features into application code, and the application code is usually worse. The honest position is that splitting data has a cost that is frequently larger than the cost of sharing it.
| With a shared schema | After splitting | |
|---|---|---|
| Multi-entity atomicityprotocol | One transaction | A saga plus compensation, per invariant |
| Cross-entity queriestypical | One join, planner-optimised | Fan-out, client join, or a derived copy |
| Referential integrityprotocol | Enforced by the database | A convention, enforced by nobody |
| Freshnessprotocol | Always current | Eventually consistent, with a window |
| Schema changetypical | A coordinated fleet-wide event | Private to the owner |
| Independent deploytypical | Constrained by the shared schema | Genuinely independent |
| Who is authoritativeassumption | Ambiguous — anyone can write | Exactly one writer, by construction |
What you pay
The schema becomes a public interface without becoming a contract. Every column name, type, nullability and index is now depended upon by code the owning team cannot see. Renaming a column is a breaking API change performed without any of the ceremony a breaking API change normally gets. There is no versioning, no deprecation window, no consumer list — just a migration and a hope.
Ownership blurs. When two services write the same table, neither is authoritative. Business rules get enforced in one service and not the other; a write that violates an invariant arrives from the side that does not know about it. Most incidents labelled "data inconsistency" are really this, and the failing question is [[source-of-truth]]: which component decides what is true?
Independent deployment goes away. A migration must be compatible with every service reading the schema, so schema changes become coordinated releases — the defining symptom of a [[distributed-monolith]]. Blast radius concentrates. One slow query taken by one service consumes connections and locks that every other service needs; a bad deploy in the least important service can saturate the database for the most important one.
And the costs are asymmetric in time. Sharing is cheapest at the start, when two services and one small schema are easy to reason about. It becomes most expensive later, when eleven services read the schema, nobody knows which columns are load-bearing, and separating them requires archaeology.
-- orders-team migration 0142, reviewed by the orders team only
ALTER TABLE orders DROP COLUMN legacy_status; -- "unused since 2024"
-- consumers of orders.legacy_status, discovered afterwards:
reporting-svc nightly export -> silently exports NULL for 9 days
fulfilment-svc WHERE legacy_status -> 500s within 40 seconds
billing-svc read via a view -> view invalid, alerts at month end
data-platform ELT job -> column dropped from the warehouse,
two dashboards quietly wrong
-- There was no consumer list, no deprecation window and no version.
-- The same change behind a service API would have taken a quarter.When sharing is the right call
Sharing is defensible, and sometimes clearly correct. One team owns all the services involved. The coordination cost the split would remove does not exist, so you are paying distributed costs for nothing. The data is genuinely one unit with invariants across it. Orders and order lines are not two domains; splitting them converts a foreign key into a saga and buys nothing at all. The system is young and the boundaries are still moving. Every premature split becomes a versioned contract you must migrate; keeping the data together preserves your ability to change your mind cheaply.
One service writes and the others only read, with reads confined to a stable view. This is the strongest version: it keeps joins and freshness while preserving a single authority, and the view is a real contract that can be versioned. It fails only if the view is treated as a formality rather than as an interface.
It is also a legitimate deliberate transitional state during an extraction — provided somebody owns the schedule for ending it. Transitional states that nobody owns are how systems arrive at eleven services on one schema.
- One team owns every service involved — there is no coordination to remove.
- The data has invariants across it that would become sagas if split.
- The domain is young and its boundaries are still moving.
- One writer, many readers, reads confined to a stable versioned view.
- A deliberate, time-boxed transitional state with a named owner for ending it.
If you share, share deliberately
The difference between a defensible shared database and an accident is a handful of disciplines, all cheap relative to splitting.
Exactly one writer per table. This is the single most valuable rule and it costs almost nothing: sharing reads is a mild coupling, sharing writes destroys ownership. Expose reads through views owned by the writer, so the physical schema stays private and the view is the versioned contract. Maintain a consumer list per table — even a comment in the migration directory — so a migration can be reviewed as the API change it is. Give each service its own database credentials with permissions limited to what it may touch, which makes ownership enforceable rather than aspirational and makes the consumer list discoverable from the grants.
Separate connection pools per service, sized deliberately, so one service cannot exhaust the database on behalf of the others. And treat every migration as an API change: expand, migrate, contract — never a breaking change in one step, never a drop without a deprecation window.
With those in place, a shared database is a considered architecture with known costs. Without them, it is the coupling that makes every other coupling in the system unfixable — because while two services write the same tables, neither can migrate, deploy or scale alone, and no amount of work elsewhere changes that.
1-- orders-svc owns the orders tables: it alone may write.2GRANT SELECT, INSERT, UPDATE, DELETE ON orders, order_lines TO orders_svc;3 4-- Everyone else reads through a view owned by orders-svc.5-- The physical schema stays private; the view is the versioned contract.6CREATE VIEW orders_v2 AS7 SELECT id, customer_id, status, total_cents, placed_at FROM orders;8 9GRANT SELECT ON orders_v2 TO fulfilment_svc, billing_svc, reporting_svc;10 11-- Consequences worth noting:12-- * a write from fulfilment_svc fails at the database, not in review13-- * "who reads orders?" is answerable from the grants, not from memory14-- * orders-svc can restructure the physical table behind orders_v215-- * dropping orders_v2 is visibly a breaking change; dropping a column16-- behind it is notKey points
- Sharing keeps transactions, joins, referential integrity and freshness — genuinely expensive things to replace.
- Sharing costs you a schema that is a public interface with no versioning, blurred ownership, coordinated deploys and a concentrated blast radius.
- The costs are cheapest at the start and most expensive later, which is why the decision tends to be made badly.
- One writer per table is the highest-value discipline and nearly free.
- Expose reads through views owned by the writer, so the physical schema stays private and the contract is versioned.
- It is defensible when one team owns everything, when the data is genuinely one unit, or when boundaries are still moving.
The chain, answered
Every field here is required, which is why no lesson in this domain can recommend a design without naming what an operator sees when it fails, what survives the partition, what repairs it afterwards, and the simpler thing to consider first.
- • Two or more services connect to the same database and address the same tables.
- • Each service embeds assumptions about column names, types, nullability and indexes.
- • A schema change by one team must satisfy every service reading it, which requires knowing who they are.
- • Writes from multiple services mean invariants are enforced only by whichever service happens to know about them.
- • Query load from every service shares one set of connections, locks and buffer pool.
- • A migration breaks a service the migrating team did not know existed.
- • Two services write the same row with different business rules, and the row ends in a state neither considers valid.
- • One service’s slow query holds locks or exhausts connections, degrading everything else.
- • A service is deployed expecting a column that has not been added yet, or that was just removed.
- • A rollback of an application deploy is impossible because the migration has already run and is not reversible.
- • Migration collateral: the operator sees a service start returning 500s within a minute of an unrelated team’s migration, with no deploy of its own.
- • Silent data corruption: the operator sees records in a state the owning service considers impossible, written by another service that never knew the rule.
- • Cross-service saturation: the operator sees every service degrade simultaneously because one service deployed a query missing an index and consumed the connection pool.
- • Deploy-order breakage: the operator sees errors during a rollout because one service expects a schema version that another has not migrated to.
- • Unrollbackable release: the operator cannot roll back an application version because the migration that accompanied it dropped a column the old version reads.
- • Undiscoverable ownership: the operator, investigating a bad row, cannot determine which of five services wrote it, because they all share one credential.
- • Schema changes become the main coordination point: every migration is an agreement across every reader.
- • That coordination is invisible in the tooling — a migration looks like a database task, not a cross-team API change — which is why it is skipped.
- • One writer per table removes the hardest coordination (write-rule agreement) while keeping the benefits of shared reads.
- • Per-service credentials convert coordination-by-convention into coordination enforced by the database, which is the only kind that survives turnover.
- • The database’s guarantees hold uniformly for every service sharing it — that is the benefit, and it does not degrade under partial failure.
- • The database is a single fault domain: its unavailability is total for every service that shares it, with no partial-availability story.
- • Invariants enforced only in application code hold for writes from services that implement them and not for others, at all times.
- • During a migration window, services may observe schema states neither the old nor the new code was written against.
- • Detect: alert on migrations as change events correlated with error rates across all services, not only the migrating one.
- • Contain: roll back the migration if it is reversible; if it is not, that fact is the finding, and expand-migrate-contract is the fix.
- • Recover: restore the missing column or view, then re-deploy affected services in a compatible order.
- • Reconcile: repair records written in violation of invariants by services that did not know about them — the database will not have caught these.
- • Verify: re-run the invariant checks across the shared tables, and confirm the consumer list now includes everyone that broke.
- • Writers per table, from database grants or from query logs — the direct measure of ownership clarity.
- • Readers per table, maintained as a list, so migrations can be reviewed against real consumers.
- • Connection-pool usage attributed per service, so one service’s saturation is attributable rather than mysterious.
- • Migrations recorded as deploy markers on every service’s error dashboard, not only the owner’s.
- • Count of invariant violations found by periodic checks — a proxy for how much ownership has blurred.
- • One team owning all the services involved, where the split removes no coordination.
- • Data with genuine cross-entity invariants and transactions, where splitting converts constraints into sagas.
- • Young systems whose boundaries are still moving, where a premature split becomes a contract you must migrate.
- • One-writer, many-reader arrangements behind stable views, which keep joins and freshness while preserving authority.
- • Multiple teams that need to deploy independently, since schema changes become their coordination bottleneck.
- • Many writers, where ownership blurs and invariants are enforced inconsistently by construction.
- • Systems with wildly different load profiles per service, where one service’s query load determines everyone’s latency.
- • Any system large enough that nobody can enumerate the consumers of a table — at that point migrations are performed blind.
- • One writer, many readers through a versioned view: keeps almost all the benefits while restoring a single authority. The best first step, and often sufficient.
- • Separate schemas in the same database instance: independent migrations and clear ownership, while keeping operational simplicity — and cross-schema joins if the engine allows, at the cost of re-creating the coupling.
- • Full separation with an event feed and a derived read model: real independence, paid for with eventual consistency and a
[[reconciliation]]obligation. - • A read replica per consumer: removes the load coupling and keeps the schema coupling — useful when contention is the problem and ownership is not.
Two services need the same data. Four honest answers.
| Both services write the same tables | One writer; others read through views it owns | Owner publishes a dataset; consumers keep their own copy | Separate stores, events across the boundary | |
|---|---|---|---|---|
| Multi-table transactionsprotocol | yes, across both | yes, for the writer | no | no — sagas |
| Joins across both datasetsprotocol | yes | yes, through the view | local join on a copy | no |
| Stalenesstypical | none | none | lag, must be measured | lag, must be measured |
| Schema privacytypical | none — it is a public interface | partial — the view is the contract | full | full |
| Independent deployassumption | no | view changes coordinate | yes | yes |
| Failure isolationassumption | none — one store, one fate | none | consumer survives owner outage | both survive |
| Reconciliation neededtypical | no | no | yes, per derived copy | yes, per derived copy |
| Who enforces invariantssimplified | the database, for everyone | the database, one writer | the owning service | application code you wrote |
What people believe, and what is true
Sharing a database is always wrong with microservices.
It is a trade. It keeps transactions, joins and freshness, all of which are expensive to rebuild. Whether that outweighs the coupling depends on team structure, invariants and system age.
Each service must have its own database.
Each piece of state must have exactly one owner. That is achievable with one writer per table inside a shared instance, which is a much cheaper change than separating storage.
We share the database but each service only touches its own tables, so we are fine.
Then you have separate schemas with shared infrastructure — a much better position, and worth making explicit with grants so it stays true.
The schema is internal, so we can change it freely.
The moment a second service reads it, it is a public interface with consumers you cannot see and no version. Migrations need the review a breaking API change would get.
We will split the database later when it becomes a problem.
Splitting is cheapest when two services share a small schema and hardest when eleven share a large one. "Later" is precisely when the cost peaks.
Go deeper
Only the levels this lesson can honestly fill — a missing level is a claim nobody had.
Overview
Sharing keeps joins and transactions; it costs schema coupling, blurred ownership and coordinated deploys. Both halves are real — decide on the facts of your system rather than on a rule.
Practical
If you share: one writer per table, reads through views owned by the writer, per-service credentials so ownership is enforced and consumers are discoverable, separate connection pools, and every migration handled as expand-migrate-contract with a consumer review.
Advanced
The trade is really about where invariants are enforced. A database enforces them for every writer, at the cost of making the schema a shared interface. Split ownership moves enforcement into one service, which regains schema privacy and gives up mechanical enforcement over everyone else. Shared *reads* keep the first benefit at low cost; shared *writes* give up the second entirely. That asymmetry is why one-writer-many-readers is so often the right point on this spectrum.
Apply it
- 🔧 List every service that writes to your largest shared table. If the count is above one, work out what it would take to reduce it to one.
- 🔧 Take your last schema migration and reconstruct the consumer list it needed. Note how you found it, and whether you could have found it beforehand.
- ⚡ A team wants to drop a column marked unused. Five services share the schema and there is no consumer list. What is the safe procedure, and what does the need for it tell you?
- 💬 Two services need the same data. Walk me through the options and what each costs.
- 💬 What do you actually give up when you split a shared database?
- 💬 You must keep the shared database. What five things do you do to make it defensible?
- 💬 Why is one-writer-many-readers so much better than many writers, given both share the schema?