A saga is a sequence of local transactions. Each local transaction updates the database and publishes a message or event to trigger the next local transaction in the saga. If a local transaction fails, the saga executes a series of compensating transactions that undo the changes made by the preceding local transactions.
Let’s see a saga in action for a simple e-commerce order. Imagine a customer places an order. This involves several steps:
- Create Order: A new order is created in the
OrderService. This is a local transaction. - Reserve Credit: The
PaymentServicereserves the credit for the order amount. This is another local transaction. - Update Inventory: The
InventoryServicedecrements the stock for the ordered items. This is a third local transaction. - Approve Order: If all previous steps succeed, the
OrderServicemarks the order as approved. This is the final local transaction.
Here’s how a saga orchestrates this:
Orchestration-based Saga:
The OrderService acts as the orchestrator. It initiates the saga and sends commands to other services.
- Client: Sends "Place Order" request to
OrderService. - OrderService:
- Starts saga.
- Performs local transaction:
INSERT INTO orders (order_id, customer_id, status) VALUES (123, 456, 'PENDING'); - Publishes
OrderCreatedevent.
- PaymentService:
- Listens for
OrderCreatedevent. - Performs local transaction:
INSERT INTO payments (order_id, amount, status) VALUES (123, 100.00, 'RESERVED'); - Publishes
PaymentReservedevent.
- Listens for
- InventoryService:
- Listens for
PaymentReservedevent. - Performs local transaction:
UPDATE inventory SET quantity = quantity - 1 WHERE item_id = 'ABC'; - Publishes
InventoryUpdatedevent.
- Listens for
- OrderService:
- Listens for
InventoryUpdatedevent. - Performs local transaction:
UPDATE orders SET status = 'APPROVED' WHERE order_id = 123; - Saga completes successfully.
- Listens for
Now, what if InventoryService fails?
- InventoryService:
- Fails to update inventory (e.g., out of stock).
- Publishes
InventoryUpdateFailedevent.
- OrderService:
- Listens for
InventoryUpdateFailedevent. - Initiates compensating transactions.
- Performs compensating local transaction:
UPDATE orders SET status = 'CANCELLED' WHERE order_id = 123; - Publishes
OrderCancelledevent.
- Listens for
- PaymentService:
- Listens for
OrderCancelledevent. - Performs compensating local transaction:
UPDATE payments SET status = 'VOIDED' WHERE order_id = 123; - Saga completes with rollback.
- Listens for
The problem sagas solve is managing consistency across multiple independent microservices without resorting to distributed locking or two-phase commit (2PC), which are notoriously difficult to implement and scale in a distributed environment. Each service maintains its own ACID-compliant database, and the saga coordinates them through a series of reliable messages.
The key levers you control are the local transaction logic within each service and the event publishing/consuming mechanism. You define the sequence of operations, the events that signal completion or failure, and the compensating actions for each step. This approach gives you eventual consistency, meaning that while the system might be temporarily inconsistent during the saga, it will eventually reach a consistent state.
The most surprising thing is that a saga doesn’t guarantee atomicity in the traditional sense. It’s designed to achieve semantic atomicity – the business process as a whole either succeeds or fails gracefully. If a compensating transaction itself fails, you’re in a more complex recovery scenario, often requiring manual intervention or a higher-level retry mechanism, which is why the design of robust compensating actions is paramount.
The next problem you’ll run into is handling complex conditional logic within a saga or managing sagas that span many services, often leading to the exploration of choreography-based sagas.