The quiet cost of a shared database
Contents
Every company I have advised in the last five years reached the same fork. Two teams, one Postgres instance, and a table that both of them write to. It works, it is fast, and it costs nothing to set up. That is exactly why it is dangerous.
The cost is not performance. Postgres will happily carry far more load than most companies ever give it. The cost is that the schema quietly becomes an API that nobody owns, nobody versions, and nobody can deprecate.
The failure mode
Here is the shape it takes. Team A adds a column with a default and a NOT NULL
constraint. Team B’s writer, deployed twenty minutes earlier, does not know about
the column. Inserts start failing in a service whose code did not change.
-- Team A's migration, entirely reasonable in isolation
ALTER TABLE invoices
ADD COLUMN settlement_channel text NOT NULL DEFAULT 'ach';
The incident review blames the migration. The next quarter introduces a migration review board. Six months later the board is the bottleneck and the shared table has four more columns.
What I ask instead
Before anyone argues about microservices, I ask a team to write down which service owns each table. Not which service reads it — which one is allowed to write it.
// ownership.ts — checked into the repo that owns the schema
export const TABLE_OWNERS = {
invoices: 'billing',
invoice_lines: 'billing',
settlement_events: 'payouts',
customers: 'accounts',
} as const;
export type TableName = keyof typeof TABLE_OWNERS;
Writing the list is the whole exercise. It takes an afternoon and it surfaces every argument that matters: the two tables nobody claims, the one table three teams claim, and the join that only works because two services happen to share a transaction.
Splitting without a rewrite
Once ownership is written down, the split is mechanical and can be done one table at a time, in production, without a migration weekend.
- Revoke write access for every service but the owner.
- Give the non-owners a thin read view and a write endpoint on the owner.
- Move the table to its own schema, then its own database, when it actually hurts.
REVOKE INSERT, UPDATE, DELETE ON invoices FROM payouts_service;
GRANT SELECT ON invoices TO payouts_service;
Most teams stop after step two and are fine for years. The point was never to have a database per service. The point was to know who to wake up.
The rule I actually use
A shared database is fine right up until two teams disagree about a column. After that, the cheapest thing you ever built starts charging interest, and it charges it in incidents rather than in dollars, which makes it very easy to ignore.