🔥 Launch Offer: Interview Preparation Bundle @ ₹2,499 ₹9,999 • 75% OFF. Offer expires in 🔥 Launch Offer: Interview Preparation Bundle @ ₹2,499 ₹9,999 • 75% OFF. Offer expires in Interview Preparation Bundle @ ₹2,499 ₹9,999

00:00:00 Get Bundle
home kafka choreography-saga-pattern-with-kafka-in-spring-boot-complete-guide-with-real-world-example
Interview guide Kafka 41 sections

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…

Start reading
By javainterviewprep June 12, 2026 11 min read

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?

  1. Save the order.
  2. Mark it as CREATED.
  3. 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 publish InventoryReserved.
  • Payment Service knows: When I receive InventoryReserved, process payment and publish either PaymentCompleted or PaymentFailed.
  • Shipping Service knows: When I receive PaymentCompleted, create a shipment and publish ShipmentCreated.

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

FeatureOrchestrationChoreography
CoordinatorCentral Saga OrchestratorNo coordinator
CommunicationREST/Kafka commandsKafka events
CouplingMore centralizedLoosely coupled
Workflow visibilityEasy to understandDistributed across services
Adding a new stepUpdate orchestratorSubscribe to an event
Failure handlingOrchestrator triggers compensationsServices react to failure events
ScalabilityGoodExcellent
ComplexityEasier to debugHarder to visualize as the workflow grows
Best forSimple or moderately complex workflowsLarge 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.