Home
ArenaGraphSignalTopics
/Apache Kafka and Event-Driven Systems: Building Real-Time Streaming Pipelines
Chapter 1 • Module 3 7 min breakdown +15 XP Module

The Dual-Write Problem in Distributed Systems and How to Solve It

From Track:Apache Kafka and Event-Driven Systems: Building Real-Time Streaming PipelinesEvent-Driven Architecture & Distributed Systems

When developing microservices that interface with both a relational database (PostgreSQL, MySQL) and a message broker (Apache Kafka, RabbitMQ), software engineers almost universally encounter the Dual-Write Problem.

The Dual-Write Problem is an unavoidable distributed consistency dilemma: It is physically impossible to execute an atomic, all-or-nothing write across two heterogeneous, uncoordinated storage systems over an asynchronous network without specialized protocols.

Naive code that updates a database and publishes an event in the same endpoint will inevitably cause split-brain data corruption, phantom events, or silent message loss under production failure conditions.

Interactive Blueprint
Rendering diagram...

1. Deconstructing the Dual-Write Traps

Let us dissect the two naive implementation orders to expose why neither can guarantee consistency.

Trap 1: "Write to Database First, Then Publish to Kafka"

Consider this standard endpoint implementation found in countless production codebases:

typescript
Loading code editor...

The Failure Surface:

Between the microsecond PostgreSQL finishes committing its Write-Ahead Log (WAL) to NVMe disk and the microsecond Kafka acknowledges the message write, dozens of catastrophic failure modes can occur:

  1. Application Crash / OOM: The Node.js/JVM process runs out of memory or is killed by Kubernetes (OOMKilled or SIGKILL during deployment).
  2. Network Partition to Kafka: The network switch to the Kafka broker times out.
  3. Kafka Broker Transient Error: Kafka's buffer pool is full (TimeoutException: Failed to allocate memory within max.block.ms).

The Resulting Corruption (Silent Data Loss):

The order is permanently stored in PostgreSQL. The user sees a 500 Internal Server Error in their browser and assumes the checkout failed. However, the order was actually created in the DB, but Kafka was never notified.

  • The Billing Service never charges the card.
  • The Warehouse Service never packs the item.
  • The Analytics dashboard misses the revenue.
  • The customer is stuck in an inconsistent "ghost order" state that requires manual database repair scripts.

Trap 2: "Publish to Kafka First, Then Write to Database"

Attempting to fix Trap 1 by reversing the write order creates an even more catastrophic failure mode:

typescript
Loading code editor...

The Failure Surface:

Once a message is committed to Kafka's distributed commit log, it is immutable and instantly visible to downstream consumers (Inventory, Payment, Logistics). If Step 2 encounters:

  1. An SQL constraint violation (e.g., duplicate key value violates unique constraint).
  2. An optimistic locking concurrency conflict (version mismatch).
  3. A transient database connection pool timeout.
  4. A disk full error on the primary database.

The Resulting Corruption (Phantom Event Disaster):

The database transaction rolls back completely. The order does not exist in the database. However, downstream services consumed OrderCreated seconds ago:

  • The Payment Service charged the customer's credit card \500$.
  • The Warehouse Service printed a shipping label for order ID ord_9981.
  • When the Warehouse Service attempts to query GET /api/orders/ord_9981 to verify shipping contents, it receives 404 Not Found.

Phantom events represent the worst category of distributed data corruption because reversing external side effects (like bank credit card charges) requires expensive manual human intervention.


2. Why Can't We Use Distributed Transactions (2PC / XA)?

A common question from engineers with relational database backgrounds is: "Why can't we wrap both PostgreSQL and Kafka in a Two-Phase Commit (2PC / XA) transaction?"

Interactive Blueprint
Rendering diagram...

The Technical Realities:

1. Lack of Support in Modern Event Streaming Platforms

Apache Kafka does not implement the XA / Open Group distributed transaction standard. Kafka has its own internal transactional coordinator (optimized for Kafka-to-Kafka exactly-once stream processing via Kafka Streams), but it cannot be enlisted as an XA Resource Manager alongside a relational database.

2. The Blocking Nature and Lock Contention of 2PC

In Phase 1 of 2PC, the database must acquire and hold exclusive row-level and table-level locks on all modified records while waiting for the coordinator to send the GLOBAL_COMMIT command.

  • If the network between the Coordinator and Kafka experiences a latency spike, the database locks are held for .
  • Under high concurrency (), holding transactional database locks across network RPCs triggers catastrophic lock escalation, connection pool starvation, and database deadlocks.

3. Single Point of Failure (Coordinator Crash)

