Distributed Transactions
Understanding distributed transactions, two-phase commit, saga pattern, and idempotency for maintaining consistency across services.
Table of Contents
- The Distributed Transaction Problem
- Two-Phase Commit
- Three-Phase Commit
- Saga Pattern
- Idempotency
- Transaction Patterns
- Practical Considerations
The Distributed Transaction Problem
ACID in Monoliths
ACID Transactions:
- Atomicity: All operations succeed or all fail
- Consistency: Database remains in valid state
- Isolation: Concurrent transactions don't interfere
- Durability: Committed changes persist
Single database: ACID provided by database management system.
The Distributed Challenge
Microservices architecture:
- Each service owns its database
- No shared transaction coordinator
- Network failures possible
- Partial failures common
Problem: How to maintain consistency across multiple databases?
Example: E-Commerce Order
Operation: Place order
Steps:
- Order Service: Create order
- Payment Service: Charge payment
- Inventory Service: Reserve items
- Shipping Service: Create shipment
Failure scenarios:
- Payment fails: Must cancel order
- Inventory unavailable: Must refund payment
- Shipping fails: Must refund and restock
- Network partition: Some operations succeed, others fail
All-or-nothing atomicity needed across services.
Why Traditional Transactions Don't Work
2PC (Two-Phase Commit) problems:
- Blocking (locks held during coordination)
- Coordinator is single point of failure
- Poor performance across services
- Doesn't work across organizational boundaries
XA transactions problems:
- Requires all services to support XA
- Locks databases during commit
- Performance penalty
- Most modern systems don't support XA
Reality: Traditional distributed transactions don't scale in microservices.
Two-Phase Commit
How 2PC Works
Participants: Multiple databases/services.
Coordinator: Orchestrates transaction.
Phase 1 (Prepare/Voting):
- Coordinator asks all participants: "Can you commit?"
- Participants lock resources and vote YES or NO
- Participants write vote to log (for crash recovery)
Phase 2 (Commit/Abort):
- If all voted YES, coordinator sends COMMIT
- If any voted NO, coordinator sends ABORT
- Participants commit or abort
- Participants release locks
- Participants send ACK
Phase 2 completion:
- Coordinator waits for all ACKs
- Only then transaction is complete
2PC Example
Scenario: Transfer $100 from Account A to Account B (different databases)
Phase 1 (Prepare):
Coordinator → DB1: "Prepare to deduct $100 from A"
DB1: Locks Account A, verifies balance sufficient
DB1 → Coordinator: "YES"
Coordinator → DB2: "Prepare to add $100 to B"
DB2: Locks Account B
DB2 → Coordinator: "YES"
Phase 2 (Commit):
Coordinator → DB1: "COMMIT"
DB1: Deducts $100, releases lock
DB1 → Coordinator: "ACK"
Coordinator → DB2: "COMMIT"
DB2: Adds $100, releases lock
DB2 → Coordinator: "ACK"
Coordinator: Transaction complete
2PC Problems
Blocking:
- Locks held during entire protocol
- If coordinator fails, participants blocked indefinitely
- Participants can't release locks until coordinator decides
Scenario:
Phase 1 completes (all vote YES, resources locked)
Coordinator crashes before Phase 2
Participants stuck waiting (locks held, can't proceed)
Single Point of Failure:
- Coordinator crash blocks entire transaction
- Requires timeout and recovery mechanisms
- Recovery is complex
Performance:
- Multiple round trips (high latency)
- Locks held for duration
- Synchronous protocol (can't do other work)
- Doesn't scale to many participants
Availability:
- If any participant unavailable, transaction aborts
- Reduces overall availability
When 2PC Is Appropriate
Rarely in microservices. Use only when:
- Services in same datacenter
- Low latency network
- High reliability needed
- Number of participants is small (2-3)
- Services under single organizational control
Common uses:
- Single database distributed transactions
- Within tightly coupled system boundaries
- Where strong consistency is absolute requirement
Avoid for:
- Cross-service transactions in microservices
- Long-running operations
- External services (third parties)
- High-scale systems
Three-Phase Commit
Improvements Over 2PC
3PC adds a "Pre-Commit" phase between Prepare and Commit.
Phases:
- Prepare: Can you commit? (vote YES/NO)
- Pre-Commit: All voted YES, prepare to commit
- Commit: Actually commit
Key difference: Pre-commit phase allows recovery without blocking.
How 3PC Works
Phase 1 (Prepare):
- Coordinator asks all participants: "Can you commit?"
- Participants vote YES or NO
Phase 2 (Pre-Commit):
- If all YES, coordinator sends PRE-COMMIT
- Participants acknowledge PRE-COMMIT
- All participants now know all voted YES
Phase 3 (Commit):
- Coordinator sends COMMIT
- Participants commit
- Send ACK
3PC Advantage
Non-blocking recovery:
If coordinator fails after Pre-Commit:
- Participants know all voted YES
- Can elect new coordinator
- New coordinator can safely commit
If coordinator fails before Pre-Commit:
- Participants haven't received Pre-Commit
- New coordinator can safely abort
3PC eliminates indefinite blocking of 2PC.
3PC Problems
Network partitions:
- 3PC assumes synchronous network
- Can't distinguish crash from slow network
- Network partition can cause split-brain
Scenario:
Coordinator sends Pre-Commit to Participant A
Network partition prevents message to Participant B
Participant A thinks transaction will commit
Participant B thinks transaction will abort
Coordinator timeout → abort
A commits, B aborts → Inconsistent!
More complex:
- Extra phase means extra latency
- More failure scenarios to handle
- More complex recovery logic
Reality: 3PC rarely used in practice. Benefits don't outweigh complexity.
Saga Pattern
What Is a Saga
Saga: Sequence of local transactions, each updating single service.
Key idea: Instead of single atomic transaction, use sequence of transactions that can be undone.
If failure occurs: Execute compensating transactions to undo completed steps.
Saga Example
Order placement saga:
Forward transactions:
- Order Service: Create order
- Payment Service: Charge payment
- Inventory Service: Reserve items
- Shipping Service: Create shipment
Compensating transactions (if failure):
- Shipping Service: Cancel shipment
- Inventory Service: Release items
- Payment Service: Refund payment
- Order Service: Cancel order
Choreography-Based Saga
No central coordinator. Services coordinate through events.
Process:
- Order Service creates order → emits OrderCreated event
- Payment Service listens, charges payment → emits PaymentSucceeded event
- Inventory Service listens, reserves inventory → emits InventoryReserved event
- Shipping Service listens, creates shipment → emits ShipmentCreated event
Failure handling:
- Inventory Service fails to reserve
- Emits InventoryReservationFailed event
- Payment Service listens, refunds payment → emits PaymentRefunded event
- Order Service listens, cancels order → emits OrderCancelled event
Advantages:
- Simple for basic flows
- No single point of failure
- Loose coupling
Disadvantages:
- Hard to understand overall flow
- Difficult to track saga state
- Cyclic dependencies possible
- Complex error handling logic
Orchestration-Based Saga
Central orchestrator coordinates saga.
Process:
- Orchestrator calls Order Service: Create order
- Orchestrator calls Payment Service: Charge payment
- Orchestrator calls Inventory Service: Reserve inventory
- Orchestrator calls Shipping Service: Create shipment
Failure handling:
- If Inventory Service fails
- Orchestrator calls Payment Service: Refund payment
- Orchestrator calls Order Service: Cancel order
Advantages:
- Clear flow and control
- Easy to track saga state
- Simple error handling
- No cyclic dependencies
Disadvantages:
- Orchestrator is centralized component
- Orchestrator knows about all services
- More coupled architecture
Recommendation: Use orchestration for complex sagas. Better clarity and control.
Saga Guarantees
Sagas provide:
- Atomicity: Either all transactions complete or all are compensated
- Eventually consistent: System becomes consistent after compensation
Sagas don't provide:
- Isolation: Intermediate states are visible
- Atomicity in face of failures: Compensation may fail
Isolation Problems
Dirty reads:
Saga: Transfer $100 from A to B
T1: Deduct $100 from A (completed)
T2: Add $100 to B (pending)
Meanwhile:
Another transaction reads A → sees deducted amount
Saga fails, compensates → adds $100 back to A
Other transaction saw incorrect intermediate state
Lost updates:
Saga 1: Update inventory quantity to 10
Saga 2: Concurrently updates quantity to 15
Both read quantity=20
Saga 1 writes 10
Saga 2 writes 15
Saga 1's update lost
Non-repeatable reads:
Transaction reads inventory=10
Saga modifies inventory=5
Transaction reads again, sees inventory=5
Different value on second read
Isolation Solutions
Semantic locks:
- Application-level locking
- Flag records as "being updated"
- Other transactions check flag
Pessimistic locking:
- Lock resources in first step
- Hold locks until saga completes
- Reduces concurrency
Optimistic locking:
- Use version numbers
- Check version hasn't changed before update
- Retry if version changed
Commutative updates:
- Design operations to be order-independent
- Example: increment/decrement rather than set value
Reread and verify:
- Reread values before final commit
- Verify values haven't changed
- Retry if changed
Compensating Transactions
Requirements:
- Idempotent: Can be retried safely
- Eventually succeed: Must not fail permanently
- Undo semantic effect: Not necessarily undo exact state changes
Example: Refund payment
Forward: Charge $100 from credit card
Compensate: Refund $100 to credit card (not undo the charge)
Semantic: Customer has $100 again (not same transaction state)
Challenges:
- Some operations hard to compensate (sent email, called external API)
- Compensation may fail (network issues, service down)
- Must retry compensation until succeeds
Design principle: Make compensating transactions as simple and reliable as possible.
Idempotency
Why Idempotency Matters
Distributed systems reality:
- Messages may be delivered multiple times
- Retries on failures
- Network duplicates
- Saga compensation may retry
Non-idempotent operations cause problems:
- Charge customer twice
- Create duplicate orders
- Send duplicate notifications
Idempotent operations can be safely retried.
Naturally Idempotent Operations
Setting absolute values:
Set account balance to $100
Retry: Still $100 (idempotent)
DELETE operations:
Delete record with ID=123
Retry: Record still deleted (idempotent)
GET operations:
Read data (no state change)
Retry: Same result (idempotent)
Making Operations Idempotent
Idempotency keys:
Client provides unique key for operation:
POST /payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
{
"amount": 100,
"currency": "USD"
}
Server logic:
- Check if key seen before
- If yes, return cached result
- If no, process request and cache result with key
- Store key-result mapping for 24-48 hours
Idempotency key generation:
- UUID v4 (random)
- Hash of request parameters
- Combination of user ID + operation + timestamp
Storage:
- Database table (key → result)
- Redis/Memcached (key → result with TTL)
Database constraints:
CREATE TABLE payments (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
amount DECIMAL NOT NULL,
idempotency_key VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL
);
CREATE UNIQUE INDEX idx_idempotency
ON payments(user_id, idempotency_key);
Duplicate requests violate unique constraint → rejected.
State machines:
Order states: draft → pending_payment → paid → shipped
Operation: Mark as paid
- If state = pending_payment, transition to paid ✓
- If state = paid, no-op (already paid) ✓ (idempotent)
- If state = shipped, error (invalid transition)
Conditional updates:
UPDATE orders
SET status = 'paid'
WHERE id = 123 AND status = 'pending_payment';
If already paid, UPDATE affects 0 rows → idempotent.
Versioning (optimistic locking):
UPDATE orders
SET status = 'paid', version = version + 1
WHERE id = 123 AND version = 5;
Retry fails if version already incremented → prevents duplicate updates.
Idempotency at Scale
Key distribution:
- Must check idempotency across all instances
- Use shared storage (Redis, database)
- Or use consistent hashing to route same keys to same instance
Key expiration:
- Can't store keys forever
- Typical TTL: 24-48 hours
- After expiration, same key is treated as new operation
Performance:
- Idempotency check adds latency
- Cache hot keys for performance
- Consider async idempotency check for non-critical operations
Transaction Patterns
Try-Confirm/Cancel (TCC)
Three phases:
- Try: Reserve resources, don't commit
- Confirm: Commit reservation (if all try succeed)
- Cancel: Release reservation (if any try fails)
Example:
Try:
- Reserve $100 in account A
- Reserve inventory for product X
Confirm (all try succeeded):
- Deduct $100 from account A
- Deduct 1 from inventory X
Cancel (any try failed):
- Release $100 reservation in account A
- Release inventory reservation for product X
Characteristics:
- More complex than sagas
- Better isolation (resources reserved)
- Requires service support for try/confirm/cancel
Best-Effort Transactions
Attempt operations, accept failures.
Pattern:
- Attempt all operations
- If some fail, log and continue
- Background process retries failures
- Eventually reaches consistent state
Use when:
- Perfect consistency not required
- User doesn't need immediate confirmation
- Operations can be retried indefinitely
Example:
- Analytics updates (eventual consistency acceptable)
- Notification sending (retry until delivered)
Event Sourcing
Store events as source of truth, not current state.
Pattern:
- Store immutable event log
- Replay events to derive current state
- Compensation = new event (not delete/modify)
Advantages:
- Complete audit trail
- Time travel (replay to any point)
- Natural compensation model (add compensating event)
Disadvantages:
- Complex to implement
- Event schema evolution challenges
- Replay can be slow
Practical Considerations
Choosing a Pattern
Use 2PC when:
- Services in same datacenter
- Low latency critical
- Strong consistency required
- Small number of participants
Use sagas when:
- Cross-service transactions
- Microservices architecture
- Long-running operations
- External services involved
Use TCC when:
- Need better isolation than sagas
- All services can implement try/confirm/cancel
- Performance overhead acceptable
Use best-effort when:
- Eventual consistency acceptable
- Operations naturally retryable
- User doesn't need immediate confirmation
Saga Implementation
Orchestration tools:
- Temporal/Cadence (workflow orchestration)
- Camunda (BPMN workflow engine)
- Custom orchestrator service
State persistence:
- Orchestrator must persist saga state
- Enables recovery from orchestrator failures
- Database or durable workflow engine
Timeout handling:
- Set timeouts for each step
- If timeout, execute compensation
- Don't wait indefinitely for responses
Compensation failures:
- Compensation can fail
- Must retry until succeeds
- Exponential backoff
- Alert if compensation consistently fails
Testing Distributed Transactions
Happy path testing:
- All steps succeed
- Verify end-to-end correctness
Failure scenarios:
- Each step fails
- Verify compensation executes correctly
- Verify final state is consistent
Chaos testing:
- Inject random failures
- Network partitions
- Service crashes during transaction
- Verify system recovers correctly
Monitoring
Track saga metrics:
- Success rate
- Failure rate by step
- Compensation rate
- Duration (p50, p95, p99)
Alert on:
- High failure rate
- Compensation failures
- Long-running sagas
- Stuck sagas (not progressing)
Documentation
Document transaction boundaries:
- Which operations are atomic
- Which use sagas
- Compensation logic for each step
- Expected behavior during failures
Runbooks:
- How to identify stuck sagas
- How to manually compensate
- How to resume failed sagas
- Emergency procedures