Why architecture matters here

Deadlock handling is architectural because it is the price of pessimistic concurrency control, and pessimistic locking is how most relational databases give you serializable or near-serializable isolation. Locks are what stop two transactions from corrupting each other's view of the data, but the instant you have locks that transactions hold across multiple operations, you have the possibility that their acquisition orders cross and form a cycle. You cannot have the isolation guarantees without the locks, and you cannot have long-lived locks without the risk of deadlock; so deadlock handling is not a bug to be eliminated but a structural consequence of the isolation model that must be managed. The question is never 'how do we prevent all deadlocks' in the abstract — it is 'how do we detect and resolve them cheaply, and how do we make them rare'.

The reason detection rather than pure prevention is the common default comes down to a cost trade. Preventing deadlocks entirely — by forcing every transaction to acquire locks in a globally consistent order, or to declare all its locks up front — is possible but expensive and rigid: it demands that application code know its full lock set in advance and cooperate on ordering, which real codebases with dynamic queries cannot always do. Detection, by contrast, lets transactions acquire locks naturally and only pays a cost when a cycle actually forms, which under a well-designed schema is rare. The database absorbs the complexity of maintaining the wait-for graph and searching it, in exchange for letting application developers write straightforward transactional code most of the time.

But the resolution — aborting a victim — pushes a real obligation back onto the application, and that is the part developers most often miss. A transaction that is chosen as a deadlock victim does not fail quietly in the background; it returns an error, and its work is rolled back as if it never happened. Correct code must catch that specific error and retry the transaction, because a deadlock abort is a transient failure, not a logic error: the same transaction retried a moment later, when the conflicting party has moved on, will usually succeed. Code that treats a deadlock abort as a fatal error, or worse ignores the error and assumes the write happened, turns a routine concurrency event into data loss or a user-visible crash. The architecture of deadlock handling therefore spans the boundary between database and application: the database detects and resolves, but the application must retry.

There is a deeper point about why keeping deadlocks rare matters as much as handling them correctly, and it is about throughput under contention. Every deadlock costs a full transaction's worth of wasted work — the victim did everything up to the abort for nothing, then has to redo it on retry — and every retry re-enters the same contended lock region, so a schema that produces frequent deadlocks does not merely annoy developers; it burns CPU and lock-manager time on work that is thrown away and redone, and it does so precisely when the system is busiest, because deadlocks cluster under high concurrency. Two systems can have identical detection machinery and wildly different real throughput purely because one accesses rows in a consistent order and the other does not. This is why the operational levers that reduce deadlock frequency — consistent lock ordering, short transactions, narrow lock scope — belong in the architecture conversation alongside the detection algorithm itself: the cheapest deadlock is the one that never forms, and the detector is the safety net for the ones your discipline failed to prevent, not the primary strategy.

Advertisement

The architecture: every piece explained

Start with the lock manager. It is the component that grants and blocks lock requests: when a transaction asks to lock a row, the manager either grants it (if compatible with existing locks) or blocks the transaction until the conflicting lock is released. The two transactions at the top of the diagram — A holds R1 and wants R2, B holds R2 and wants R1 — are the canonical deadlock: each is blocked in the lock manager waiting for a lock the other holds. The lock manager is where the raw material for detection lives, because it knows exactly which transaction holds which lock and which transaction is waiting on which.

From that information the database builds the wait-for graph: a directed graph with one node per transaction and an edge from A to B whenever A is blocked waiting for a resource B holds. In the deadlock above there is an edge A→B (A waits for B's R2) and an edge B→A (B waits for A's R1) — a two-node cycle. The cycle detector searches this graph for such loops. It can run eagerly, doing a depth-first search from a transaction the moment it blocks (fast detection, work on every block), or lazily, running a periodic scan of the whole graph every so often (cheap per-block, up to one scan-interval of detection latency). Real databases pick a point on this spectrum based on how costly a stalled transaction is versus how much graph maintenance they want to pay.

When a cycle is found the victim policy chooses which transaction in it to abort. Common heuristics: kill the youngest transaction (it has likely done the least work and disrupting it wastes the least), or the one holding the fewest locks or having done the least work (minimize rollback cost), sometimes weighted to avoid repeatedly victimizing the same transaction and starving it. The chosen victim is subjected to abort and rollback: its changes are undone and, crucially, its locks are released — which is what lets the survivors proceed, since the resource each was waiting for is now free.

