Skip to main content
Articles/architecture/saga-pattern-orchestration-choreography-and-compensation
Series: Distributed Transactions & Microservices•Part 2 of 4
Scenario Article

Saga Pattern: Coordinating Distributed Transactions with Compensation

Deep Dive: Saga Internals and Failure Semantics

Abstract Algorithms•Mar 13, 2026
💡 Executive Summary

A Saga replaces fragile distributed 2PC with a sequence of local transactions, each backed by an explicit compensating transaction.

🎯 System Scenario & Requirements

System Goal

A Saga replaces fragile distributed 2PC with a sequence of local transactions, each backed by an explicit compensating transaction.

Actors & Components

  • Application or API tier
  • Core service under design
  • Operator or SRE observer

Functional Requirements

  • TLDR: A Saga replaces fragile distributed 2PC with a sequence of local transactions, each backed by an explicit compensating transaction.
  • Use orchestration when workflow control needs a single brain; use choreography when services must stay loosely coupled.
  • Compensating Transactions Are Not Rollbacks A compensating transaction is a new, forward-moving transaction that reverses the business effect of a prior step — it is not a database rollback.

Constraints & Assumptions

  • Compensating Transactions Are Not Rollbacks A compensating transaction is a new, forward-moving transaction that reverses the business effect of a prior step — it is not a database rollback.
  • This distinction has real implications: - ReleaseInventoryCommand tells the inventory service "release reservation R-4892 ." The reservation record is not deleted; it is marked released with a timestamp and reason.
  • No architecture pattern is free.
  • Sagas trade atomic rollback for eventual consistency and explicit operational complexity.

⚡ Execution Stages & Decision Trace

1

Stage 1 — Java Frameworks for Saga Orchestration

Action / Decision:

Eventuate Tram: Saga Coordination Without a Dedicated Framework Eventuate Tram (a lightweight saga/transactional messaging library by Chris Richardson) implements sagas as SagaDefinition objects with explicit step seque…

Component Behavior:

SagaDefinition chains steps with .withCompensation() — Eventuate Tram executes compensations in reverse step order automatically on failure.

Observable Output: No central orchestrator wakeup loop; the framework manages message dispatch and state persistence via JDBC.
Read Full Stage in Deep Dive ↓
📖

Deep Dive

Detailed Architectural Analysis

Scenario context

A Saga replaces fragile distributed 2PC with a sequence of local transactions, each backed by an explicit compensating transaction. Use orchestration when workflow control needs a single brain; use choreography when services must stay loosely coupled.

flowchart TD
    S([Start]) --> INV[ReserveInventory]
    INV -->|InventoryReservedEvent| PAY[AuthorizePayment]
    INV -->|InventoryReservationFailed…

This walkthrough focuses on **Deep Dive: Saga Internals and Failure Semantics**.

Before making a design choice, clarify:

- Compensating Transactions Are Not Rollbacks A compensating transaction is a new, forward-moving transaction that reverses the business effect of a prior step — it is not a database rollback.
- This distinction has real implications: - ReleaseInventoryCommand tells the inventory service "release reservation R-4892 ." The reservation record is not deleted; it is marked released with a timestamp and reason.
- - Compensations must be idempotent : if ReleaseInventoryCommand is delivered twice (broker retry, network hiccup), the second delivery must be a safe no-op, not a double-release.

**Concepts in play:** event-driven-architecture, graph-traversal-algorithms, jvm-internals-and-garbage-collection, message-queues

## Stage 1 — Java Frameworks for Saga Orchestration

- Eventuate Tram: Saga Coordination Without a Dedicated Framework Eventuate Tram (a lightweight saga/transactional messaging library by Chris Richardson) implements sagas as SagaDefinition objects with explicit step seque…
- SagaDefinition chains steps with .withCompensation() — Eventuate Tram executes compensations in reverse step order automatically on failure.
- No central orchestrator wakeup loop; the framework manages message dispatch and state persistence via JDBC.
- For a full deep-dive on Eventuate Tram saga state management and transactional messaging patterns, a dedicated follow-up post is planned.

*Applies [**event-driven-architecture**](/concept/event-driven-architecture) to the decision above.*

