In microservices architecture, Database-per-service is a fundamental principle where each individual microservice owns and manages its own dedicated data store. This design choice grants services complete autonomy, allowing them to choose the best database technology for their specific needs, scale independently, and develop without impacting other services' data. However, this isolation means you can no longer rely on traditional ACID transactions (Atomicity, Consistency, Isolation, Durability) that span multiple services, as there's no single transaction coordinator across disparate databases.
To handle business processes that inherently require updates across multiple services, the Saga pattern emerges as a solution for distributed transactions. A saga is a sequence of local transactions, where each local transaction updates its own service's database and publishes an event. Subsequent services then react to these events, performing their own local transactions. Crucially, if any step in the saga fails, compensation transactions are executed in reverse order to undo the changes made by preceding successful steps, ensuring the system can revert to a consistent state. Sagas can be implemented via choreography (services communicating directly via events) or orchestration (a central service coordinating the saga).
This approach naturally leads to eventual consistency. During the execution of a saga, the overall system state might be temporarily inconsistent. For example, an order might be marked as "created" by the Order Service, but the Inventory Service might not have completed reserving items yet. The system doesn't guarantee immediate consistency across all services; instead, it guarantees that data will eventually converge to a consistent state once all saga steps (or compensation steps) have successfully completed. This trade-off between immediate consistency and increased availability, scalability, and resilience is a cornerstone of robust microservices design.
Key Takeaways
- Database-per-service provides service autonomy but prevents traditional ACID transactions across multiple services.
- The Saga pattern manages distributed transactions as a series of local transactions, using events for inter-service communication.
- Compensation transactions are vital components of a saga, designed to undo previous changes if any part of the distributed process fails.
- Eventual consistency is the inherent outcome: data across services might temporarily diverge during a saga but will eventually become consistent, prioritizing availability and scalability.
Code Example
# Pseudo-code illustrating a Choreography Saga
# Service: Order
def create_order(details):
db.save(Order(details, status='PENDING')) # Local transaction 1
event_bus.publish('OrderCreated', {'orderId': details.id, 'items': details.items})
# Service: Inventory
def handle_OrderCreated(event):
if inventory.reserve(event.items): # Local transaction 2
db.update_inventory_status(event.items, 'RESERVED')
event_bus.publish('InventoryReserved', {'orderId': event.orderId})
else:
event_bus.publish('InventoryFailed', {'orderId': event.orderId}) # Triggers compensation
# Service: Payment
def handle_InventoryReserved(event):
if payment_gateway.process(event.orderId, event.amount): # Local transaction 3
db.record_payment(event.orderId, 'PAID')
event_bus.publish('PaymentProcessed', {'orderId': event.orderId})
else:
event_bus.publish('PaymentFailed', {'orderId': event.orderId})
# Order and Inventory services would listen to 'PaymentFailed' to compensate/revertHow this code works
This code illustrates a Choreography Saga, a pattern essential for coordinating business processes across independent microservices, each managing its own data store (Database-per-service). It shows how services achieve eventual consistency by reacting to events, rather than using a single, long transaction.
The flow begins with create_order in the Order service, which saves the order as PENDING and publishes an OrderCreated event via the event_bus. The Inventory service's handle_OrderCreated then attempts to reserve items; if successful, it publishes InventoryReserved. Next, the Payment service's handle_InventoryReserved processes the payment, publishing PaymentProcessed on success. A subtle but crucial aspect is the compensation mechanism: if inventory.reserve fails, an InventoryFailed event is published to trigger cleanup. Likewise, PaymentFailed requires the Order and Inventory services to listen and revert their respective actions, ensuring all data is consistently rolled back if any step in the overall saga fails.