One team's migration breaks another team's service
“We deployed a harmless column rename and a different service started failing. Nobody knows who reads which tables, and a batch job from another team keeps locking ours.”
ERROR CODES YOU MAY SEE
A code is a clue. Use its meaning and surrounding evidence to narrow the cause.
42703PostgreSQL SQLSTATE · standardA query referenced a column that does not exist (undefined_column), for example: column "x" does not exist.
If the query worked yesterday, someone renamed or dropped the column. Find every reader of that table, including other services, reports, and ETL jobs, before changing it back.
Official reference for 42703 (opens in new tab)Code definition reviewed42P01PostgreSQL SQLSTATE · standardA query referenced a table or view that does not exist (undefined_table), for example: relation "x" does not exist.
After a migration, this usually means another component still reads a table that was renamed, moved to another schema, or dropped.
Official reference for 42P01 (opens in new tab)Code definition reviewed55P03PostgreSQL SQLSTATE · standardA lock could not be acquired, for example when lock_timeout expires or NOWAIT is used (lock_not_available).
For a migration this is the safe failure: the DDL gave up instead of queueing traffic. Find the blocker with pg_blocking_pids() and retry.
Official reference for 55P03 (opens in new tab)Code definition reviewed
THE PRINCIPLE
Sharing a database server is fine; sharing a schema is shared, unversioned API surface. Data has one owner, and everyone else goes through a contract.
FIRST MOVES
- Inventory who touches what: per table, list the database roles, services, reports and ETL jobs that read or write it (query logs, pg_stat_statements, per-service DB users).
- Assign one owning service per table. Writes from anyone else are the first thing to remove; route them through the owner's API or commands.
- Give readers an explicit contract instead of raw tables: an owner-published API, a versioned read view, or events that let them build their own copy.
- Give each service its own DB role and grant it only its own schema, so new cross-service queries fail in review instead of in production.
- Until consumers are migrated, make schema changes expand-and-contract: add, dual-write or backfill, move readers, then drop.
- If the same team owns every service touching the tables and they always ship together, merging them into one service (or one modular monolith with a schema per module) is often cheaper than building contracts between them.
TOOLS: THEN → NOW
PATTERN SNAPSHOT
-- Run as orders_svc: it owns its schema; nobody else gets grants on it.
CREATE SCHEMA orders AUTHORIZATION orders_svc;
-- Readers get a versioned contract, not the tables behind it.
CREATE SCHEMA orders_api;
CREATE VIEW orders_api.order_summary_v1 AS
SELECT id, customer_id, status, total_cents FROM orders.orders;
GRANT USAGE ON SCHEMA orders_api TO billing_svc;
GRANT SELECT ON orders_api.order_summary_v1 TO billing_svc;CLOSE THE AI. EXPLAIN THIS.
Why is sharing a physical database server acceptable while sharing tables is not, and what is the first kind of cross-service access you should remove?RELATED SYMPTOMS
SOURCES
Guide reviewed