Every microservices deck draws the same box: one database per service, labeled as a security boundary. That label is the assumption most teams copy. Database per Service pays off for a different reason.
The coupling you feel before the outage
Picture two product teams on one Postgres instance. Team A ships a column rename Friday afternoon. Team B's release does not go out until Monday because migration ordering became a meeting, not a merge.
Nobody got paged. The deploy queue stalled. That is what shared-schema coupling looks like in week two of "we are microservices now."
For a long time one database with many services was normal. Coordinated migration windows, shared release trains, one schema everyone negotiated. The diagram said microservices. The datastore said monolith.
Then the org splits into teams with different roadmaps. Same database. Different release trains. The pain shows up in deploy queues long before it shows up in an outage.
What Database per Service actually decouples
Database per Service is not isolation theater. It separates three things a shared database fuses together:
- Schema ownership — who can change the tables, and who gets called when a migration fails.
- Deployment cadence — how fast each team can ship schema changes without a committee.
- Failure blast radius — how far a poison migration or lock contention fans out.
Each datastore is a bounded context contract. You are not drawing a security perimeter on a slide. You are deciding who can change orders.status without a three-team approval thread.
flowchart LR
subgraph shared["Shared database (coupled)"]
S1[Orders service] --> PG[(Postgres)]
S2[Inventory service] --> PG
S3[Billing service] --> PG
end
subgraph split["Database per Service (decoupled)"]
O[Orders] --> DO[(orders_db)]
I[Inventory] --> DI[(inventory_db)]
B[Billing] --> DB[(billing_db)]
end
What you trade away
Independence has a price. The weekly revenue report that used to be one query becomes a pipeline problem.
-- Works on a shared schema. Breaks when orders and inventory are separate databases.
SELECT o.id, i.sku, o.total
FROM orders o
JOIN inventory i ON o.sku = i.sku
WHERE o.created_at > now() - interval '7 days';
After the split, that join lives somewhere else:
- An outbox stream into a warehouse.
- A materialized view fed by CDC.
- A saga with compensating steps instead of a cross-service transaction.
None of those is simpler than a join. They are simpler than four teams negotiating one migration order every sprint.
Cadence is the second win. Billing ships schema v3 while catalog stays on v1.
Blast radius is the third. A poison migration in one database does not lock tables in another.
When to split vs when to stay shared
Split databases when team boundaries and release independence matter more than cross-service transactions and ad-hoc reporting joins.
Keep one shared database when you are still one product, one release train, and analytics lives on relational joins you are not ready to rebuild as events.
If you copied the diagram but not the org structure, you pay the distributed-data tax without getting the ownership benefit.
Decision checklist
Before you draw separate database boxes on the next architecture review, ask:
- Do different teams own different services with different roadmaps?
- Has a shared migration already blocked someone's deploy?
- Would a bad migration in Service A be acceptable if it took down Service B's tables?
- Is your reporting workload built on cross-service SQL joins you cannot yet replace with events or a warehouse?
If 1 and 2 are yes and 3 is no, Database per Service is probably earning its keep. If you are still one team shipping one product, a shared database is often the honest choice until the org boundary catches up to the diagram.
Top comments (0)