Choreography Saga Pattern with Kafka in Spring Boot | Complete Guide with Real-World Example
If you're preparing for product-based company interviews (Oracle, Walmart, Microsoft, Amazon, Flipkart, etc.), understanding the Saga Pattern—especially Choreography Saga using Kafka—is extremely valuable. Large-scale distributed systems rarely rely…
If you’re preparing for product-based company interviews (Oracle, Walmart, Microsoft, Amazon, Flipkart, etc.), understanding the Saga Pattern—especially Choreography Saga using Kafka—is extremely valuable.
Large-scale distributed systems rarely rely on traditional database transactions across multiple microservices. Instead, they use event-driven communication to achieve eventual consistency while keeping services independent.
One of the most common approaches is the Choreography Saga Pattern.
In this article, we’ll understand:
- What is Choreography Saga?
- Why do we need it?
- How does it work with Kafka?
- Complete Spring Boot example
- Success flow
- Failure (Compensation) flow
- Advantages and disadvantages
- Interview questions
- Production improvements
Let’s begin.
Why Do We Need Saga Pattern?
Imagine an e-commerce application where placing an order involves multiple microservices.
- Order Service
- Inventory Service
- Payment Service
- Shipping Service
If everything were inside one application, we could simply use:
@Transactional
If any step failed, the entire transaction would roll back.
However, in a microservice architecture, every service owns its own database.
For example:
Order Service Database
Inventory Service Database
Payment Service Database
Shipping Service Database
Since each service has its own database, a single ACID transaction cannot span all services.
Suppose the following happens:
- Order is created successfully.
- Inventory is reserved.
- Payment fails.
Now what?
We cannot simply roll back everything using @Transactional because each service has already committed its own transaction.
This is exactly why the Saga Pattern exists.
Instead of one large transaction, a saga breaks the workflow into multiple local transactions.
If one step fails, previously completed steps execute compensating transactions to undo their work.
This ensures eventual consistency across the entire system.
What is Choreography Saga?
In the Choreography Saga Pattern, there is no central coordinator or orchestrator controlling the workflow.
Instead, every service knows only two things:
- Which event to listen to.
- Which event to publish after completing its work.
That’s it.
Each service is completely independent.
No service directly invokes another service.
All communication happens through events, typically using Kafka.
Real-World Use Case
Let’s build an online shopping application.
A customer places an order.
There are four microservices:
- Order Service
- Inventory Service
- Payment Service
- Shipping Service
Communication happens entirely through Kafka.
Overall Workflow
Client
|
▼
Order Service
|
Publish OrderCreated Event
|
Kafka
|
Inventory Service
|
Reserve Inventory
|
Publish InventoryReserved Event
|
Kafka
|
Payment Service
|
Charge Payment
|
Publish PaymentCompleted Event
|
Kafka
|
Shipping Service
|
Create Shipment
|
Publish ShipmentCreated Event
|
Kafka
|
Order Service
|
Mark Order Completed
Notice something very important:
Nobody calls another service.
Everything happens through events.
This loose coupling is the biggest advantage of choreography.
Event Classes
Events represent business facts that have already happened.
Instead of saying:
Reserve inventory.
We say:
Inventory has been reserved.
This makes the system event-driven.
OrderCreatedEvent
public class OrderCreatedEvent {
private Long orderId;
private Long productId;
private int quantity;
private double amount;
// getters setters
}
This event is published immediately after an order is created.
InventoryReservedEvent
public class InventoryReservedEvent {
private Long orderId;
private Long productId;
private int quantity;
}
This event indicates inventory has been successfully reserved.
PaymentCompletedEvent
public class PaymentCompletedEvent {
private Long orderId;
}
Published after payment succeeds.
PaymentFailedEvent
public class PaymentFailedEvent {
private Long orderId;
}
Published if payment fails.
Step 1: Order Service
The customer places an order.
@PostMapping("/orders")
public String placeOrder(@RequestBody OrderRequest request){
Order order = new Order();
order.setStatus("CREATED");
repository.save(order);
OrderCreatedEvent event = new OrderCreatedEvent();
event.setOrderId(order.getId());
event.setProductId(request.getProductId());
event.setQuantity(request.getQuantity());
event.setAmount(request.getAmount());
kafkaTemplate.send("order-created", event);
return "Order Created";
}
What happens here?
- Save the order.
- Mark it as CREATED.
- Publish an
OrderCreatedEvent.
The Order Service is now finished.
It does not know:
- Who consumes the event.
- Whether inventory exists.
- Whether payment succeeds.
- Whether shipping succeeds.
Its only responsibility is to publish an event.
Step 2: Inventory Service
Inventory Service listens for the event.
@KafkaListener(topics = "order-created")
public void reserveInventory(OrderCreatedEvent event){
Product product =
repository.findById(event.getProductId()).get();
if(product.getQuantity() < event.getQuantity()){
kafkaTemplate.send(
"inventory-failed",
new InventoryFailedEvent(event.getOrderId())
);
return;
}
product.setQuantity(
product.getQuantity()-event.getQuantity());
repository.save(product);
kafkaTemplate.send(
"inventory-reserved",
new InventoryReservedEvent(
event.getOrderId(),
event.getProductId(),
event.getQuantity()
));
}
Inventory Service never calls Payment Service.
Instead, it simply publishes another event.
That event becomes the trigger for the next service.
Step 3: Payment Service
Payment Service waits for inventory to be reserved.
@KafkaListener(topics="inventory-reserved")
public void makePayment(InventoryReservedEvent event){
try{
paymentGateway.pay();
kafkaTemplate.send(
"payment-completed",
new PaymentCompletedEvent(event.getOrderId())
);
}
catch(Exception e){
kafkaTemplate.send(
"payment-failed",
new PaymentFailedEvent(event.getOrderId())
);
}
}
Notice that the Payment Service never calls Shipping Service.
It simply publishes:
payment-completed
or
payment-failed
Whoever is interested can consume those events.
Step 4: Shipping Service
Shipping Service listens for successful payment.
@KafkaListener(topics="payment-completed")
public void createShipment(PaymentCompletedEvent event){
Shipment shipment = new Shipment();
shipment.setOrderId(event.getOrderId());
repository.save(shipment);
kafkaTemplate.send(
"shipment-created",
event
);
}
Shipment created.
Another event is published.
Step 5: Order Service Again
Finally, Order Service listens for shipment completion.
@KafkaListener(topics="shipment-created")
public void completeOrder(PaymentCompletedEvent event){
Order order =
repository.findById(event.getOrderId()).get();
order.setStatus("COMPLETED");
repository.save(order);
}
The order is now marked as completed.
Saga successfully ends.
Complete Success Flow
Order Service
Create Order
|
▼
Publish OrderCreated
|
▼
Inventory Service
Reserve Inventory
|
▼
Publish InventoryReserved
|
▼
Payment Service
Charge Card
|
▼
Publish PaymentCompleted
|
▼
Shipping Service
Create Shipment
|
▼
Publish ShipmentCreated
|
▼
Order Service
Complete Order
Every service performs one local transaction and emits an event.
What Happens If Payment Fails?
Suppose the payment gateway declines the card.
Payment Service publishes:
payment-failed
Who should react?
Inventory Service.
Inventory Compensation
@KafkaListener(topics="payment-failed")
public void releaseInventory(PaymentFailedEvent event){
Order order =
orderClient.getOrder(event.getOrderId());
Product product =
repository.findById(order.getProductId()).get();
product.setQuantity(
product.getQuantity()+order.getQuantity());
repository.save(product);
kafkaTemplate.send(
"inventory-released",
event
);
}
Here the inventory reserved earlier is released.
Notice this is called a compensating transaction.
Instead of rolling back a database transaction, we execute another business transaction that reverses the previous action.
Order Cancellation
Order Service listens for inventory release.
@KafkaListener(topics="inventory-released")
public void cancelOrder(PaymentFailedEvent event){
Order order =
repository.findById(event.getOrderId()).get();
order.setStatus("CANCELLED");
repository.save(order);
}
Saga ends.
Order is cancelled.
Inventory is restored.
No distributed transaction was required.
Complete Failure Flow
Order Service
Create Order
|
▼
Publish OrderCreated
|
▼
Inventory Service
Reserve Inventory
|
▼
Publish InventoryReserved
|
▼
Payment Service
Payment Failed
|
▼
Publish PaymentFailed
|
▼
Inventory Service
Release Inventory
|
▼
Publish InventoryReleased
|
▼
Order Service
Cancel Order
Every successful local transaction has a corresponding compensating transaction when needed.
Why Is It Called Choreography?
Think of a dance performance.
There is no manager standing on the stage telling every dancer what to do.
Instead, each dancer already knows:
- When to enter.
- What to perform.
- When to leave.
Together, they create a perfectly synchronized performance.
A choreography saga works the same way.
- Order Service knows: After creating an order, publish
OrderCreated. - Inventory Service knows: When I receive
OrderCreated, reserve stock and publishInventoryReserved. - Payment Service knows: When I receive
InventoryReserved, process payment and publish eitherPaymentCompletedorPaymentFailed. - Shipping Service knows: When I receive
PaymentCompleted, create a shipment and publishShipmentCreated.
No service knows the entire business workflow.
Each service only understands its own responsibility.
Advantages of Choreography Saga
- Very loosely coupled architecture.
- Services remain completely independent.
- Easy to add new consumers without changing existing services.
- Excellent scalability.
- Works naturally with Kafka.
- Supports asynchronous communication.
- Ideal for event-driven architectures.
- Improves fault isolation because services can continue operating independently.
Disadvantages of Choreography Saga
As the number of services grows, the event chain can become difficult to understand.
For example:
OrderCreated
↓
InventoryReserved
↓
PaymentCompleted
↓
ShipmentCreated
↓
NotificationSent
↓
InvoiceGenerated
↓
RewardPointsAdded
↓
AnalyticsUpdated
Now imagine debugging why an order failed.
The workflow is spread across multiple services.
This makes monitoring and debugging more challenging.
For this reason, production systems often use:
- Distributed tracing (OpenTelemetry, Jaeger, Zipkin)
- Correlation IDs
- Centralized logging
- Kafka monitoring tools
These tools make it possible to trace a single saga execution across all services.
Choreography vs Orchestration
| Feature | Orchestration | Choreography |
|---|---|---|
| Coordinator | Central Saga Orchestrator | No coordinator |
| Communication | REST/Kafka commands | Kafka events |
| Coupling | More centralized | Loosely coupled |
| Workflow visibility | Easy to understand | Distributed across services |
| Adding a new step | Update orchestrator | Subscribe to an event |
| Failure handling | Orchestrator triggers compensations | Services react to failure events |
| Scalability | Good | Excellent |
| Complexity | Easier to debug | Harder to visualize as the workflow grows |
| Best for | Simple or moderately complex workflows | Large event-driven microservice architectures |
Production Improvements
The above example is excellent for learning, but production systems require additional considerations.
1. Avoid Synchronous Calls During Compensation
In the sample above, Inventory Service performs:
orderClient.getOrder(event.getOrderId());
This introduces synchronous coupling.
A better approach is to include all information required for compensation in the event itself, such as:
- orderId
- productId
- quantity
or maintain a local saga state so that compensation can happen without making a remote call.
This keeps the choreography fully event-driven and more resilient.
2. Use Correlation IDs
Every event should carry a unique Saga ID or Correlation ID.
Example:
Saga ID:
8c7b9a12-43dd-4f98-bac8-3f34e0b58f12
This allows logs from every microservice to be correlated, making debugging significantly easier.
3. Make Consumers Idempotent
Kafka provides at-least-once delivery, which means the same event may be delivered more than once.
Consumers should therefore be idempotent—processing the same event multiple times should not change the final result.
For example, before reserving inventory or creating a shipment, check whether the event has already been processed.
4. Use Dead Letter Topics (DLT)
If an event repeatedly fails to process, don’t keep retrying forever.
Instead, move it to a Dead Letter Topic for later investigation.
This prevents one problematic message from blocking the system.
5. Version Your Events
Never change the structure of an existing event in a way that breaks consumers.
Instead, introduce a new version (for example, OrderCreatedEventV2) or use backward-compatible changes.
This allows old and new services to coexist during deployments.
6. Handle Ordering Correctly
If events related to the same order are processed out of sequence, the workflow can break.
Use the orderId as the Kafka message key so that all events for the same order are routed to the same partition and processed in order.
7. Add Retry Mechanisms
Transient failures such as temporary database issues or network glitches should be retried automatically using exponential backoff before sending the event to a Dead Letter Topic.
8. Monitor the Entire Saga
In production, monitoring is just as important as implementation.
Track metrics such as:
- Orders created
- Inventory reservations
- Payment failures
- Compensation executions
- Average saga completion time
These metrics help detect bottlenecks and failures before they impact users.
Common Interview Questions
Why can’t we use @Transactional?
Because each microservice has its own database, and a single ACID transaction cannot span multiple independent services.
Why Kafka instead of REST?
Kafka enables asynchronous communication, loose coupling, higher scalability, and better resilience. Services continue publishing and consuming events without waiting for immediate responses.
What is a compensating transaction?
A compensating transaction is a business operation that logically undoes the effect of a previously completed local transaction, such as releasing reserved inventory after a payment failure.
What is eventual consistency?
Eventual consistency means that while all services may not be immediately synchronized, they will eventually reach a consistent state after all events and compensating transactions are processed.
Can one event have multiple consumers?
Yes. That’s one of Kafka’s biggest strengths.
For example, PaymentCompleted can be consumed by:
- Shipping Service
- Notification Service
- Invoice Service
- Analytics Service
- Loyalty Points Service
without changing the Payment Service at all.
Key Takeaways
- Choreography Saga removes the need for a central coordinator.
- Services communicate exclusively through events.
- Kafka acts as the event broker between microservices.
- Each service performs a local transaction and publishes an event.
- Failures are handled through compensating transactions instead of distributed rollbacks.
- The architecture is highly scalable and loosely coupled, making it ideal for large event-driven systems.
- Production implementations should include correlation IDs, idempotent consumers, retries, Dead Letter Topics, event versioning, and comprehensive monitoring.
Final Thoughts
The Choreography Saga Pattern is one of the most commonly used approaches for building scalable, resilient, and loosely coupled microservices. By relying on asynchronous event-driven communication with Kafka, services remain independent and can evolve without tightly coupling to one another.
While the learning example presented here demonstrates the core concepts, production-grade implementations add layers of resilience through idempotency, retries, observability, correlation IDs, dead-letter topics, and event versioning.
If you’re preparing for interviews at companies like Oracle, Walmart, Microsoft, Amazon, or other product-based organizations, understanding how the success flow, compensation flow, and event-driven communication work together will help you confidently answer both conceptual and coding-based questions on distributed transactions and microservice architecture.