The Saga pattern for distributed business transactions, compensating transactions, when sagas fit, Choreography (event-driven coordination, loose coupling, pros/cons, an order→payment→shipping event flow) vs Orchestration (a central workflow engine, how it controls services, advantages/disadvantages, travel-booking use case), and how to choose — with Spring/Kafka and workflow-engine examples.
Published September 25, 2026
Choreography and orchestration are two ways to coordinate a saga, and two general styles of service collaboration. Interviewers want:
Lesson 2 of the microservices chapter covers saga failure handling in depth. This lesson focuses on the patterns themselves.
Short answer: It manages a business transaction that spans several services, each with its own database, without a global ACID transaction. The transaction is split into local transactions, coordinated by events (choreography) or commands from an orchestrator. Failures trigger compensating actions. Example: placing an order touches Order, Inventory, Payment and Shipping. If Payment fails, the saga releases the stock, cancels the order, and notifies the customer.
Learn it in depth → Saga Pattern
Short answer: Services collaborate by publishing and reacting to events, with no central coordinator. Each service decides for itself what to do when it sees an event. Example: a social application publishes PostCreated, and Feed (fan-out to followers), Notification (alert the mentioned users) and Analytics (count it) each react independently. The Post service doesn't know they exist.
Learn it in depth → Event-Driven Architecture Patterns
Short answer: A central orchestrator owns the workflow. It sends commands to the services in a defined sequence, waits for their replies, applies branching, timeouts and retries, and runs compensations on failure. Example: an order-fulfilment orchestrator calls Payment → Inventory → Shipping in order, and knows exactly which step each order is at.
Short answer: "Replace one distributed ACID transaction with a sequence of local transactions, each publishing the next step, and each paired with a compensating transaction that semantically undoes it." Sagas are ACD, not ACID: they're atomic (through compensation), consistent and durable, but not isolated. Intermediate states are visible, and must be handled with semantic locks, or PENDING states.
Short answer:
Key points to cover:
Short answer:
| Choreography | Orchestration | |
|---|---|---|
| Control | Decentralised: services react to events | Centralised: the orchestrator sends commands |
| Workflow definition | Implicit, spread across services | Explicit, in one place (code, BPMN or a state machine) |
| Coupling | Services know event types, not each other | The orchestrator knows every participant's API |
| Visibility and debugging | Hard: you reconstruct the flow from events and traces | Easy: the orchestrator holds each saga's state |
| Adding steps | Add a new subscriber (easy), but the global flow is harder to reason about | Change the orchestrator (a clear, single change) |
| Risks | Cyclic dependencies, "event spaghetti" | The orchestrator becomes a bottleneck or god service |
| Best for | Short, simple flows; fan-out side effects | Long, complex, business-critical flows with timeouts |
Short answer: A compensating transaction is a business operation that semantically reverses a previously committed step, when a later step fails:
ReserveStock → ReleaseStock;AuthorisePayment → VoidAuthorisation (or Refund after capture);BookFlight → CancelFlight.They run in reverse order of completion. Unlike a database rollback, a compensation:
Common trap: the source says compensations return the system "to its initial state". Say "to a consistent state". Some effects can only be offset, not erased.
Short answer:
It's not needed when the invariant lives inside one service. Keep it in one local transaction. And sagas are overkill for pure fire-and-forget side effects (notifications); plain events do.
Short answer:
PaymentFailed) trigger compensations in the services that care.@KafkaListener(topics = "orders.events", groupId = "payment-service")
void on(OrderCreated e) {
PaymentResult r = payments.authorize(e.orderId(), e.total(), e.idempotencyKey());
outbox.publish(r.approved() ? new PaymentCompleted(e.orderId(), r.authId())
: new PaymentFailed(e.orderId(), r.reason()));
}
@KafkaListener(topics = "payments.events", groupId = "inventory-service")
void on(PaymentFailed e) { inventory.releaseFor(e.orderId()); } // compensation reacts to the failure event
Short answer:
Key points to cover:
Short answer:
Short answer: E-commerce order flow:
PENDING, and publishes OrderCreated.OrderCreated): authorises the payment, and publishes PaymentCompleted, or PaymentFailed.PaymentCompleted): reserves the stock, and publishes StockReserved, or StockUnavailable.StockReserved): creates the shipment, and publishes ShipmentScheduled.PAID → CONFIRMED → SHIPPED, or CANCELLED.StockUnavailable, Payment reacts by refunding or voiding, and Order marks the order CANCELLED. Notification reacts to the terminal events, to email the customer.Each service reacts only to the events it cares about. None calls another directly.
Short answer: In orchestration, a dedicated orchestrator (a saga manager, or a workflow engine such as Temporal, Camunda, Netflix Conductor, AWS Step Functions, or a Spring State Machine-based service) explicitly drives the workflow. It sends commands ("ReserveStock for order 42"), and handles replies. In choreography, no one is in charge: services react to events.
Participants in orchestration are still loosely coupled to each other. They're only coupled to the orchestrator's commands.
Short answer:
// Temporal-style workflow: durable, replayable orchestration written as plain code
public class TripBookingWorkflowImpl implements TripBookingWorkflow {
private final BookingActivities acts = Workflow.newActivityStub(BookingActivities.class,
ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(30))
.setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(3).build()).build());
public TripConfirmation book(TripRequest req) {
Saga saga = new Saga(new Saga.Options.Builder().build());
try {
String flight = acts.bookFlight(req); saga.addCompensation(acts::cancelFlight, flight);
String hotel = acts.bookHotel(req); saga.addCompensation(acts::cancelHotel, hotel);
String car = acts.bookCar(req); saga.addCompensation(acts::cancelCar, car);
return new TripConfirmation(flight, hotel, car);
} catch (ActivityFailure e) {
saga.compensate(); // runs the cancellations in reverse order
throw e;
}
}
}
Short answer:
Keep orchestrators focused on sequencing, and domain rules inside the services.
Short answer: A travel package booking:
Other good fits:
Q: Can you mix choreography and orchestration? A: Yes, and it's common. Orchestrate the core transactional workflow (order fulfilment) explicitly, and let peripheral reactions (notifications, analytics, loyalty) subscribe to the orchestrator's or services' events, choreographically.
Q: How do you avoid lost messages between saga steps? A: Use the transactional outbox for publishing (the state change and the event are committed together), idempotent handlers keyed by saga or step ID, at-least-once consumption with offsets committed after processing, and timeouts in the orchestrator to detect stuck steps.
Q: How do you handle the lack of isolation in sagas?
A: Use semantic locks (a PENDING status that other operations respect), commutative updates, re-reading values before critical decisions ("reread value"), ordering the steps so risky ones come last, and versioning or optimistic locks on the aggregates involved.
Q: Why use a workflow engine instead of hand-coding the orchestrator? A: Engines provide durable state, timers, retries, versioning of running workflows, visibility UIs and scalability out of the box. Hand-rolled orchestrators tend to re-implement these, badly. For one or two simple sagas, a Spring State Machine or a table-driven orchestrator can be enough.