```java
public class CheckoutSagaDefinition implements SimpleSaga {

    private SagaDefinition sagaDefinition =
        step()
            .invokeParticipant(this::reserveInventory)
            .withCompensation(this::releaseInventory)
        .step()
            .invokeParticipant(this::authorizePayment)
            .withCompensation(this::refundPayment)
        .step()
            .invokeParticipant(this::createShipment)
        .build();

    @Override
    public SagaDefinition getSagaDefinition() { return sagaDefinition; }

    private CommandWithDestination reserveInventory(CheckoutSagaData data) {
        return send(new ReserveInventoryCommand(data.getOrderId(), data.getItems()))
            .to("inventoryService").build();
    }
    private CommandWithDestination releaseInventory(CheckoutSagaData data) {
        return send(new ReleaseInventoryCommand(data.getOrderId(), data.getReservationId()))
            .to("inventoryService").build();
    }
    // authorizePayment, refundPayment, createShipment follow the same pattern
}
<dependency>
  <groupId>org.axonframework</groupId>
  <artifactId>axon-spring-boot-starter</artifactId>
  <version>4.9.3</version>
</dependency>

Observable outcome: For a full deep-dive on Eventuate Tram saga state management and transactional messaging patterns, a dedicated follow-up post is planned.

Stage 2 — Orchestration in Code: The Axon Checkout Saga

  • The saga class below is the single source of truth for the checkout workflow.
  • Axon persists its fields between events, so the saga survives service restarts mid-flight without losing state.
  • Key annotations: @StartSaga creates a new saga instance per trigger event.
  • @SagaEventHandler(associationProperty = "orderId") routes events to the right instance by orderId .
@Saga
public class CheckoutSaga {

    @Inject
    private transient CommandGateway commandGateway;  // transient = not serialized with saga state

    private String orderId;
    private String reservationId;
    private boolean paymentAuthorized;

    @StartSaga
    @SagaEventHandler(associationProperty = "orderId")
    public void on(OrderPlacedEvent event) {
        this.orderId = event.orderId();
        commandGateway.send(new ReserveInventoryCommand(event.orderId(), event.items()));
    }

    @SagaEventHandler(associationProperty = "orderId")
    public void on(InventoryReservedEvent event) {
        this.reservationId = event.reservationId();
        commandGateway.send(new AuthorizePaymentCommand(event.orderId(), event.totalCents()));
    }

    @SagaEventHandler(associationProperty = "orderId")
    public void on(PaymentAuthorizedEvent event) {
        this.paymentAuthorized = true;
        commandGateway.send(new CreateShipmentCommand(event.orderId()));
        SagaLifecycle.end();
    }

    // Compensation: inventory step failed — nothing reserved yet, just cancel the order
    @SagaEventHandler(associationProperty = "orderId")
    public void on(InventoryReservationFailedEvent event) {
        commandGateway.send(new CancelOrderCommand(event.orderId(), "inventory_unavailable"));
        SagaLifecycle.end();
    }

    // Compensation: payment failed — release reservation first, then cancel order
    @SagaEventHandler(associationProperty = "orderId")
    public void on(PaymentFailedEvent event) {
        commandGateway.send(new ReleaseInventoryCommand(orderId, reservationId));
        commandGateway.send(new CancelOrderCommand(orderId, "payment_failed"));
        SagaLifecycle.end();
    }
}
sequenceDiagram
    participant O as Orchestrator
    participant A as InventoryService
    participant B as PaymentService
    participant C as ShipmentService
    O->>A: ReserveInventory
    A-->>O: InventoryReserved
    O->>B: AuthorizePayment
    B-->>O: PaymentFailed
    O->>A: ReleaseInventory
    A-->>O: InventoryReleased
    Note over O: Saga compensated

Observable outcome: @SagaEventHandler(associationProperty = "orderId") routes events to the right instance by orderId .

Stage 3 — Choreography in Code: Spring Kafka Payment Participant

  • In the choreography style there is no central saga class.
  • Each service subscribes to events and publishes its own outcome.
  • The workflow emerges from the chain of reactions across services.
  • The inventory service similarly listens on the payment-failed topic and releases its reservation.
@Component
public class PaymentSagaParticipant {

    private final KafkaTemplate<String, Object> kafka;

    public PaymentSagaParticipant(KafkaTemplate<String, Object> kafka) {
        this.kafka = kafka;
    }

    @KafkaListener(topics = "order-placed")
    public void onOrderPlaced(OrderPlacedEvent event) {
        try {
            paymentService.authorize(event.orderId(), event.totalCents());
            kafka.send("payment-authorized", new PaymentAuthorizedEvent(event.orderId()));
        } catch (PaymentDeclinedException e) {
            kafka.send("payment-failed",
                new PaymentFailedEvent(event.orderId(), e.getReason()));
        }
    }
}
sequenceDiagram
    participant A as OrderService
    participant B as InventoryService
    participant C as PaymentService
    A->>B: OrderPlaced event
    B->>C: InventoryReserved event
    C->>A: PaymentFailed event
    A->>B: OrderCancelled event
    B-->>A: InventoryReleased event
    Note over A,C: Compensation via events

Observable outcome: The inventory service similarly listens on the payment-failed topic and releases its reservation.

Stage 4 — Real-World Applications

  • Sagas appear wherever a multi-step business process spans service boundaries: - Travel booking: flight → hotel → car rental.
  • If the car is unavailable, compensate by cancelling hotel and flight in reverse order.
    • Bank money transfer: debit source → credit destination → notify parties.
  • Compensation reverses the debit if the credit write fails.

Observable outcome: Compensation reverses the debit if the credit write fails.

Stage 5 — Operator Field Note: Stuck Sagas and Dead Letters

  • Alert on saga active duration — a checkout saga open for more than five minutes is almost certainly stuck.
  • Axon routes commands that exhaust retries to a Dead Letter Queue (DLQ) ; inspect the entry with its orderId , replay if the failure was transient, or escalate for manual data correction.

Observable outcome: Axon routes commands that exhaust retries to a Dead Letter Queue (DLQ) ; inspect the entry with its orderId , replay if the failure was transient, or escalate for manual data correction.

End-to-end outcome

After working through the stages, you should be able to explain:

    • Design compensations before happy paths.
  • If you cannot name the compensating transaction for each step at design time, the saga is not production-ready.

Alternatives considered

Decision Guide: When to Reach for Orchestration vs Choreography

Decision Guide: When to Reach for Orchestration vs Choreography

Failure and degraded modes

Compensating Transactions Are Not Rollbacks

  • Detection: A compensating transaction is a new, forward-moving transaction that reverses the business effect of a prior step — it is not a database rollback.
  • User or operator impact: A compensating transaction is a new, forward-moving transaction that reverses the business effect of a prior step — it is not a database rollback.

Performance Analysis

  • Detection: Sagas trade atomic rollback for latency decomposition .
  • User or operator impact: Sagas trade atomic rollback for latency decomposition .

Trade-offs & Failure Modes: Saga Trade-offs and the Failure Modes That Bite First

  • Detection: No architecture pattern is free.
  • User or operator impact: No architecture pattern is free.

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit