Saga Pattern in Microservices
In a monolithic application, keeping data consistent is fairly simple: one database and ACID transactions (Atomicity, Consistency, Isolation, Durability) make sure an operation either completes fully or not at all. When you split that monolith into microservices, each with its own private database, this simplicity disappears. How do you keep data correct across services during a business process? The answer is often the Saga pattern.
This guide explains the problem, how sagas work, both coordination styles with Java and Spring Kafka code, compensating transactions, reliability techniques and best practices.
1. The Problem: Transactions Across Microservices
Imagine an e-commerce application with separate Order, Payment and Inventory services, each with its own database. When a customer places an order, several steps must happen:
- The order service creates a new order with the status
PENDING. - The payment service charges the customer.
- The inventory service reserves the items in stock.
What if one step fails?
- If inventory reservation fails after the payment succeeded, you have charged a customer for an order you cannot fulfill.
- If the payment fails after stock was reserved, you hold stock that nobody can buy.
This is the distributed transaction problem. Each service can only commit or roll back its own database. There is no single transaction that covers all three.
2. Why Not Use a Distributed Transaction (2PC)?
The classic solution for transactions across databases is the two-phase commit (2PC) protocol: a coordinator asks every database to prepare, then tells all of them to commit. It works, but it fits microservices poorly:
| Issue with 2PC | Why it matters in microservices |
|---|---|
| Locks are held until everyone commits | Slow participants block the others and reduce throughput |
| The coordinator is a single point of failure | If it crashes mid-way, participants can stay locked |
| All participants must be available at the same time | Reduces availability; one service down blocks everything |
| Many modern databases and message brokers do not support it | NoSQL stores and Kafka cannot take part in a classic 2PC |
| Tight coupling | Services must share a transaction protocol, against the idea of independent services |
Sagas take a different approach: they give up immediate consistency in exchange for availability and independence, and reach eventual consistency instead.
3. What Is the Saga Pattern?
The Saga pattern manages data consistency across several microservices by splitting one big business transaction into a sequence of local transactions. Each local transaction updates the database of a single service and then triggers the next step.
The key idea: for every step that changes data, there is a matching compensating transaction that undoes its effect. If a step fails, the saga runs the compensating transactions for the completed steps, in reverse order, and the system returns to a consistent state.
Happy path:
T1 Create order -> T2 Charge payment -> T3 Reserve stock -> Done
Failure in T3:
T1 Create order -> T2 Charge payment -> T3 Reserve stock (fails)
|
C1 Cancel order <- C2 Refund payment <-----------+
The term comes from a 1987 database research paper that described long-running transactions as sagas: a story told in several chapters, where each chapter can be undone if the story cannot finish.
4. Compensating Transactions: The Heart of the Saga
Compensation is not a database rollback. The earlier steps have already been committed, and other users may have seen their results. A compensating transaction is a business-level undo:
| Step (transaction) | Compensating transaction |
|---|---|
| Create order | Cancel order (set status to CANCELLED) |
| Charge payment | Refund payment |
| Reserve stock | Release stock (return it to the available count) |
| Book a seat | Release the seat |
| Send a confirmation email | Send a cancellation email (you cannot “unsend” the first) |
Rules for good compensating transactions:
- They must be idempotent: running a refund twice must not refund twice.
- They must eventually succeed: retry them until they work; a failed compensation leaves data inconsistent.
- They are semantic, not exact: a refund does not erase the charge, it adds an opposite entry.
- Put hard-to-undo steps last: steps that cannot be compensated, such as shipping a parcel, should happen only after every step that might fail. For example, many shops reserve stock before charging the card, because releasing stock is easier and cheaper than refunding.
5. Choreography-Based Saga
In a choreography-based saga there is no central coordinator. Each service listens for events, does its local work and publishes a new event. The services “dance” together by reacting to each other.
5.1 Example: placing an order
- The order service creates a
PENDINGorder and publishesOrderCreated. - The payment service hears
OrderCreated, charges the customer and publishesPaymentProcessed, orPaymentFailedif the charge fails. - The inventory service hears
PaymentProcessed, reserves the stock and publishesInventoryReserved, orInventoryFailedif items are out of stock. - The order service hears
InventoryReservedand changes the order toCONFIRMED.
5.2 What happens on failure?
- If the inventory service publishes
InventoryFailed, the payment service hears it and refunds the customer, and the order service hears it and cancels the order. - If the payment service publishes
PaymentFailed, the order service cancels the order. Nothing else needs to be undone.
Order Svc --OrderCreated--> Payment Svc --PaymentProcessed--> Inventory Svc
^ | |
| PaymentFailed InventoryReserved
| | or InventoryFailed
+------------- events -------+----------------------------------+
(confirm or cancel order) (Payment Svc refunds on InventoryFailed)
| Pros | Cons |
|---|---|
| Simple to start; no extra coordinator service | The overall flow is spread across services and hard to see |
| Loosely coupled: services only know events, not each other | Harder to debug and test as the number of steps grows |
| No single point of failure | Risk of cyclic dependencies between services’ events |
6. Choreography in Java with Spring Kafka
Here is the choreography example with Spring for Apache Kafka. Each event is a small record, and each event type has its own topic to keep things simple:
public record OrderCreated(String orderId, String customerId, String productId,
int quantity, BigDecimal amount) {
}
public record PaymentProcessed(String orderId, String productId, int quantity, BigDecimal amount) {
}
public record PaymentFailed(String orderId, String reason) {
}
public record InventoryReserved(String orderId) {
}
public record InventoryFailed(String orderId, BigDecimal amountToRefund, String reason) {
}
6.1 Order service: starts the saga and reacts to the outcome
@Service
public class OrderSagaHandler {
private final OrderRepository orders;
private final KafkaTemplate<String, Object> kafka;
public OrderSagaHandler(OrderRepository orders, KafkaTemplate<String, Object> kafka) {
this.orders = orders;
this.kafka = kafka;
}
// Step 1: local transaction + event
@Transactional
public Order placeOrder(PlaceOrderRequest request) {
Order order = orders.save(Order.pending(request));
kafka.send("order-created", order.getId(), new OrderCreated(
order.getId(), request.customerId(), request.productId(),
request.quantity(), order.getTotal()));
return order;
}
// Final step on success
@KafkaListener(topics = "inventory-reserved", groupId = "order-service")
@Transactional
public void on(InventoryReserved event) {
orders.findById(event.orderId()).ifPresent(Order::confirm);
}
// Compensation: cancel the order when payment or stock fails
@KafkaListener(topics = "payment-failed", groupId = "order-service")
@Transactional
public void on(PaymentFailed event) {
orders.findById(event.orderId()).ifPresent(order -> order.cancel(event.reason()));
}
@KafkaListener(topics = "inventory-failed", groupId = "order-service")
@Transactional
public void on(InventoryFailed event) {
orders.findById(event.orderId()).ifPresent(order -> order.cancel(event.reason()));
}
}
6.2 Payment service: charges, and refunds when needed
@Service
public class PaymentSagaHandler {
private final PaymentService payments;
private final KafkaTemplate<String, Object> kafka;
public PaymentSagaHandler(PaymentService payments, KafkaTemplate<String, Object> kafka) {
this.payments = payments;
this.kafka = kafka;
}
@KafkaListener(topics = "order-created", groupId = "payment-service")
public void on(OrderCreated event) {
try {
// The order ID is the idempotency key: a duplicate event never charges twice
payments.charge(event.orderId(), event.customerId(), event.amount());
kafka.send("payment-processed", event.orderId(), new PaymentProcessed(
event.orderId(), event.productId(), event.quantity(), event.amount()));
} catch (PaymentDeclinedException ex) {
kafka.send("payment-failed", event.orderId(),
new PaymentFailed(event.orderId(), ex.getMessage()));
}
}
// Compensating transaction
@KafkaListener(topics = "inventory-failed", groupId = "payment-service")
public void on(InventoryFailed event) {
payments.refund(event.orderId(), event.amountToRefund()); // idempotent as well
}
}
6.3 Inventory service: reserves stock
@Service
public class InventorySagaHandler {
private final StockService stock;
private final KafkaTemplate<String, Object> kafka;
public InventorySagaHandler(StockService stock, KafkaTemplate<String, Object> kafka) {
this.stock = stock;
this.kafka = kafka;
}
@KafkaListener(topics = "payment-processed", groupId = "inventory-service")
public void on(PaymentProcessed event) {
boolean reserved = stock.reserve(event.orderId(), event.productId(), event.quantity());
if (reserved) {
kafka.send("inventory-reserved", event.orderId(), new InventoryReserved(event.orderId()));
} else {
kafka.send("inventory-failed", event.orderId(),
new InventoryFailed(event.orderId(), event.amount(), "Out of stock"));
}
}
}
Notice that the order ID is used as the Kafka message key everywhere, so all events for one order stay in order, and as the idempotency key, so duplicate events cause no harm. In production, publish events through the transactional outbox (section 11) so that saving data and sending the event cannot get out of step.
7. Orchestration-Based Saga
In an orchestration-based saga, a central orchestrator manages the whole business process. It tells each participant what to do and when, by sending commands and waiting for replies.
7.1 Example: placing an order
- An Order Saga Orchestrator starts when a customer places an order; the order is saved as
PENDING. - The orchestrator sends a Process Payment command to the payment service.
- If the payment succeeds, it sends a Reserve Inventory command to the inventory service.
- If all steps succeed, it tells the order service to Approve Order and marks the saga as complete.
7.2 What happens on failure?
If Reserve Inventory fails, the orchestrator runs the compensating transactions in reverse order:
- Send a Refund Payment command to the payment service.
- Send a Cancel Order command to the order service.
[ Order Saga Orchestrator ]
| | |
ProcessPayment ReserveInventory ApproveOrder / CancelOrder
| | |
v v v
Payment Svc Inventory Svc Order Svc
| |
+-- replies (success / failure) --> Orchestrator
| Pros | Cons |
|---|---|
| The whole flow is in one place: easy to read, change and monitor | The orchestrator is an extra component to build and run |
| No cyclic dependencies between services | Risk of putting too much business logic in the orchestrator |
| Easier to handle complex flows, timeouts and compensation | Participants depend on the orchestrator’s commands |
8. An Orchestrator in Java
An orchestrator stores the state of each saga, so it can continue after a restart, and moves to the next step when a reply arrives:
@Entity
@Table(name = "order_sagas")
public class OrderSaga {
public enum Step { PAYMENT_PENDING, INVENTORY_PENDING, COMPLETED, COMPENSATING, CANCELLED }
@Id
private String orderId;
@Enumerated(EnumType.STRING)
private Step step;
private BigDecimal amount;
private String productId;
private int quantity;
protected OrderSaga() {
}
public static OrderSaga start(String orderId, BigDecimal amount, String productId, int quantity) {
OrderSaga saga = new OrderSaga();
saga.orderId = orderId;
saga.amount = amount;
saga.productId = productId;
saga.quantity = quantity;
saga.step = Step.PAYMENT_PENDING;
return saga;
}
public void moveTo(Step next) {
this.step = next;
}
public String getOrderId() { return orderId; }
public Step getStep() { return step; }
public BigDecimal getAmount() { return amount; }
public String getProductId() { return productId; }
public int getQuantity() { return quantity; }
}
@Component
public class OrderSagaOrchestrator {
private final OrderSagaRepository sagas;
private final KafkaTemplate<String, Object> kafka;
public OrderSagaOrchestrator(OrderSagaRepository sagas, KafkaTemplate<String, Object> kafka) {
this.sagas = sagas;
this.kafka = kafka;
}
@Transactional
public void start(Order order) {
OrderSaga saga = sagas.save(OrderSaga.start(
order.getId(), order.getTotal(), order.getProductId(), order.getQuantity()));
kafka.send("payment-commands", saga.getOrderId(),
new ProcessPayment(saga.getOrderId(), order.getCustomerId(), saga.getAmount()));
}
@KafkaListener(topics = "payment-replies", groupId = "order-saga")
@Transactional
public void onPaymentReply(PaymentReply reply) {
OrderSaga saga = sagas.findById(reply.orderId()).orElseThrow();
if (saga.getStep() != OrderSaga.Step.PAYMENT_PENDING) {
return; // duplicate or late reply: already handled
}
if (reply.success()) {
saga.moveTo(OrderSaga.Step.INVENTORY_PENDING);
kafka.send("inventory-commands", saga.getOrderId(),
new ReserveInventory(saga.getOrderId(), saga.getProductId(), saga.getQuantity()));
} else {
saga.moveTo(OrderSaga.Step.CANCELLED);
kafka.send("order-commands", saga.getOrderId(),
new CancelOrder(saga.getOrderId(), "Payment declined"));
}
sagas.save(saga);
}
@KafkaListener(topics = "inventory-replies", groupId = "order-saga")
@Transactional
public void onInventoryReply(InventoryReply reply) {
OrderSaga saga = sagas.findById(reply.orderId()).orElseThrow();
if (saga.getStep() != OrderSaga.Step.INVENTORY_PENDING) {
return;
}
if (reply.success()) {
saga.moveTo(OrderSaga.Step.COMPLETED);
kafka.send("order-commands", saga.getOrderId(), new ApproveOrder(saga.getOrderId()));
} else {
// Compensate the completed steps in reverse order
saga.moveTo(OrderSaga.Step.COMPENSATING);
kafka.send("payment-commands", saga.getOrderId(),
new RefundPayment(saga.getOrderId(), saga.getAmount()));
kafka.send("order-commands", saga.getOrderId(),
new CancelOrder(saga.getOrderId(), "Out of stock"));
}
sagas.save(saga);
}
}
The step check at the start of each handler makes the orchestrator idempotent: a duplicate reply is simply ignored. Real orchestrators also handle timeouts, for example compensating if no payment reply arrives within a few minutes.
9. Choreography vs Orchestration
| Choreography | Orchestration | |
|---|---|---|
| Coordination | Decentralized: services react to events | Centralized: an orchestrator sends commands |
| Coupling | Very loose; services know only events | Participants depend on the orchestrator |
| Visibility of the flow | Spread across services | In one place |
| Best for | Simple flows with 2–4 steps | Complex flows, many steps, branches and timeouts |
| Adding a step | Change several services’ event handling | Change the orchestrator |
| Debugging | Harder: follow events across services | Easier: check the saga’s state |
| Single point of failure | None | The orchestrator (run several instances) |
Many teams start with choreography for simple flows and switch to orchestration when a flow grows past a few steps or needs complex error handling.
10. Saga States and Pending Data
Because a saga takes time to finish, data passes through intermediate states. Model these states explicitly, so everyone, including users, knows the data is not final yet:
+--> CONFIRMED (all steps succeeded)
|
PENDING ---+
|
+--> CANCELLED (a step failed; compensations ran)
- Show users honest messages, such as “Your order is being processed”, while the status is
PENDING. - Do not let other processes treat
PENDINGdata as final, for example by shipping a pending order. - Record why an order was cancelled, so support teams and customers understand what happened.
11. Making Sagas Reliable
Sagas depend on messages being delivered and processed correctly. These techniques make them reliable:
| Technique | Problem it solves |
|---|---|
| Idempotent handlers | Messages can be delivered more than once; handling a duplicate must not charge or refund twice |
| Transactional outbox | Saving data and publishing an event are two separate actions; the outbox writes the event into the same database transaction, and a relay publishes it afterwards |
| Retries with backoff | Temporary failures should not end a saga; see the Retry pattern |
| Timeouts | A missing reply should not leave a saga waiting forever; compensate after a deadline |
| Dead-letter topics | A message that keeps failing is set aside and investigated, instead of blocking others |
| Correlation IDs and tracing | Follow one saga across all services and logs |
@Transactional
public Order placeOrder(PlaceOrderRequest request) {
Order order = orders.save(Order.pending(request));
// Same transaction: either both rows are saved, or neither is
outbox.save(new OutboxMessage("order-created", order.getId(), toJson(order)));
return order;
}
// A separate relay (or a tool such as Debezium) reads new outbox rows and publishes them to Kafka
12. Isolation Problems and How to Handle Them
ACID transactions provide isolation: other users never see half-finished work. Sagas do not, because each step commits on its own. This can cause anomalies, such as another request reading an order that is about to be cancelled. Common countermeasures:
| Countermeasure | How it helps | Example |
|---|---|---|
| Semantic lock | Mark data as “in progress” so others treat it carefully | PENDING orders cannot be shipped or edited |
| Commutative updates | Design updates so their order does not matter | Add and subtract stock amounts instead of overwriting the total |
| Reread value | Check that data has not changed before updating it | Use version numbers (optimistic locking) |
| Pessimistic ordering | Run risky steps early and irreversible steps last | Reserve stock before charging; ship only after everything succeeded |
13. Frameworks and Tools for Sagas
| Tool | Type | Notes |
|---|---|---|
| Spring for Apache Kafka / RabbitMQ | Messaging | Build choreography or simple orchestration yourself, as shown above |
| Axon Framework | Java framework | Sagas, event sourcing and CQRS for Java |
| Eventuate Tram Sagas | Java framework | Orchestration sagas with a built-in transactional outbox |
| Temporal | Workflow engine | Write long-running workflows as normal code; retries and state are handled for you |
| Camunda / Zeebe | Workflow engine | Model flows visually with BPMN diagrams |
| AWS Step Functions | Managed service | Orchestrate steps, including compensation, in AWS |
For simple sagas, plain messaging is enough. For complex, long-running processes with many steps and timeouts, a workflow engine saves a lot of custom code.
14. When to Use the Saga Pattern
Use a saga when:
- A business transaction spans several microservices, each with its own database.
- You need data consistency without two-phase commit or a shared database.
- Each step can be reversed with compensating logic, or placed after all steps that might fail.
- The business accepts eventual consistency: a short delay before everything agrees.
Think twice when:
- All the data lives in one service; a normal local transaction is simpler.
- The business needs strict, immediate consistency everywhere; perhaps the data belongs in one service.
- A saga would need many services for one simple action; the service boundaries may be wrong. See Domain-Driven Design.
15. Best Practices
- Design compensations first: for every step, decide how it will be undone before writing code.
- Make every handler and compensation idempotent, using the saga or order ID as the key.
- Use the transactional outbox to publish events and commands reliably.
- Order steps carefully: risky steps early, irreversible steps last.
- Model intermediate states, such as
PENDING, and show them honestly to users. - Add timeouts so a saga never waits forever.
- Keep sagas short, with as few steps and services as possible.
- Choose the style deliberately: choreography for simple flows, orchestration for complex ones.
- Trace and monitor sagas: how many are running, completed, compensated or stuck.
- Test failure paths, not just the happy path: every compensation must be tested.
16. Common Mistakes to Avoid
| Mistake | What goes wrong | Better approach |
|---|---|---|
| No compensation for a step | Failures leave data permanently inconsistent | Define a compensating action for every step |
| Non-idempotent handlers | Duplicate messages cause double charges or refunds | Use the saga ID as an idempotency key |
| Saving data and sending events separately | A crash in between loses the event and stalls the saga | Use the transactional outbox |
| Irreversible steps too early | Cannot undo, for example, a shipped parcel | Run irreversible steps last |
| No timeouts | Sagas stay pending forever when a reply is lost | Compensate after a deadline |
| Treating pending data as final | Pending orders get shipped or reported as sales | Use semantic locks and clear states |
| Huge choreography webs | Nobody understands the full flow | Switch to orchestration for complex flows |
| Testing only the happy path | Compensation bugs appear in production | Test every failure scenario |
17. Summary
The Saga pattern is an essential tool for keeping data consistent in microservices without distributed transactions. It splits a business transaction into a sequence of local transactions, each with a compensating transaction that undoes it if a later step fails. Choose choreography for simple, loosely coupled flows driven by events, and orchestration for complex flows managed by a central coordinator. Make every step and compensation idempotent, publish messages reliably with the outbox pattern, model pending states, add timeouts, and test every failure path. Sagas trade the immediate consistency of ACID transactions for the scalability and independence of microservices, embracing eventual consistency as a core principle of distributed systems. Continue with Event-Driven Architecture to learn more about the events that power sagas.
18. Frequently Asked Questions (FAQ)
Q1. What is the Saga pattern in simple words?
A saga is a way to complete a business process that spans several microservices without one big database transaction. The process is split into small local steps, one per service.
If a step fails, the saga runs undo actions, called compensating transactions, for the steps that already finished, such as refunding a payment and cancelling the order.
Q2. Why can't microservices simply use one database transaction?
Each microservice owns its own database, often of a different type, and a normal transaction can only cover one database.
Distributed transactions such as two-phase commit exist, but they hold locks across services, depend on a coordinator, reduce availability and are not supported by many modern databases and message brokers. Sagas avoid these problems.
Q3. What is a compensating transaction?
A compensating transaction is a business-level undo for a step that has already been committed. It does not erase history; it adds an opposite action.
- Charge payment → refund payment.
- Reserve stock → release stock.
- Create order → cancel order.
Compensations must be idempotent and must eventually succeed, so they are retried until they work.
Q4. What is the difference between choreography and orchestration?
- Choreography: no central controller; each service reacts to events from the others and publishes its own.
- Orchestration: a central orchestrator sends commands to each service and decides the next step based on their replies.
Choreography is simpler for short flows; orchestration is easier to understand and change for long, complex flows.
Q5. Which saga style should I choose?
Use choreography when the flow has only a few steps, few branches and the teams want maximum independence.
Use orchestration when the flow has many steps, conditions, timeouts or complex compensation, or when you need a clear picture of each saga’s state. Many systems use both: choreography for simple reactions and orchestration for core business processes.
Q6. Does a saga guarantee ACID properties?
No. A saga provides atomicity in the end (all steps complete, or completed steps are compensated), and each local step is ACID within its own service.
But it does not provide isolation: other requests can see intermediate results, such as a pending order, while the saga is running. You handle this with techniques such as pending states, semantic locks and careful ordering of steps.
Q7. What is eventual consistency in a saga?
While a saga is running, services may briefly disagree: the payment is taken, but the order still shows PENDING. Once the saga finishes, either successfully or with compensation, all services agree again.
This short period of disagreement is called eventual consistency. Most business processes, such as online orders, handle it well if the user interface explains the pending state clearly.
Q8. What happens if a compensating transaction fails?
Compensations must not give up. Typical handling:
- Retry the compensation with backoff, because most failures are temporary.
- If it still fails, move it to a dead-letter queue and alert the team.
- Resolve it manually if needed, for example a support agent issues the refund.
Design compensations to be simple and idempotent, so retries are safe and likely to succeed.
Q9. How do I make saga steps idempotent?
Use a unique ID for the saga, such as the order ID, as an idempotency key in every step:
- Before charging, check whether a payment for this order ID already exists.
- Store processed message IDs and skip duplicates.
- In an orchestrator, ignore replies that do not match the saga’s current step.
- Add unique database constraints on the key as a safety net.
Q10. What is the transactional outbox, and why do sagas need it?
A saga step usually saves data and publishes an event. If the service crashes between the two, the event is lost and the saga stalls, or the event is sent for data that was never saved.
With the outbox pattern, the event is written to an outbox table in the same database transaction as the business data. A separate relay, or a change-data-capture tool such as Debezium, then publishes it reliably.
Q11. How do I handle timeouts in a saga?
Record when each step started. If a reply does not arrive within a set time, the saga treats the step as failed and starts compensating.
Be careful: a timeout does not prove the step failed, it may just be slow. Compensations must therefore handle the case where the original action did succeed later, which is another reason for idempotent, well-defined undo actions. Workflow engines such as Temporal provide timers for this.
Q12. Can a saga have steps that cannot be undone?
Yes. Some actions are irreversible, such as sending a parcel or an email. Handle them by:
- Placing them last, after every step that might fail.
- Using a semantic compensation where possible, such as a cancellation email or a return label.
- Splitting the process so the irreversible part only starts when everything else is confirmed.
Q13. How many steps should a saga have?
As few as possible. Every step adds a service to coordinate, a compensation to write and test, and another way for things to fail.
If a simple business action needs many services, the service boundaries may be wrong; consider merging closely related data into one service. Sagas of three to five steps are common and manageable.
Q14. How do I monitor sagas?
- Count sagas that are started, completed, compensated and failed.
- Alert on sagas stuck in a pending state for too long.
- Use the saga ID as a correlation ID in all logs and traces.
- With orchestration, query the saga state table to see exactly where each saga is.
Q15. How do I test a saga?
- Unit test each handler and the orchestrator’s decisions, including duplicate and late messages.
- Integration test with a real broker, such as Kafka in Testcontainers, and check the final state after success and after each failure.
- Test every compensation path, not only the happy path.
- Use tools such as Awaitility to wait for the asynchronous result instead of fixed sleeps.
Q16. Is the saga orchestrator a single point of failure?
It can be if you run only one instance and keep its state in memory. Avoid this by:
- Storing saga state in a database, so any instance can continue a saga.
- Running several instances; a message broker spreads replies between them.
- Making handlers idempotent, so a reply processed twice after a crash causes no harm.
Q17. Should the orchestrator contain business logic?
Keep it limited to coordination: which step comes next, what to do on failure, and when to time out.
Business rules, such as how to calculate a refund or whether stock is available, belong in the participating services. An orchestrator that grows full of business logic becomes a hard-to-change central monolith.
Q18. How is the Saga pattern related to event-driven architecture?
Choreography-based sagas are an event-driven design: each step reacts to an event and publishes the next. Orchestration-based sagas usually also use messaging for commands and replies.
So sagas depend on the same foundations as event-driven systems: reliable messaging, idempotent consumers, the outbox pattern and good tracing.
Q19. Can I use the Saga pattern without Kafka?
Yes. Any reliable messaging works, such as RabbitMQ, ActiveMQ, Amazon SQS and SNS, or Azure Service Bus. Orchestration sagas can even call services with REST, as long as the orchestrator stores its state and retries failed calls.
Workflow engines such as Temporal, Camunda or AWS Step Functions can also run sagas without you managing a message broker directly.
Q20. What does a saga look like to the end user?
Usually the user gets a quick answer, such as “Order received, we are processing it”, while the saga runs in the background. They then see the final result shortly after:
- An updated status on the order page: Confirmed or Cancelled.
- An email or push notification with the outcome.
Designing these messages honestly is part of designing the saga; never tell users an order is confirmed before the saga has finished.