On this page
Distributed Transactions
Distributed transactions coordinate data changes across multiple services or databases. Unlike local @Transactional, they must handle network failures and partial commits.
The Problem
Order Service ──create order──▶ Order DB ✓
│
├──reserve stock──▶ Inventory Service ──▶ Inventory DB ✓
│
└──charge payment──▶ Payment Service ──▶ Payment DB ✗ (failed!)
Order and inventory committed, payment failed → inconsistent state
Two-Phase Commit (2PC)
Coordinator:
Phase 1 (Prepare): Ask all participants "Can you commit?"
Order DB: YES → Order DB: YES → Payment DB: YES
Phase 2 (Commit): Tell all participants to commit
Order DB: COMMIT → Order DB: COMMIT → Payment DB: COMMIT
| Pros | Cons |
|---|---|
| Strong consistency | Blocking (locks resources during prepare) |
| Well understood | Coordinator is single point of failure |
| Poor performance under load | |
| Not suitable for microservices |
Java EE: JTA (Java Transaction API) with XA datasources. Rarely used in modern microservices.
Saga Pattern (Recommended)
A sequence of local transactions, each with a compensating action:
Create Order → Reserve Stock → Process Payment → Ship Order
↓ fail ↓ fail ↓ fail
Cancel Order ← Release Stock ← Refund Payment
Choreography (Event-Driven)
Each service listens for events and reacts:
Order Service: create order → publish OrderCreated
Inventory Service: listen → reserve stock → publish StockReserved
↓ fail → publish StockReservationFailed
Payment Service: listen → charge → publish PaymentProcessed
Order Service: listen StockReservationFailed → cancel order
Orchestration (Central Coordinator)
A saga orchestrator directs each step:
@Service
public class OrderSagaOrchestrator {
public void createOrder(CreateOrderRequest request) {
SagaTransaction saga = SagaBuilder.create()
.step("createOrder", () -> orderService.create(request))
.compensate("cancelOrder", (order) -> orderService.cancel(order.getId()))
.step("reserveStock", (order) -> inventoryService.reserve(order))
.compensate("releaseStock", (order) -> inventoryService.release(order))
.step("processPayment", (order) -> paymentService.charge(order))
.compensate("refundPayment", (order) -> paymentService.refund(order))
.execute();
}
}
Eventual Consistency
Accept temporary inconsistency with guaranteed convergence:
Time 0: Order created (status: PENDING)
Time 1: Stock reserved (order still PENDING)
Time 2: Payment processed (order → CONFIRMED)
Time 3: All services consistent
Clients must handle intermediate states:
@GetMapping("/orders/{id}")
public OrderStatusResponse getOrderStatus(@PathVariable Long id) {
Order order = orderService.findById(id);
return new OrderStatusResponse(order.getId(), order.getStatus(),
order.getStatus() == PENDING ? "Processing, please wait" : "Complete");
}
Outbox Pattern
Ensure reliable event publishing alongside database writes:
@Transactional
public Order createOrder(CreateOrderRequest request) {
Order order = orderRepository.save(new Order(request));
outboxRepository.save(new OutboxEvent("OrderCreated", order.getId(), toJson(order)));
return order;
}
// Separate process polls outbox and publishes to Kafka
@Scheduled(fixedRate = 1000)
public void publishOutboxEvents() {
outboxRepository.findUnpublished().forEach(event -> {
kafkaTemplate.send(event.getTopic(), event.getPayload());
event.markPublished();
outboxRepository.save(event);
});
}
Comparison
| Pattern | Consistency | Complexity | Performance |
|---|---|---|---|
| 2PC | Strong | High | Low |
| Saga (choreography) | Eventual | Medium | High |
| Saga (orchestration) | Eventual | Medium | High |
| Outbox + events | Eventual | Medium | High |
Best Practices
- Avoid 2PC in microservices — use Saga pattern instead
- Design compensating actions for every step in a saga
- Use the Outbox pattern to guarantee event delivery
- Make all saga steps idempotent — retries are inevitable
- Show intermediate states to users (PENDING, PROCESSING)