Distributed Systems
Distributed Concurrency Control & Transactions: 2PC vs Saga Pattern, Outbox & Eventual Consistency

In modern microservice architectures and distributed databases, executing atomic operations across isolated data stores is one of the most critical challenges in systems engineering. When a single business workflow—such as processing an e-commerce checkout, executing an algorithmic trade, or orchestrating a real-time AI inference pipeline—spans multiple independent databases, the classic ACID guarantees of monolithic databases no longer apply.
Engineers face a fundamental trade-off: enforce strict synchronous serializability with high latency and blocking failure modes (Two-Phase Commit), or embrace asynchronous eventual consistency with compensating recovery workflows (the Saga Pattern).
This architectural guide breaks down distributed concurrency control mechanisms, compares 2PC against Sagas, analyzes the Transactional Outbox pattern, and provides concrete implementation blueprints for Staff+ system design interviews.
+───────────────────────────────────────────────────────────────────────────+
| DISTRIBUTED TRANSACTION CONTINUUM |
+───────────────────────────────────────────────────────────────────────────+
| |
| ACID (Strict Consistency) BASE (Eventual Consistency) |
| ◄───────────────────────────────────────────────────────────────────► |
| |
| [Two-Phase Commit (2PC)] [Try-Confirm-Cancel] [Saga Pattern] |
| - Synchronous blocking - Application-level - Asynchronous |
| - Distributed row locks - Two-phase reservation - Compensating |
| - High latency / low TP - Moderate throughput transactions |
| - Coordinator SPOF risk - Higher complexity - High throughput |
| |
+───────────────────────────────────────────────────────────────────────────+
1. Two-Phase Commit (2PC): Mechanics & Failure Modes
Two-Phase Commit (2PC) is an atomic commitment protocol that ensures all participating database nodes either commit or abort a distributed transaction together.
+───────────────────────────────────────────────────────────────────────────+
| TWO-PHASE COMMIT (2PC) TIMELINE |
+───────────────────────────────────────────────────────────────────────────+
| |
| COORDINATOR PARTICIPANT A PARTICIPANT B |
| │ │ │ |
| │ ── 1. PREPARE ("Can commit?") ─► │ |
| │ ── 1. PREPARE ("Can commit?") ─────────────────────────► |
| │ │ │ |
| │ ◄── "VOTE_COMMIT" (Locked) ───┤ │ |
| │ ◄── "VOTE_COMMIT" (Locked) ────────────────────────────┤ |
| │ │ │ |
| [Decide: Global Commit] │ │ |
| │ │ │ |
| │ ── 2. COMMIT ────────────────►│ │ |
| │ ── 2. COMMIT ─────────────────────────────────────────►│ |
| │ │ │ |
| │ ◄── "ACK_COMMITTED" ──────────┤ │ |
| │ ◄── "ACK_COMMITTED" ───────────────────────────────────┤ |
| │ │ │ |
| [Transaction Complete] │ │ |
| |
+───────────────────────────────────────────────────────────────────────────+
Phase 1: Prepare (Voting Phase)
- The Coordinator allocates a globally unique transaction ID ($TxID$) and writes a
START_2PCrecord to its Write-Ahead Log (WAL). - The Coordinator transmits a
PREPAREcommand over RPC to all participants ($P_1, P_2, \dots, P_n$). - Each participant executes the local transaction operations up to the commit point, acquires exclusive row-level locks, writes undo/redo logs to its local WAL, and replies with either
VOTE_COMMITorVOTE_ABORT.
Phase 2: Commit / Abort (Execution Phase)
- Unanimous Commit: If all participants vote
VOTE_COMMIT, the Coordinator writesGLOBAL_COMMITto its WAL and broadcastsCOMMITto all nodes. Each participant commits its local transaction, releases row locks, and sends anACK. - Partial Abort: If any single participant votes
VOTE_ABORTor times out, the Coordinator writesGLOBAL_ABORTto its WAL and broadcastsROLLBACK. All participants rollback local state using undo logs and release locks.
The Fatal Flaw: Blocking Under Coordinator Crash
2PC is a synchronous blocking protocol. If the Coordinator crashes after participants have voted VOTE_COMMIT but before broadcasting COMMIT, participants are left in an indeterminate state. They must hold row-level database locks indefinitely until the Coordinator recovers, leading to thread pool exhaustion, cascading latency spikes, and system-wide throughput collapse.
2. The Saga Pattern: Eventual Consistency via Compensation
The Saga pattern decomposes a distributed transaction into a sequence of local transactions (T_1, T_2, ..., T_n). Each local transaction updates a single microservice database and publishes an event or message.
If a local transaction T_k fails, the Saga orchestrator executes a sequence of compensating transactions (C_k-1, C_k-2, ..., C_1) in reverse order to undo changes and restore data consistency.
SUCCESSFUL SAGA EXECUTION:
[Order Service: Create Order (T1)] ──► [Payment Service: Charge Card (T2)] ──► [Inventory Service: Reserve Stock (T3)]
FAILED SAGA & COMPENSATING CHAIN:
[Order Service: Create Order (T1)] ──► [Payment Service: Charge Card (T2)] ──► [Inventory Service: Out of Stock (T3 FAIL!)]
│
[Order Service: Cancel Order (C1)] ◄── [Payment Service: Refund Card (C2)] ◄──────────┘
Orchestration vs Choreography
| Architectural Dimension | Choreography (Event-Driven) | Orchestration (State Machine) |
|---|---|---|
| Control Model | Decentralized; services react to domain events | Centralized orchestrator service directs step execution |
| Coupling | Loose coupling; publisher is unaware of consumers | Tighter coupling to the orchestrator state machine |
| Complexity | Simple for 2-3 services; chaotic cyclic graph at scale | Easy to visualize, monitor, and enforce timeouts |
| Testing & Observability | Difficult to trace distributed event flows | Straightforward; orchestrator logs all state transitions |
| Best Used For | Simple workflows with few participants | Complex business flows with conditional branching |
3. The Transactional Outbox Pattern
When using event-driven Sagas, microservices must atomically update their local database and publish a domain event to an external message broker (Apache Kafka, RabbitMQ). Performing these operations naively creates a dual-write race condition:
// ANTI-PATTERN: Non-Atomic Dual Write
await db.orders.insert(orderData); // Succeeds
await kafka.produce('order.created', orderData); // Network failure -> Inconsistency!
The Transactional Outbox Pattern solves this by writing domain entities and outgoing messages to the same relational database within a single atomic ACID transaction.
+───────────────────────────────────────────────────────────────────────────+
| TRANSACTIONAL OUTBOX ARCHITECTURE |
+───────────────────────────────────────────────────────────────────────────+
| |
| +───────────────────────────────────────────────────────────────────+ |
| | Microservice Local Transaction Boundary (ACID) | |
| | 1. INSERT INTO orders (...) | |
| | 2. INSERT INTO outbox_table (event_id, topic, payload, status) | |
| +─────────────────────────────────┬─────────────────────────────────+ |
| │ |
| ▼ (PostgreSQL WAL Engine) |
| +───────────────────────────────────────────────────────────────────+ |
| | Change Data Capture (CDC) via Debezium / Kafka Connect Engine | |
| +─────────────────────────────────┬─────────────────────────────────+ |
| │ |
| ▼ (At-Least-Once Delivery) |
| +───────────────────────────────────────────────────────────────────+ |
| | Apache Kafka Partitioned Topic: "order.created" | |
| +───────────────────────────────────────────────────────────────────+ |
| |
+───────────────────────────────────────────────────────────────────────────+
PostgreSQL Transactional Outbox Schema
-- Core business entity
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
amount_cents BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Transactional outbox table
CREATE TABLE outbox_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
aggregate_type VARCHAR(64) NOT NULL,
aggregate_id UUID NOT NULL,
event_type VARCHAR(64) NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Atomic dual-write within a single local transaction
BEGIN;
INSERT INTO orders (id, user_id, amount_cents, status)
VALUES ('9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d', '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed', 4999, 'PENDING');
INSERT INTO outbox_events (aggregate_type, aggregate_id, event_type, payload)
VALUES (
'Order',
'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d',
'OrderCreated',
'{"userId": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed", "amountCents": 4999, "currency": "USD"}'::jsonb
);
COMMIT;4. Architectural Comparison: 2PC vs Sagas vs TCC
+─────────────────────────────────────────────────────────────────────────────────────────────────────────+
| Pattern | Consistency | Isolation | Latency | Throughput | Recovery Mechanism |
+───────────────────+─────────────+───────────────+────────────+────────────+─────────────────────────────+
| Two-Phase Commit | Strong ACID | Full Locking | High (RTT) | Low (<1k) | Coordinator WAL Replay |
| Saga Orchestration| Eventual | Semantic Only | Low (Async)| High (>50k)| Compensating Transactions |
| Saga Choreography | Eventual | Semantic Only | Minimal | High (>100k| Compensating Event Streams |
| Try-Confirm-Cancel| Strong/Ev. | Reserved Res. | Moderate | Medium | Cancel Phase Timeout |
+─────────────────────────────────────────────────────────────────────────────────────────────────────────+
5. System Design Interview Defense Strategies
When asked how to handle transactions in distributed system design interviews, follow this structured decision heuristic:
- Classify Consistency Requirements: Determine whether financial transactions demand strict serializability or if eventual consistency with compensation is acceptable.
- Address the Lack of Isolation (ACID 'I') in Sagas: Because Sagas do not hold global locks, concurrent operations can read intermediate state (dirty reads). Explain mitigation patterns:
- Semantic Locking: Flagging records with pending statuses (
PENDING_APPROVAL,RESERVED). - Pessimistic Versioning: Using monotonic version counters and optimistic concurrency control (
OCC). - Idempotency Keys: Guaranteeing that retried messages do not cause duplicate state transitions using unique event IDs and Redis deduplication sets.
- Semantic Locking: Flagging records with pending statuses (
Related Technical & Distributed Systems Guides
- System Design Interview Blueprint & Technical STAR Method — 45-minute blueprint for high-concurrency interviews.
- Conquering Distributed Systems Design Interviews — Patterns for distributed caching, sharding, and consensus.
- Kairo Live Architecture Practice — Practice real-time system design defense with sub-400ms AI copilot assistance.