Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Transactional Messaging
HLD

Transactional Messaging

The dual-write problem and its fixes — atomicity between your database and the message broker.

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.

My Private Notes

Notes are auto-saved locally to this device.