In a monolithic architecture, maintaining data consistency is simple: the application executes operations within a single database session using local ACID transactions. However, when decomposing a monolith into microservices—where each service owns its isolated database—a business transaction spanning multiple services cannot rely on local ACID guarantees.
To preserve consistency across distributed databases without incurring lock contention, microservice architectures use specialized distributed transaction management patterns: Two-Phase Commit (2PC) and the Saga Pattern.
Two-Phase Commit (2PC) vs. Saga Pattern
| Dimension | Two-Phase Commit (2PC) | Saga Pattern |
|---|---|---|
| Transaction Type | Synchronous & Blocking (ACID across nodes) | Asynchronous & Eventually Consistent (BASE) |
| Data Locking | Holds database locks across all services until commit | No distributed locks; relies on local transactions |
| Availability & Scale | Poor (Single node failure blocks entire cluster) | High (Services execute independently) |
| Failure Recovery | Automatic Rollback managed by Coordinator | Explicit **Compensating Transactions** (Undo actions) |
The Saga Pattern (Orchestration vs. Choreography)
A Saga is a sequence of local transactions where each service updates its own database and publishes an event or message to trigger the next step. If a step fails, the Saga executes a series of compensating transactions to undo changes made by previous steps.
- Choreography: Services exchange events asynchronously without a central controller. Simple to set up, but hard to track as complexity grows.
- Orchestration: A centralized Saga Orchestrator service directs each microservice on what operation to execute next and handles failure rollbacks.
1. Implementing an Orchestrated Saga in Node.js
Consider an e-commerce checkout transaction involving three services: Order Service, Payment Service, and Inventory Service.
// services/orderSagaOrchestrator.js
import axios from 'axios';
export const createOrderSaga = async (orderPayload) => {
const sagaState = {
orderCreated: false,
paymentProcessed: false,
inventoryReserved: false,
orderId: null,
};
try {
// Step 1: Create Pending Order (Local Transaction 1)
const orderRes = await axios.post('http://order-service/api/orders', orderPayload);
sagaState.orderId = orderRes.data.id;
sagaState.orderCreated = true;
// Step 2: Reserve Inventory (Local Transaction 2)
await axios.post('http://inventory-service/api/inventory/reserve', {
orderId: sagaState.orderId,
items: orderPayload.items,
});
sagaState.inventoryReserved = true;
// Step 3: Process Payment (Local Transaction 3 - Simulated Failure)
await axios.post('http://payment-service/api/payments/charge', {
orderId: sagaState.orderId,
amount: orderPayload.totalAmount,
});
sagaState.paymentProcessed = true;
// Step 4: Complete Order Status
await axios.patch(`http://order-service/api/orders/${sagaState.orderId}`, { status: 'CONFIRMED' });
return { status: 'SUCCESS', orderId: sagaState.orderId };
} catch (error) {
console.error('⚠️ Saga Step Failed! Initiating Compensating Transactions...', error.message);
// ==========================================
// COMPENSATING TRANSACTIONS (ROLLBACK PHASE)
// ==========================================
// Compensation 1: Release Inventory if it was reserved
if (sagaState.inventoryReserved) {
await axios.post('http://inventory-service/api/inventory/release', {
orderId: sagaState.orderId,
}).catch((err) => console.error('Failed to release inventory:', err.message));
}
// Compensation 2: Cancel Order if created
if (sagaState.orderCreated) {
await axios.patch(`http://order-service/api/orders/${sagaState.orderId}`, {
status: 'CANCELLED',
}).catch((err) => console.error('Failed to cancel order:', err.message));
}
return { status: 'FAILED', reason: error.message };
}
};
Handling Edge Cases: Idempotency & Pivot Steps
- Compensating Actions Must Be Idempotent: Network retries may cause a compensation call (e.g., refund payment) to be executed multiple times. Ensure endpoints use unique
idempotencyKeychecks. - Identify the Pivot Transaction: The point in a saga where the transaction goes from reversible to irreversible. Once a pivot step succeeds (e.g., sending a confirmation email or shipping goods), subsequent steps cannot be compensated via automated code.
- Use Outbox Pattern for Messaging: To avoid discrepancies between database writes and event publishing, write outbox events directly to a local DB table within the same ACID transaction before dispatching them to RabbitMQ or Kafka.