← Back to the library
ArchitectureApplied · 8 min

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 · standard

A 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 reviewed
42P01PostgreSQL SQLSTATE · standard

A 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 reviewed
55P03PostgreSQL SQLSTATE · standard

A 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

  1. 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).
  2. 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.
  3. 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.
  4. 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.
  5. Until consumers are migrated, make schema changes expand-and-contract: add, dual-write or backfill, move readers, then drop.
  6. 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

One application user with access to every tableA role and schema per service, with grants limited to its own data
Cross-service joins in SQLOwner APIs, versioned read views, or event-fed local copies
Coordinated migrations across teamsExpand/contract migrations behind a contract the owner versions

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?

SOURCES

Guide reviewed