CMD Guide
HomeSystem DesignMental Models & Systems Thinking

Mental Models II — Coordination Avoidance, Dual-Write/Outbox, Latency Physics, Circuit Breakers & Framing Rules (Deep Dive)

The mental model behind the mental models

Every hard distributed-systems decision reduces to one question: is this constraint a law of physics/logic, or a dial you're free to turn? Coordination, latency, and failure each have a "floor" you cannot engineer past — and a lever you can pull instead. This page is the second layer of intuition on top of the guide's existing mental models (fan-out tail latency, idempotency, the consistency spectrum, back-pressure, mechanical sympathy, the USE method, time/clocks, designing for failure) — it adds the ones those pages don't cover: the dual-write/outbox trap, coordination-avoidance (the CALM theorem), the physics of cross-region latency, the circuit-breaker timeout-budget rule, and the framing rules for reaching for the right model under time pressure.

1. The dual-write problem and the outbox mental model

Writing to your database and publishing to a queue are two separate systems with no shared transaction. Do them one after another and a crash in between either loses the event (DB committed, publish never happened) or double-emits (publish succeeded, then the DB transaction rolled back) — this is the dual-write problem, covered mechanically with its own diagram in Transactional Outbox. The fix there is the same fix restated as a mental model: never let "write my state" and "tell the world" be two atomicity domains. Fold the second into the first by writing an outbox row in the same local DB transaction, then let a separate relay (poller or CDC log-tailer) publish it later. Atomicity moves from "DB + broker" (impossible without 2PC) to "DB + DB" (a single ACID transaction) — the broker gets involved only after the fact is already durable.

The part that mental-model pages skip: the outbox row itself needs a lifecycle, not just an insert. Give every row a status (PENDINGSENT) and a lease/timeout — when the relay picks up a PENDING row it stamps a leased_until timestamp instead of deleting it immediately. If the relay crashes mid-publish, the row is still there; once leased_until expires, a recovery sweep re-claims it and republishes. Trace a crash through it:

tEventOutbox row state
t0Order + outbox row inserted in one DB transaction, commit succeedsPENDING, no lease
t1Relay A polls, claims the row, sets leased_until = t1+30sPENDING, leased by A
t2Relay A publishes to the broker successfully...(in flight)
t3...but Relay A crashes before marking the row SENTPENDING, stale lease
t1+30sLease expires; recovery sweep sees PENDING + expired leaseeligible for re-claim
t4Relay B re-claims and republishes the same eventduplicate delivery
t5Consumer processes the duplicate; its idempotency key (order id) matches a record it already applied — it discards the repeat, keeping only the first applicationSENT, terminal

The recovery rule is simple — on ambiguity, re-send — because the outbox can only guarantee at-least-once delivery, never exactly-once. That is a deliberate trade, not a gap: the hard part (collapsing duplicates into one effect) is explicitly pushed onto the consumer via an idempotency key. Outbox solves "did the event get out at all"; idempotency solves "what happens when it gets out twice." Neither one alone is the fix — the pairing is.

2. Coordination-avoidance: the CALM theorem

The deeper reason the outbox pattern reaches for "avoid the join" instead of "add a distributed transaction" is a general principle: coordination is the expensive thing in a distributed system — expensive in latency (a round trip per decision), in availability (a coordinator that must be reachable), and in complexity. The end-to-end argument says the same thing from a different angle: don't pay for a guarantee in the middle of the system (say, a broker doing exactly-once) if the endpoint has to re-verify it anyway (the consumer's idempotency check) — build the guarantee once, at the edge that actually needs it, not at every hop.

The CALM theorem (Consistency As Logical Monotonicity, Hellerstein & Alvaro) makes this precise: a program has a coordination-free, eventually-consistent implementation if and only if it is monotonic — every step only adds facts or refines a conclusion, and never has to retract one already announced. Concretely:

