The Dual-Write Problem
code that writes DB *and* publishes an event:
def place_order(order):
db.save(order) # ①
broker.publish(OrderPlaced) # ② ← crash HERE?
database has the order. NOBODY was notified. forever.
silent inconsistency — no error, no retry, just drift.
reverse order, same hole:
publish succeeds → db write fails → phantom event.
TWO systems, no shared transaction = this gap is STRUCTURAL.
every naive dual-write has it. every one.
Fix 1: Transactional Outbox (the standard)
write the event INTO THE DATABASE, atomically:
BEGIN;
INSERT INTO orders (...); -- business row
INSERT INTO outbox(payload, status) -- event as row!
VALUES (OrderPlaced, 'pending');
COMMIT; ← both or neither ✓
separate RELAY publishes outbox rows to broker:
[outbox table] ──poll/CDC──► [relay] ──► [broker]
│
mark published
relay failure? retries safely (at-least-once to broker).
consumers idempotent anyway (they must be!). consistent ✓
Fix 2: Change Data Capture
skip explicit outbox — make the BUSINESS WRITE the event:
db.commit(order) ──► log ──► [debezium-style CDC] ──► broker
"orders table changed" IS the OrderPlaced event source.
vs outbox:
+ zero extra write path; can't forget the event
− event shape coupled to table schema
− CDC pipeline is its own ops surface
both fix the same hole via different plumbing;
outbox = more control, CDC = less code. team call.
Why Not Just Retry The Whole Thing?
"wrap both in try/retry" doesn't work:
db.save(order) ✓ committed
publish(...) ✗ fails → retry publish → fails again...
give up? order saved, event lost (original problem).
ROLLBACK db? too late — it's committed!
distributed transactions across DB+broker (2PC/XA):
exist, mostly avoided — lock/latency costs + broker support
spotty + operational fragility at scale.
outbox achieves the same ATOMICITY with local transactions only.
Reading Events in the Same Transaction?
related trap: consumer reading DB state not yet visible:
producer commits AFTER relay read outbox row?
→ event published for transaction still in-flight!
outbox discipline: only emit COMMITTED rows
- poll on committed snapshot ✓
- CDC reads commit log = post-commit by definition ✓
sequence numbers let consumers reorder-safe anyway.
Interview Framing
“Order service saves to Postgres and emits to Kafka — reliability review?” scored find: name dual-write as THE flaw unprompted, implement outbox (same-transaction insert + relay), mention CDC alternative with tradeoff, note relay’s at-least-once pushes idempotency duty downstream (linking lessons). Spotting dual-write in a design review is a top-tier senior signal — this lesson is how you earn it.
Premium Content
Unlock Transactional Messaging and all premium lessons with a subscription.
All premium lessons
Ad-free experience
Priority support
From ₹199.99/year — See plans