The bottom row holds the alternatives. A timeout fallback is the crude but cheap option: instead of maintaining a graph, simply abort any transaction that has waited for a lock longer than some threshold. It catches deadlocks (a deadlocked transaction waits forever, so it will time out) but also aborts transactions merely stuck behind a long legitimate wait, and it adds detection latency equal to the timeout. Wound-wait and wait-die are prevention schemes that use transaction timestamps to decide whether a transaction waits or aborts on conflict, structured so a cycle can never form — trading some unnecessary aborts for a guarantee of no deadlock. The ops strip is the discipline that keeps deadlocks rare and recoverable: acquire locks in a consistent order, keep transactions short, monitor the deadlock rate, and always retry on abort.

Deadlock detection — find the cycle, break it, let the rest proceedwait-for graph, cycle search, victim selectionTxn A holds R1wants R2Txn B holds R2wants R1Lock managergrants / blocksWait-for graphedges = who waitsCycle detectorDFS / periodic scanVictim policyleast work / youngestAbort + rollbackrelease victim's locksSurvivors proceedunblockedTimeout fallbackcoarse, cheapWound-wait / retryprevention schemesOps — consistent lock order, short txns, watch deadlock rate, retry on abortwaitwaittracksearchorpickfreeoperateoperate
Deadlock detection: the lock manager records who waits for whom in a wait-for graph; a cycle detector finds a loop, a victim policy picks the cheapest transaction to abort, its locks are released, and the surviving transactions proceed.
Advertisement

End-to-end flow

Trace the classic two-transaction deadlock under an eager detector. Transaction A begins, locks row R1 (a user's account), and does some work. Concurrently transaction B begins, locks row R2 (another account), and does its work. Now A asks to lock R2 to complete a transfer; the lock manager sees R2 is held by B and blocks A, adding edge A→B to the wait-for graph. A moment later B asks to lock R1; the lock manager sees R1 is held by A and blocks B, adding edge B→A. The eager detector, triggered by B blocking, runs a DFS from B, follows B→A→B, and finds the cycle. A deadlock is confirmed within microseconds of it forming.

Resolution follows immediately. The victim policy compares A and B: say B has done less work and holds fewer locks, so B is chosen. B is aborted, its changes rolled back, and its lock on R2 released. That release unblocks A — the lock manager grants A the R2 lock it was waiting for — and A proceeds to commit its transfer. B, meanwhile, receives a deadlock error. Because B's application code is written correctly, it catches the specific deadlock error code, waits a brief randomized moment, and retries the whole transaction. On retry A has committed and released its locks, so B acquires R1 and R2 cleanly and commits. The end state is correct: both transfers happened, one just took a retry.

Now contrast with a timeout-only system on the same scenario. There is no wait-for graph; the lock manager only knows A and B are each waiting. Nothing detects the cycle. Both transactions sit blocked until the lock-wait timeout — say ten seconds — expires. Then one of them (whichever's timer fires) is aborted for waiting too long, the other proceeds, and the aborted one retries. The final result is the same as the graph-based case, but it took ten seconds instead of microseconds, and during those ten seconds any other transaction queuing behind R1 or R2 was also stalled. Worse, the timeout cannot distinguish a genuine deadlock from a transaction merely stuck behind a slow-but-legitimate long lock hold, so it also aborts innocent waiters. This is the trade: timeouts are trivially cheap to implement but blunt and slow; graph-based detection is precise and fast but must maintain and search the wait-for graph.

Consider the pathological version that good design prevents. Imagine dozens of transactions all touching the same set of rows but in inconsistent order — some lock R1 then R2, others lock R2 then R1. Under load this manufactures deadlocks continuously: the detector is constantly finding cycles, constantly aborting victims, and the aborted transactions constantly retry back into the same contended region and deadlock again. The detector is working perfectly, yet throughput collapses because the workload is generating deadlocks faster than resolving them helps. The fix is not a better detector — it is the ops-strip discipline of consistent lock ordering: if every transaction always locks R1 before R2, the wait-for graph can never form the crossing edges that make a cycle, and the deadlocks vanish at the source. The detector remains as the safety net for the rare cycle that slips through, but the primary defense is a workload that does not create cycles in the first place.