Distribution Boundaries

The Shared Database: An Honest Trade, Not a Prohibition

Two services reading and writing the same schema keep joins and transactions — which are genuinely valuable and expensive to replace. They also make the schema a public interface, blur ownership, and turn independent deployment into a coordinated one. Both halves are true, and which dominates depends on facts you can check.

▶ Run the lab

The question this answers

The question

Two services need the same data. Is sharing a database a shortcut or a mistake?

The guarantee — the property claimed, and its scope

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.

What a node knows — observation versus inference

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 guarantee?What does a node know?How does it work?What can fail?How does it fail?Where is coordination?What holds under failure?How does it recover?How would you know?What is the simpler thing?
shared databasecouplingschematrade-offs

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 schemaAfter splitting
Multi-entity atomicityprotocolOne transactionA saga plus compensation, per invariant
Cross-entity queriestypicalOne join, planner-optimisedFan-out, client join, or a derived copy
Referential integrityprotocolEnforced by the databaseA convention, enforced by nobody
FreshnessprotocolAlways currentEventually consistent, with a window
Schema changetypicalA coordinated fleet-wide eventPrivate to the owner
Independent deploytypicalConstrained by the shared schemaGenuinely independent
Who is authoritativeassumptionAmbiguous — anyone can writeExactly one writer, by construction
What sharing keeps, and what it costs

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.
A migration that was not reviewed as an API change

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 AS
7 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 review
13-- * "who reads orders?" is answerable from the grants, not from memory
14-- * orders-svc can restructure the physical table behind orders_v2
15-- * dropping orders_v2 is visibly a breaking change; dropping a column
16-- behind it is not
Enforcing ownership with grants — the consumer list becomes discoverable

Key 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.

How it works
  • 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.
What can fail at the boundary
  • 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.
How it fails — what an operator sees
  • 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.
Where coordination is required
  • 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.
What still holds under failure
  • 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.
How it recovers
  • 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.
How you would know
  • 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.
When it helps
  • 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.
When it hurts
  • 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.
Simpler alternatives
  • 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.

Two services need the same data. Four honest answers.
Not a prohibition — a spectrum, and which end dominates depends on facts about your system that you can check.
typicalCell values describe common implementations. Isolation levels, view support, replication mechanics and change-data-capture all vary by engine, and any of them can move a row.
Both services write the same tablesOne writer; others read through views it ownsOwner publishes a dataset; consumers keep their own copySeparate stores, events across the boundary
Multi-table transactionsprotocolyes, across bothyes, for the writernono — sagas
Joins across both datasetsprotocolyesyes, through the viewlocal join on a copyno
Stalenesstypicalnonenonelag, must be measuredlag, must be measured
Schema privacytypicalnone — it is a public interfacepartial — the view is the contractfullfull
Independent deployassumptionnoview changes coordinateyesyes
Failure isolationassumptionnone — one store, one fatenoneconsumer survives owner outageboth survive
Reconciliation neededtypicalnonoyes, per derived copyyes, per derived copy
Who enforces invariantssimplifiedthe database, for everyonethe database, one writerthe owning serviceapplication code you wrote
The trade is about where invariants are enforced, and what that costs in schema privacy.
what you keep
Atomic multi-table transactions across both services' data, referential integrity, and joins at the configured isolation level. These are genuinely valuable and expensive to replace.
what you give up
Schema privacy, independent deployment, and any clear answer to "who owns this row". A column drop fails in a service the migrating team has never opened.
migration protocol
Expand, migrate, contract — with a consumer review, because you cannot know from inside the database who depends on the shape of what they read.
where invariants live
The database enforces invariants for every writer. That is the benefit, and the price is that the schema became a public interface the moment the second writer appeared.
A shared database preserves, across the services that share it, exactly the guarantees the database provides — and guarantees nothing about either service being able to change its schema, deploy or scale without the other. Notice the asymmetry the spectrum exposes: shared reads keep most of the benefit at low cost; shared writes give up ownership entirely. That is why one writer with many readers sits where it does, and why it is so often the right point. If you stay here: one writer per table, reads through views the writer owns, per-service credentials so ownership is enforced rather than agreed, separate connection pools, and every migration handled as expand-migrate-contract with a consumer review.

What people believe, and what is true

Claim

Sharing a database is always wrong with microservices.

Reality

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.

Claim

Each service must have its own database.

Reality

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.

Claim

We share the database but each service only touches its own tables, so we are fine.

Reality

Then you have separate schemas with shared infrastructure — a much better position, and worth making explicit with grants so it stays true.

Claim

The schema is internal, so we can change it freely.

Reality

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.

Claim

We will split the database later when it becomes a problem.

Reality

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

Build it, then break 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.
Reason about this
  • 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?
Interview questions
  • 💬 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?