The design lever falls straight out of the theorem: reformulate a non-monotonic operation as a monotonic one and the coordination requirement disappears. A delete becomes a monotonic add of a tombstone fact (the record isn't retracted, a "deleted" marker is appended — this is exactly what CRDTs and log-structured stores do). A running total with corrections becomes a monotonic append of adjustment events summed at read time, instead of an in-place decrement. What's left after every reformulation you can manage — uniqueness constraints, "never go negative" invariants, single-leader elections — is the genuine, irreducible coordination in your system; that's where you spend a consensus protocol (Raft/Paxos) or a serializing lock, deliberately and sparingly, rather than by default.

3. Latency physics: the speed-of-light floor

Cross-region latency is not a performance bug waiting for a faster network card — it is arithmetic. Light in vacuum travels at c ≈ 300,000 km/s; in optical fiber it travels at roughly ⅔ c ≈ 200,000 km/s (the glass's refractive index slows it down). New York to London is about 5,600 km of undersea cable. Trace the calculation:

StepArithmeticResult
One-way propagation5,600 km ÷ 200,000 km/s28 ms
Round-trip floor2 × 28 ms56 ms
Real measured RTTfloor + routing/queuing/serialization overhead≈ 70–80 ms

That 56ms is a hard floor — no amount of bandwidth, caching within the request, or code optimization changes it, because it is bound by the length of the wire and the refractive index of glass, not by any software you control. The only lever is to not make the trip: serve from an edge/CDN node near the user, keep a regional read replica so most reads never cross an ocean, replicate asynchronously so the write path doesn't wait on the far region, or bundle several round trips into one (batching, HTTP/2 multiplexing). This is the same "law vs. lever" split as tail-latency thinking and the same reason fan-out amplification matters — you cannot out-engineer physics, only route around it.

Diagram: New York to London latency floor calculation — 5,600km at ~200,000km/s gives a 28ms one-way, 56ms round-trip physical floor; real RTT is ~70-80ms; the only lever is avoiding the round trip via edge caching and regional replicas.
Diagram: New York to London latency floor calculation — 5,600km at ~200,000km/s gives a 28ms one-way, 56ms round-trip physical floor; real RTT is ~70-80ms; the only lever is avoiding the round trip via edge caching and regional replicas.

4. Circuit breakers and the timeout-budget rule

The mechanism (closed → open → half-open, with a worked failure/recovery trace and a step-through debugger) already has its own deep page: Circuit Breaker — The State Machine — reach for that page for the state-transition detail. What belongs here is the mental model interviewers actually probe once you know the three states: a circuit breaker only protects you from a dependency that is already down; it does nothing about a timeout that is set wrong in the first place, and most production incidents are the second problem.

The rule: per-hop timeouts must sum to less than the caller's overall budget. If a client gives itself a 500ms budget for a request, and every service in a 3-hop chain (Gateway → Service A → Service B → DB) sets its own downstream timeout to that same 500ms, the chain doesn't fail in 500ms — the client gives up at 500ms while A, B, and the DB call are all still separately waiting on their own 500ms clocks, each started slightly later than the one before it. Every hop is holding a thread/connection for work whose answer is already useless. Trace the fix — a shrinking waterfall, each hop's timeout carved out of what its parent has left, sized to the downstream's measured p99, not to "whatever budget remains":

HopBudget it receivedDownstream p99Timeout it sets on the next call
Client → Gateway500 ms (total)500 ms
Gateway → Service A500 ms − ~20ms own overheadA's p99 ≈ 300 ms350 ms (margin over p99, still < remaining budget)
Service A → Service B350 ms − ~20ms own overheadB's p99 ≈ 150 ms200 ms
Service B → DB200 ms − ~20ms own overheadDB p99 ≈ 120 ms150 ms

Each row's timeout is strictly smaller than what its caller has left, and is anchored to what the downstream actually does (its p99), not copy-pasted from the top. This is also exactly where the breaker's own threshold should be set: trip on a failure/latency signal measured against that same p99, not an arbitrary round number — a breaker tuned looser than the real p99 never trips before users feel it; tuned tighter than natural variance, it trips on noise.

5. Framing rules — reaching for the right model under time pressure

A decision rule, problem-shape → reach for:

Under real interview time pressure, a 6-step framework (Requirements → Estimation → API → Data model → High-level design → Deep dive) rarely fits in full. Cut in this order when the clock is short: (1) trim estimation to stated assumptions instead of derived arithmetic — say "assume 10M DAU, ~1K QPS peak" rather than deriving it live; (2) pick one component to go deep on rather than spreading detail evenly — depth on one thing reads stronger than a shallow pass over six; (3) never cut requirements or the high-level design — those are what the interviewer is actually grading.

Two independence caveats worth stating out loud in an interview, because both are commonly assumed away and both are wrong in practice:

Pitfalls

Judgment layer — when to coordinate vs. avoid

Default to avoidance: reformulate for monotonicity, push idempotency to the edge, cache to dodge the round trip, fail fast with a breaker instead of queuing behind a dead dependency. Reach for real coordination (consensus, distributed locks, synchronous cross-region commit) only for the small, named set of invariants that are truly non-monotonic — uniqueness, "never negative," single-leader — and even then, prefer to shrink the blast radius of that coordination (partition it, scope it to a region, batch it) rather than doing it on every request. Size breaker thresholds and per-hop timeouts to the downstream's measured p99, never to a round number pulled from habit — a badly-set timeout defeats the point of a well-designed breaker.

What to put on a dashboard for this. Each lever has a signal that tells you it is misbehaving: outbox age — the age of the oldest PENDING row and the relay's publish lag (a rising oldest-age means the relay is wedged or the broker is down, and events are silently backing up); orphaned work — count of requests where the client already timed out but a downstream call is still in flight (the copy-the-budget bug in §4); and cross-region p50 sitting near the physics floor (e.g. ~70–80ms NY–London) — when it does, stop filing "the app is slow" tickets, because you are at the wire's limit and only routing around it (edge/replica) will move the number.

Takeaways

Related pages


Re-authored/Deepened for this guide.

🤖 Don't fully get this? Learn it with Claude

Stuck on Mental Models II — Coordination Avoidance, Dual-Write/Outbox, Latency Physics, Circuit Breakers & Framing Rules (Deep Dive)? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Mental Models II — Coordination Avoidance, Dual-Write/Outbox, Latency Physics, Circuit Breakers & Framing Rules (Deep Dive)** (System Design) and want to truly understand it. Explain Mental Models II — Coordination Avoidance, Dual-Write/Outbox, Latency Physics, Circuit Breakers & Framing Rules (Deep Dive) from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Mental Models II — Coordination Avoidance, Dual-Write/Outbox, Latency Physics, Circuit Breakers & Framing Rules (Deep Dive)** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Mental Models II — Coordination Avoidance, Dual-Write/Outbox, Latency Physics, Circuit Breakers & Framing Rules (Deep Dive)** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Mental Models II — Coordination Avoidance, Dual-Write/Outbox, Latency Physics, Circuit Breakers & Framing Rules (Deep Dive)** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes