Pattern · seen in 1 breakdown across 1 company

Loose Foreign Keys

Definition

When a table boundary hardens into a database boundary — decomposition, sharding, service extraction — foreign keys across the line must die, but the semantics they enforced (cascading DELETE, cascading NULLIFY) still need to hold. Replace the constraint with asynchronous convergence: an on-delete trigger on the parent table records each deletion into a queue table inside the parent's own database, and a periodic worker drains the queue, applying the cascade to child records across the boundary in controlled batches. Triggers, unlike application-level delete callbacks, cannot be skipped and fire for bulk deletes, so the capture side keeps database-grade guarantees; only the enforcement becomes eventual.

The pattern's second face is a cure for a wound synchronous cascades inflict even inside one database: a parent with millions of children no longer detonates a single giant transactional delete — the worker controls batch size and pacing, so cleanup never times out and never overloads the store. The costs are the async ones: a convergence window during which orphaned children exist and queries must tolerate them (or filter by parent existence), a queue table and worker that must be monitored like any pipeline, and the loss of the constraint's read-time guarantee — nothing prevents inserting a child for a deleted parent except application discipline. Boundary against Content-Free Change Events: that pattern signals identity and re-reads truth to synchronize derived stores; this one carries the delete intent itself in-queue and mutates authoritative child tables. Boundary against real foreign keys: choose loose only when the boundary makes real ones impossible — the pattern is a bridge across a line you drew, not an upgrade.

When it applies

01Database decomposition or service extraction severs existing foreign keys whose cascade semantics the application still depends on
02Bulk and cascading deletes at volumes where synchronous enforcement causes lock contention or statement timeouts
03Any cross-store parent-child relationship where deletes must eventually propagate and brief orphan windows are tolerable

Tradeoffs

Convergence windows are real: child records outlive their parents until the worker arrives, and every consumer of the child tables must tolerate or filter the orphans
The capture trigger is only half the guarantee — insert-side integrity (no new children for dead parents) has no enforcement left and falls entirely to application discipline
The queue and its worker are new operational surface: backlog growth, batch sizing, and pacing now determine how stale referential integrity is allowed to get

The same move, 1 ways

Every row is a production system that bet on this pattern — the note says how, in that system's own terms.

GitLab
GitLab Blog (Engineering)
2022
GitLab's own name for this, and it fits: replace the foreign keys between the two databases with Postgres triggers. A foreign key normally makes the database clean up child rows when their parent is deleted. Since that cannot work across two databases, a trigger fires on each delete (including bulk deletes, which some app-level code misses) and adds the parent to a queue. A background job then cleans up the child rows a moment later, still honoring delete or set-to-null. A nice side effect: the giant chained deletes that used to time out now run in small, steady batches. Read the breakdown →

Problems this pattern answers

The walls where its breakdowns live — each opens the cross-company comparison.