If the 2PC coordinator crashes after the database votes to commit but before the commit decision is broadcast, the database is left in an in-doubt (uncertain) state. The database must keep the row locks open indefinitely until the coordinator recovers, blocking all subsequent transactions.

[!CAUTION] Production Rule: Never attempt distributed 2PC transactions across application code, relational databases, and message brokers. 2PC guarantees consistency by sacrificing availability and throughput ( in CAP theorem), making it completely unsuitable for cloud-native microservices.


3. The Theoretical Root: The Two Generals' Problem

The Dual-Write Problem is a direct manifestation of the Two Generals' Problem, a proven fundamental impossibility result in computer science:

Interactive Blueprint
Rendering diagram...

In an asynchronous network where packets can be delayed or dropped arbitrarily, two independent nodes can never reach guaranteed common knowledge of state through an uncoordinated exchange of messages.

Therefore, the system architecture must be restructured so that state transitions rely on a single, local atomic boundary.


4. Architectural Solutions to the Dual-Write Problem

To eliminate dual writes, we must follow the Single Atomic Commit Principle: Every business operation must modify exactly one atomic system of record in a single local transaction.

There are two primary industry-standard patterns used by top-tier engineering organizations (Netflix, Uber, Stripe, Airbnb) to solve this:

Interactive Blueprint
Rendering diagram...

Solution 1: The Transactional Outbox Pattern

Instead of attempting to write to Kafka in the application endpoint, the application writes both the business data (orders table) and the domain event (outbox_events table) inside the exact same local database ACID transaction.

sql
Loading code editor...

An independent, background Outbox Relay process (either an asynchronous poller or a Change Data Capture connector) reads rows from outbox_events and publishes them to Kafka with automated retries until acknowledged.


Solution 2: Log-Based Change Data Capture (CDC)

In pure Log-Based CDC, the application does not even maintain an outbox table. The application performs standard SQL queries against business tables. A specialized CDC daemon (such as Debezium) attaches directly to the database engine's transaction log (PostgreSQL Write-Ahead Log (WAL) via logical decoding plugins like pgoutput, or MySQL Binlog).

  • Whenever a transaction commits, the database engine writes the byte stream to its WAL.
  • Debezium reads the WAL stream with sub-millisecond latency and automatically produces structured JSON/Avro change events to Kafka topics.
  • Zero dual writes, zero application-level messaging code, and zero performance overhead on application thread pools.

5. Failure Mode Comparison: Naive vs Outbox vs CDC

ScenarioNaive Dual-Write (DB First)Naive Dual-Write (Kafka First)Transactional Outbox PatternLog-Based CDC (Debezium)
App crashes after DB writeData Loss: Event never sent to Kafka.N/ASafe: Outbox row on disk; relay picks it up on restart.Safe: WAL committed; CDC picks up from WAL offset.
App crashes before DB write✅ Safe (Nothing written).Phantom Event: Event in Kafka, no DB row.✅ Safe (Transaction rolled back completely).✅ Safe (WAL rolled back).
Kafka cluster goes offline for 1 hour❌ App throws 503 errors; orders cannot be placed.❌ Complete system outage.Safe: Orders continue placing; outbox rows accumulate in DB and drain when Kafka recovers.Safe: DB transactions succeed; WAL retains changes until Kafka recovers.
Downstream Consumer Duplicate RiskLow (if no retries).High.Requires Idempotent Consumer (At-Least-Once Delivery).Requires Idempotent Consumer (At-Least-Once Delivery).
Performance Overhead on DBMinimal.Minimal.Moderate (Extra INSERT + Polling query index load).Minimal (Sequential zero-copy WAL reading).

Summary and Key Takeaways

  1. The Dual-Write Problem is an inevitable physical consequence of uncoordinated writes across heterogeneous network boundaries.
  2. Writing to DB first causes silent data loss when application crashes or network flaps occur before message publishing.
  3. Writing to Kafka first causes phantom events when subsequent database constraints or transactional rollbacks fail.
  4. Distributed 2-Phase Commit (2PC / XA) is a distributed anti-pattern for cloud microservices due to blocking locks, extreme latency amplification, and lack of Kafka native support.
  5. The only reliable architectural solution is the Single Atomic Commit Principle, implemented via the Transactional Outbox Pattern or Log-Based Change Data Capture (CDC).
  6. In the next lesson, we will implement the complete Transactional Outbox and Inbox Patterns in TypeScript and PostgreSQL, building the production-grade foundation for our first Landmark Global Arena Capstone.
Milestone Verification

Ready for the next lesson?

Mark this module complete to record verified progress and earn +15 XP toward your architect profile.