Skip to main content

Command Palette

Search for a command to run...

Understanding the Saga Pattern in Spring Boot: A Complete Guide

Published
β€’24 min read
S

πŸ‘‹ Hey there! I’m Shohanur Rahman!

I’m a backend developer with over 5.5 years of experience in building scalable and efficient web applications. My work focuses on Java, Spring Boot, and microservices architecture, where I love designing robust API solutions and creating secure middleware for complex integrations.

πŸ’Ό What I Do Backend Development: Expert in Spring Boot, Spring Cloud, and Spring WebFlux, I create high-performance microservices that drive seamless user experiences. Cloud & DevOps: AWS enthusiast, skilled in using EC2, S3, RDS, and Docker to design scalable and reliable cloud infrastructures. Digital Security: Passionate about securing applications with OAuth2, Keycloak, and digital signatures for data integrity and privacy. πŸš€ Current Projects I’m currently working on API integrations with Spring Cloud Gateway and designing an e-invoicing middleware. My projects often involve asynchronous processing, digital signature implementations, and ensuring high standards of security.

πŸ“ Why I Write I enjoy sharing what I’ve learned through blog posts, covering everything from backend design to API security and cloud best practices. Check out my posts if you’re into backend dev, cloud tech, or digital security!

Introduction

In the world of microservices architecture, managing distributed transactions across multiple services is one of the most challenging problems developers face. Traditional ACID transactions that work seamlessly in monolithic applications fall short when dealing with distributed systems. This is where the Saga pattern comes to the rescue.

In this blog post, we'll explore what the Saga pattern is, the problems it solves, and how to implement both Choreography and Orchestration approaches in Spring Boot with practical examples.

What is the Saga Pattern?

The Saga pattern is a design pattern for managing distributed transactions in microservices architecture. It breaks down a long-running transaction into a series of smaller, independent transactions (called "steps" or "local transactions"), where each step has a corresponding compensating transaction to undo its effects if something goes wrong.

Think of it as a chain of events where:

  • Each step performs a specific business operation

  • If any step fails, compensating transactions are executed in reverse order to maintain data consistency

  • The overall system remains eventually consistent

The Problem Saga Solves

Traditional Distributed Transaction Challenges

In a microservices architecture, you typically face these challenges:

  1. No Distributed ACID Transactions: Each microservice has its own database, making traditional two-phase commit (2PC) protocols impractical or impossible.

  2. Data Consistency: Maintaining consistency across multiple services without a distributed transaction manager is complex.

  3. Service Independence: Services need to remain loosely coupled and independent, which conflicts with tightly coordinated transactions.

  4. Failure Handling: When one service in a chain fails, how do you roll back changes made by other services?

Real-World Example

Imagine an e-commerce order processing system:

  1. Order Service: Creates an order

  2. Payment Service: Processes payment

  3. Inventory Service: Reserves inventory

  4. Shipping Service: Schedules delivery

If the shipping service fails after payment and inventory are already processed, you need to:

  • Refund the payment

  • Release the reserved inventory

  • Cancel the order

The Saga pattern provides a structured way to handle this scenario.

Types of Saga Implementation

There are two main approaches to implementing the Saga pattern:

1. Choreography-based Saga

Each service publishes events and listens to other services' events to decide the next action. Services communicate through an event bus. No central coordinator.

Pros:

  • Simple for small workflows

  • No single point of failure

  • Loose coupling

  • Services are autonomous

Cons:

  • Complex to track and debug

  • Circular dependencies risk

  • Difficult to understand the overall flow

  • Harder to implement complex workflows

Diagram:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€οΏ½οΏ½οΏ½β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚    Order     β”‚ event   β”‚   Payment    β”‚ event   β”‚  Inventory   β”‚
β”‚   Service    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚   Service    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚   Service    β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚                        β”‚                        β”‚
       β”‚β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚         Listens to all events (for monitoring)

2. Orchestration-based Saga

A central orchestrator (coordinator) tells services what operations to perform and handles the workflow logic.

Pros:

  • Centralized logic and easier to understand

  • Better visibility and monitoring

  • Easier to handle complex workflows

  • Clear separation of concerns

Cons:

  • Orchestrator is a single point of failure

  • Additional component to maintain

Diagram:

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ Saga Orchestrator    β”‚
                    β”‚   (Coordinator)      β”‚
                    β””β”€β”€β”€β”€β”€οΏ½οΏ½οΏ½β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚                    β”‚                    β”‚
          β–Ό                    β–Ό                    β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚    Order     β”‚     β”‚   Payment    β”‚    β”‚  Inventory   β”‚
   β”‚   Service    β”‚     β”‚   Service    β”‚    β”‚   Service    β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

In this guide, we'll implement BOTH approaches!


Part 1: Choreography-based Saga Implementation

Choreography Architecture Overview

In choreography, services react to events published by other services. There's no central coordinator.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          Kafka Event Bus                            β”‚
β”‚                                                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”‚
β”‚  β”‚order-events   β”‚  β”‚payment-events β”‚  β”‚inventory-eventsβ”‚         β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚   β–²              β”‚   β–²              β”‚   β–²
         β”‚   β”‚              β”‚   β”‚              β”‚   β”‚
    publishesβ”‚         publishesβ”‚         publishesβ”‚
    listens  β”‚         listens  β”‚         listens  β”‚
         β”‚   β”‚              β”‚   β”‚              β”‚   β”‚
         β–Ό   β”‚              β–Ό   β”‚              β–Ό   β”‚
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚   Order     β”‚    β”‚   Payment   β”‚    β”‚  Inventory  β”‚
   β”‚   Service   β”‚    β”‚   Service   β”‚    β”‚   Service   β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   β”‚    DB       β”‚    β”‚    DB       β”‚    β”‚    DB       β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Choreography Event Flow

Success Scenario

User Request
     β”‚
     β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Order     β”‚ 1. Create Order
β”‚  Service    β”‚ 2. Publish ORDER_CREATED to order-events
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚                                β”‚
       β–Ό                                β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Payment   β”‚                  β”‚  Inventory  β”‚
β”‚   Service   β”‚                  β”‚   Service   β”‚
β”‚(listening)  β”‚                  β”‚ (listening) β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β”‚ 3. Process Payment
       β”‚ 4. Publish PAYMENT_COMPLETED to payment-events
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Inventory  β”‚ 5. Listen to PAYMENT_COMPLETED
β”‚   Service   β”‚ 6. Reserve Inventory
β”‚             β”‚ 7. Publish INVENTORY_RESERVED to inventory-events
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Order     β”‚ 8. Listen to INVENTORY_RESERVED
β”‚  Service    β”‚ 9. Update Order Status to COMPLETED
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Failure Scenario

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Inventory  β”‚ 1. Inventory reservation FAILS
β”‚   Service   β”‚ 2. Publish INVENTORY_FAILED to inventory-events
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚                                β”‚
       β–Ό                                β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Payment   β”‚                  β”‚    Order    β”‚
β”‚   Service   β”‚                  β”‚   Service   β”‚
β”‚(listening)  β”‚                  β”‚ (listening) β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜                  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚                                β”‚
       β”‚ 3. Compensate Payment          β”‚ 4. Update Order
       β”‚ 4. Publish PAYMENT_REFUNDED    β”‚    Status to FAILED
       β”‚                                β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Choreography Implementation

Step 1: Common Event Models

// OrderEvent.java
package com.example.common.event;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderEvent {
    private String eventId;
    private String orderId;
    private String userId;
    private String productId;
    private Integer quantity;
    private Double amount;
    private OrderEventType eventType;
    private LocalDateTime timestamp;

    public enum OrderEventType {
        ORDER_CREATED,
        ORDER_COMPLETED,
        ORDER_FAILED,
        ORDER_CANCELLED
    }
}
// PaymentEvent.java
package com.example.common.event;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class PaymentEvent {
    private String eventId;
    private String orderId;
    private String userId;
    private Double amount;
    private PaymentEventType eventType;
    private LocalDateTime timestamp;

    public enum PaymentEventType {
        PAYMENT_PROCESSING,
        PAYMENT_COMPLETED,
        PAYMENT_FAILED,
        PAYMENT_REFUNDED
    }
}
// InventoryEvent.java
package com.example.common.event;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class InventoryEvent {
    private String eventId;
    private String orderId;
    private String productId;
    private Integer quantity;
    private InventoryEventType eventType;
    private LocalDateTime timestamp;

    public enum InventoryEventType {
        INVENTORY_CHECKING,
        INVENTORY_RESERVED,
        INVENTORY_FAILED,
        INVENTORY_RELEASED
    }
}

Step 2: Order Service (Choreography)

// OrderService.java
package com.example.orderservice.service;

import com.example.common.event.InventoryEvent;
import com.example.common.event.OrderEvent;
import com.example.common.event.PaymentEvent;
import com.example.orderservice.entity.Order;
import com.example.orderservice.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.UUID;

@Service
@RequiredArgsConstructor
@Slf4j
public class OrderService {

    private final OrderRepository orderRepository;
    private final KafkaTemplate<String, OrderEvent> orderEventKafkaTemplate;

    /**
     * Create order and publish ORDER_CREATED event
     */
    @Transactional
    public Order createOrder(String userId, String productId, Integer quantity, Double amount) {
        log.info("Creating order for user: {}", userId);

        // Create and save order
        Order order = new Order();
        order.setOrderId(UUID.randomUUID().toString());
        order.setUserId(userId);
        order.setProductId(productId);
        order.setQuantity(quantity);
        order.setAmount(amount);
        order.setStatus("PENDING");
        order.setCreatedAt(LocalDateTime.now());

        orderRepository.save(order);

        // Publish ORDER_CREATED event - other services will react to this
        OrderEvent event = new OrderEvent(
            UUID.randomUUID().toString(),
            order.getOrderId(),
            userId,
            productId,
            quantity,
            amount,
            OrderEvent.OrderEventType.ORDER_CREATED,
            LocalDateTime.now()
        );

        orderEventKafkaTemplate.send("order-events", event);
        log.info("Published ORDER_CREATED event for order: {}", order.getOrderId());

        return order;
    }

    /**
     * Listen to INVENTORY_RESERVED events
     * When inventory is reserved, order is completed
     */
    @KafkaListener(topics = "inventory-events", groupId = "order-service-group")
    @Transactional
    public void handleInventoryEvent(InventoryEvent event) {
        log.info("Received inventory event: {} for order: {}", event.getEventType(), event.getOrderId());

        if (event.getEventType() == InventoryEvent.InventoryEventType.INVENTORY_RESERVED) {
            // Success! Complete the order
            Order order = orderRepository.findById(event.getOrderId())
                .orElseThrow(() -> new RuntimeException("Order not found: " + event.getOrderId()));

            order.setStatus("COMPLETED");
            order.setUpdatedAt(LocalDateTime.now());
            orderRepository.save(order);

            log.info("βœ… Order completed: {}", event.getOrderId());

            // Publish ORDER_COMPLETED event (for monitoring/analytics)
            OrderEvent orderEvent = new OrderEvent(
                UUID.randomUUID().toString(),
                order.getOrderId(),
                order.getUserId(),
                order.getProductId(),
                order.getQuantity(),
                order.getAmount(),
                OrderEvent.OrderEventType.ORDER_COMPLETED,
                LocalDateTime.now()
            );
            orderEventKafkaTemplate.send("order-events", orderEvent);
        }
    }

    /**
     * Listen to PAYMENT_FAILED events
     * If payment fails, mark order as failed
     */
    @KafkaListener(topics = "payment-events", groupId = "order-service-group")
    @Transactional
    public void handlePaymentEvent(PaymentEvent event) {
        log.info("Received payment event: {} for order: {}", event.getEventType(), event.getOrderId());

        if (event.getEventType() == PaymentEvent.PaymentEventType.PAYMENT_FAILED) {
            Order order = orderRepository.findById(event.getOrderId())
                .orElseThrow(() -> new RuntimeException("Order not found: " + event.getOrderId()));

            order.setStatus("FAILED");
            order.setUpdatedAt(LocalDateTime.now());
            orderRepository.save(order);

            log.error("❌ Order failed due to payment failure: {}", event.getOrderId());
        }
    }
}
// OrderController.java
package com.example.orderservice.controller;

import com.example.orderservice.entity.Order;
import com.example.orderservice.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {

    private final OrderService orderService;

    @PostMapping
    public ResponseEntity<Order> createOrder(@RequestBody Map<String, Object> request) {
        Order order = orderService.createOrder(
            (String) request.get("userId"),
            (String) request.get("productId"),
            (Integer) request.get("quantity"),
            (Double) request.get("amount")
        );
        return ResponseEntity.ok(order);
    }
}

Step 3: Payment Service (Choreography)

// PaymentService.java
package com.example.paymentservice.service;

import com.example.common.event.InventoryEvent;
import com.example.common.event.OrderEvent;
import com.example.common.event.PaymentEvent;
import com.example.paymentservice.entity.Payment;
import com.example.paymentservice.repository.PaymentRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.Optional;
import java.util.UUID;

@Service
@RequiredArgsConstructor
@Slf4j
public class PaymentService {

    private final PaymentRepository paymentRepository;
    private final KafkaTemplate<String, PaymentEvent> paymentEventKafkaTemplate;

    /**
     * Listen to ORDER_CREATED events
     * When order is created, process payment
     */
    @KafkaListener(topics = "order-events", groupId = "payment-service-group")
    @Transactional
    public void handleOrderEvent(OrderEvent event) {
        log.info("Received order event: {} for order: {}", event.getEventType(), event.getOrderId());

        if (event.getEventType() == OrderEvent.OrderEventType.ORDER_CREATED) {
            processPayment(event);
        }
    }

    /**
     * Process payment and publish result
     */
    private void processPayment(OrderEvent orderEvent) {
        try {
            log.info("Processing payment for order: {}", orderEvent.getOrderId());

            // Check for idempotency
            Optional<Payment> existing = paymentRepository.findByOrderId(orderEvent.getOrderId());
            if (existing.isPresent()) {
                log.info("Payment already processed for order: {}", orderEvent.getOrderId());
                return;
            }

            // Simulate payment processing
            // In real scenario, call payment gateway API

            // Save payment
            Payment payment = new Payment();
            payment.setOrderId(orderEvent.getOrderId());
            payment.setUserId(orderEvent.getUserId());
            payment.setAmount(orderEvent.getAmount());
            payment.setStatus("COMPLETED");
            payment.setCreatedAt(LocalDateTime.now());
            paymentRepository.save(payment);

            log.info("βœ… Payment completed for order: {}", orderEvent.getOrderId());

            // Publish PAYMENT_COMPLETED event - Inventory service will react
            PaymentEvent paymentEvent = new PaymentEvent(
                UUID.randomUUID().toString(),
                orderEvent.getOrderId(),
                orderEvent.getUserId(),
                orderEvent.getAmount(),
                PaymentEvent.PaymentEventType.PAYMENT_COMPLETED,
                LocalDateTime.now()
            );

            paymentEventKafkaTemplate.send("payment-events", paymentEvent);
            log.info("Published PAYMENT_COMPLETED event for order: {}", orderEvent.getOrderId());

        } catch (Exception e) {
            log.error("❌ Payment failed for order: {}", orderEvent.getOrderId(), e);

            // Publish PAYMENT_FAILED event
            PaymentEvent failureEvent = new PaymentEvent(
                UUID.randomUUID().toString(),
                orderEvent.getOrderId(),
                orderEvent.getUserId(),
                orderEvent.getAmount(),
                PaymentEvent.PaymentEventType.PAYMENT_FAILED,
                LocalDateTime.now()
            );

            paymentEventKafkaTemplate.send("payment-events", failureEvent);
        }
    }

    /**
     * Listen to INVENTORY_FAILED events
     * When inventory fails, compensate by refunding payment
     */
    @KafkaListener(topics = "inventory-events", groupId = "payment-service-group")
    @Transactional
    public void handleInventoryEvent(InventoryEvent event) {
        log.info("Received inventory event: {} for order: {}", event.getEventType(), event.getOrderId());

        if (event.getEventType() == InventoryEvent.InventoryEventType.INVENTORY_FAILED) {
            compensatePayment(event.getOrderId());
        }
    }

    /**
     * Compensate payment (refund)
     */
    private void compensatePayment(String orderId) {
        log.info("Compensating payment for order: {}", orderId);

        Payment payment = paymentRepository.findByOrderId(orderId)
            .orElseThrow(() -> new RuntimeException("Payment not found for order: " + orderId));

        payment.setStatus("REFUNDED");
        payment.setUpdatedAt(LocalDateTime.now());
        paymentRepository.save(payment);

        log.info("βœ… Payment refunded for order: {}", orderId);

        // Publish PAYMENT_REFUNDED event
        PaymentEvent refundEvent = new PaymentEvent(
            UUID.randomUUID().toString(),
            orderId,
            payment.getUserId(),
            payment.getAmount(),
            PaymentEvent.PaymentEventType.PAYMENT_REFUNDED,
            LocalDateTime.now()
        );

        paymentEventKafkaTemplate.send("payment-events", refundEvent);
    }
}
// Payment.java (Entity)
package com.example.paymentservice.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "payments")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Payment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String orderId;
    private String userId;
    private Double amount;
    private String status; // COMPLETED, REFUNDED
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}
// PaymentRepository.java
package com.example.paymentservice.repository;

import com.example.paymentservice.entity.Payment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface PaymentRepository extends JpaRepository<Payment, Long> {
    Optional<Payment> findByOrderId(String orderId);
}

Step 4: Inventory Service (Choreography)

// InventoryService.java
package com.example.inventoryservice.service;

import com.example.common.event.InventoryEvent;
import com.example.common.event.PaymentEvent;
import com.example.inventoryservice.entity.Inventory;
import com.example.inventoryservice.repository.InventoryRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.UUID;

@Service
@RequiredArgsConstructor
@Slf4j
public class InventoryService {

    private final InventoryRepository inventoryRepository;
    private final KafkaTemplate<String, InventoryEvent> inventoryEventKafkaTemplate;

    /**
     * Listen to PAYMENT_COMPLETED events
     * When payment is completed, reserve inventory
     */
    @KafkaListener(topics = "payment-events", groupId = "inventory-service-group")
    @Transactional
    public void handlePaymentEvent(PaymentEvent event) {
        log.info("Received payment event: {} for order: {}", event.getEventType(), event.getOrderId());

        if (event.getEventType() == PaymentEvent.PaymentEventType.PAYMENT_COMPLETED) {
            // Extract product details from the event
            // In real scenario, you might need to query Order Service or pass more data in event
            // For simplicity, we're assuming productId is passed through OrderEvent
            // You might need to store this temporarily or pass through events

            // For demonstration, let's assume we have the data
            // In production, you'd design events to carry necessary context
            log.warn("⚠️ Note: In production, you'd need to pass productId and quantity through events");
        }
    }

    /**
     * Reserve inventory for order
     * This would be called with proper context in production
     */
    @Transactional
    public void reserveInventory(String orderId, String productId, Integer quantity) {
        try {
            log.info("Reserving inventory for order: {}, product: {}, quantity: {}", 
                     orderId, productId, quantity);

            Inventory inventory = inventoryRepository.findById(productId)
                .orElseThrow(() -> new RuntimeException("Product not found: " + productId));

            if (inventory.getAvailableQuantity() >= quantity) {
                // Reserve inventory
                inventory.setAvailableQuantity(inventory.getAvailableQuantity() - quantity);
                inventory.setReservedQuantity(inventory.getReservedQuantity() + quantity);
                inventoryRepository.save(inventory);

                log.info("βœ… Inventory reserved for order: {}", orderId);

                // Publish INVENTORY_RESERVED event - Order service will react
                InventoryEvent inventoryEvent = new InventoryEvent(
                    UUID.randomUUID().toString(),
                    orderId,
                    productId,
                    quantity,
                    InventoryEvent.InventoryEventType.INVENTORY_RESERVED,
                    LocalDateTime.now()
                );

                inventoryEventKafkaTemplate.send("inventory-events", inventoryEvent);
                log.info("Published INVENTORY_RESERVED event for order: {}", orderId);

            } else {
                throw new RuntimeException("Insufficient inventory for product: " + productId);
            }

        } catch (Exception e) {
            log.error("❌ Inventory reservation failed for order: {}", orderId, e);

            // Publish INVENTORY_FAILED event - Payment service will compensate
            InventoryEvent failureEvent = new InventoryEvent(
                UUID.randomUUID().toString(),
                orderId,
                productId,
                quantity,
                InventoryEvent.InventoryEventType.INVENTORY_FAILED,
                LocalDateTime.now()
            );

            inventoryEventKafkaTemplate.send("inventory-events", failureEvent);
            log.info("Published INVENTORY_FAILED event for order: {}", orderId);
        }
    }
}

Step 5: Enhanced Payment Service with Product Context

To make the choreography work properly, we need to pass product context through events:

// Enhanced PaymentEvent.java
package com.example.common.event;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class PaymentEvent {
    private String eventId;
    private String orderId;
    private String userId;
    private String productId;  // Added for context
    private Integer quantity;   // Added for context
    private Double amount;
    private PaymentEventType eventType;
    private LocalDateTime timestamp;

    public enum PaymentEventType {
        PAYMENT_PROCESSING,
        PAYMENT_COMPLETED,
        PAYMENT_FAILED,
        PAYMENT_REFUNDED
    }
}
// Enhanced InventoryService.java with proper event handling
@Service
@RequiredArgsConstructor
@Slf4j
public class InventoryService {

    private final InventoryRepository inventoryRepository;
    private final KafkaTemplate<String, InventoryEvent> inventoryEventKafkaTemplate;

    /**
     * Listen to PAYMENT_COMPLETED events
     * When payment is completed, reserve inventory
     */
    @KafkaListener(topics = "payment-events", groupId = "inventory-service-group")
    @Transactional
    public void handlePaymentEvent(PaymentEvent event) {
        log.info("Received payment event: {} for order: {}", event.getEventType(), event.getOrderId());

        if (event.getEventType() == PaymentEvent.PaymentEventType.PAYMENT_COMPLETED) {
            reserveInventory(event.getOrderId(), event.getProductId(), event.getQuantity());
        }
    }

    /**
     * Reserve inventory for order
     */
    @Transactional
    public void reserveInventory(String orderId, String productId, Integer quantity) {
        try {
            log.info("Reserving inventory for order: {}, product: {}, quantity: {}", 
                     orderId, productId, quantity);

            Inventory inventory = inventoryRepository.findById(productId)
                .orElseThrow(() -> new RuntimeException("Product not found: " + productId));

            if (inventory.getAvailableQuantity() >= quantity) {
                // Reserve inventory
                inventory.setAvailableQuantity(inventory.getAvailableQuantity() - quantity);
                inventory.setReservedQuantity(inventory.getReservedQuantity() + quantity);
                inventoryRepository.save(inventory);

                log.info("βœ… Inventory reserved for order: {}", orderId);

                // Publish INVENTORY_RESERVED event
                InventoryEvent inventoryEvent = new InventoryEvent(
                    UUID.randomUUID().toString(),
                    orderId,
                    productId,
                    quantity,
                    InventoryEvent.InventoryEventType.INVENTORY_RESERVED,
                    LocalDateTime.now()
                );

                inventoryEventKafkaTemplate.send("inventory-events", inventoryEvent);
                log.info("Published INVENTORY_RESERVED event for order: {}", orderId);

            } else {
                throw new RuntimeException("Insufficient inventory for product: " + productId);
            }

        } catch (Exception e) {
            log.error("❌ Inventory reservation failed for order: {}", orderId, e);

            // Publish INVENTORY_FAILED event
            InventoryEvent failureEvent = new InventoryEvent(
                UUID.randomUUID().toString(),
                orderId,
                productId,
                quantity,
                InventoryEvent.InventoryEventType.INVENTORY_FAILED,
                LocalDateTime.now()
            );

            inventoryEventKafkaTemplate.send("inventory-events", failureEvent);
            log.info("Published INVENTORY_FAILED event for order: {}", orderId);
        }
    }
}

Step 6: Kafka Configuration for Choreography

# application.yml (for all services)
spring:
  application:
    name: order-service  # Change for each service

  kafka:
    bootstrap-servers: localhost:9092

    # Consumer configuration
    consumer:
      group-id: ${spring.application.name}-group
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      properties:
        spring.json.trusted.packages: "*"
        spring.json.type.mapping: >
          orderEvent:com.example.common.event.OrderEvent,
          paymentEvent:com.example.common.event.PaymentEvent,
          inventoryEvent:com.example.common.event.InventoryEvent

    # Producer configuration
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer

  # Database Configuration
  datasource:
    url: jdbc:h2:mem:orderdb
    driver-class-name: org.h2.Driver
    username: sa
    password: 

  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true

server:
  port: 8081  # Different for each service

Choreography Topics Summary

Topic NamePublisherListener(s)Purpose
order-eventsOrder ServicePayment ServiceNotify when order is created
payment-eventsPayment ServiceInventory Service, Order ServiceNotify payment status
inventory-eventsInventory ServiceOrder Service, Payment ServiceNotify inventory status

Choreography vs Orchestration: Side-by-Side Comparison

Event Flow Comparison

Choreography:

Order Service  β†’  Payment Service  β†’  Inventory Service  β†’  Order Service
   (publishes)      (listens & acts)    (listens & acts)      (listens)

Orchestration:

                    β”Œβ”€ Orchestrator ─┐
                    β”‚    (decides)   β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    ↓       ↓       ↓
Order Service   Payment Service   Inventory Service

Code Comparison

Choreography - No Orchestrator:

// Each service reacts to events independently
@KafkaListener(topics = "order-events")
public void handleOrderEvent(OrderEvent event) {
    if (event.getEventType() == ORDER_CREATED) {
        processPayment(event);
    }
}

Orchestration - Central Coordinator:

// Orchestrator decides the next step
@KafkaListener(topics = "saga-events")
public void handleSagaEvent(SagaEvent event) {
    switch (event.getEventType()) {
        case ORDER_CREATED:
            kafkaTemplate.send("payment-commands", event);
            break;
        case PAYMENT_PROCESSED:
            kafkaTemplate.send("inventory-commands", event);
            break;
    }
}

Part 2: Orchestration-based Saga Implementation

Architecture Overview

Here's how our saga implementation will work:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          Kafka Topics                               β”‚
β”‚                                                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚
β”‚  β”‚saga-events  β”‚  β”‚payment-      β”‚  β”‚inventory-    β”‚  ...more     β”‚
β”‚  β”‚             β”‚  β”‚commands      β”‚  β”‚commands      β”‚             β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β–²β”‚                  β”‚β–²                 β”‚β–²
         β”‚β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚                 β”‚β”‚
         β”‚     publishes      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
         β”‚     listens                           β”‚
         β”‚                                       β”‚
    β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€οΏ½οΏ½οΏ½β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”
    β”‚          Saga Orchestrator                    β”‚
    β”‚     (Coordinates the entire workflow)         β”‚
    β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
         β”‚ commands                             β”‚ commands
         β–Ό                                      β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚   Payment    β”‚                      β”‚  Inventory   β”‚
   β”‚   Service    β”‚                      β”‚   Service    β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   β”‚    DB        β”‚                      β”‚    DB        β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Let's implement an Orchestration-based Saga for our e-commerce order example using Spring Boot.

Project Structure

saga-pattern-demo/
β”œβ”€β”€ common-lib/
β”‚   └── src/main/java/com/example/common/
β”‚       β”œβ”€β”€ dto/
β”‚       β”‚   β”œβ”€β”€ OrderDTO.java
β”‚       β”‚   └── OrderStatus.java
β”‚       └── event/
β”‚           β”œβ”€β”€ SagaEvent.java
β”‚           └── EventType.java
β”œβ”€β”€ order-service/
β”‚   └── src/main/java/com/example/orderservice/
β”‚       β”œβ”€β”€ controller/
β”‚       β”œβ”€β”€ entity/
β”‚       β”œβ”€β”€ repository/
β”‚       └── service/
β”œβ”€β”€ payment-service/
β”‚   └── src/main/java/com/example/paymentservice/
β”‚       β”œβ”€β”€ entity/
β”‚       β”œβ”€β”€ repository/
β”‚       └── service/
β”œβ”€β”€ inventory-service/
β”‚   └── src/main/java/com/example/inventoryservice/
β”‚       β”œβ”€β”€ entity/
β”‚       β”œβ”€β”€ repository/
β”‚       └── service/
└── saga-orchestrator/
    └── src/main/java/com/example/orchestrator/
        └── service/

Step 1: Add Dependencies

Add these dependencies to your pom.xml for all services:

<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Boot Starter Data JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- Spring Kafka for event messaging -->
    <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka</artifactId>
    </dependency>

    <!-- Lombok for reducing boilerplate -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- H2 Database -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

Step 2: Define Common Models

// OrderDTO.java
package com.example.common.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderDTO {
    private String orderId;
    private String userId;
    private String productId;
    private Integer quantity;
    private Double amount;
    private OrderStatus status;
}
// OrderStatus.java
package com.example.common.dto;

public enum OrderStatus {
    PENDING,
    ORDER_CREATED,
    PAYMENT_COMPLETED,
    INVENTORY_RESERVED,
    ORDER_COMPLETED,
    ORDER_FAILED,
    ORDER_CANCELLED
}
// SagaEvent.java
package com.example.common.event;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SagaEvent {
    private String orderId;
    private EventType eventType;
    private Object payload;
}
// EventType.java
package com.example.common.event;

public enum EventType {
    ORDER_CREATED,
    PAYMENT_PROCESSED,
    PAYMENT_FAILED,
    INVENTORY_RESERVED,
    INVENTORY_FAILED,
    ORDER_COMPLETED,
    ORDER_FAILED,
    COMPENSATE_PAYMENT,
    COMPENSATE_INVENTORY
}

Step 3: Order Service

// Order.java (Entity)
package com.example.orderservice.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Entity
@Table(name = "orders")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
    @Id
    private String orderId;
    private String userId;
    private String productId;
    private Integer quantity;
    private Double amount;
    private String status;
}
// OrderRepository.java
package com.example.orderservice.repository;

import com.example.orderservice.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderRepository extends JpaRepository<Order, String> {
}
// OrderService.java
package com.example.orderservice.service;

import com.example.common.dto.OrderDTO;
import com.example.common.dto.OrderStatus;
import com.example.common.event.EventType;
import com.example.common.event.SagaEvent;
import com.example.orderservice.entity.Order;
import com.example.orderservice.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.UUID;

@Service
@RequiredArgsConstructor
@Slf4j
public class OrderService {

    private final OrderRepository orderRepository;
    private final KafkaTemplate<String, SagaEvent> kafkaTemplate;

    @Transactional
    public OrderDTO createOrder(OrderDTO orderDTO) {
        // Generate order ID
        String orderId = UUID.randomUUID().toString();
        orderDTO.setOrderId(orderId);
        orderDTO.setStatus(OrderStatus.PENDING);

        // Save order
        Order order = new Order();
        order.setOrderId(orderDTO.getOrderId());
        order.setUserId(orderDTO.getUserId());
        order.setProductId(orderDTO.getProductId());
        order.setQuantity(orderDTO.getQuantity());
        order.setAmount(orderDTO.getAmount());
        order.setStatus(OrderStatus.PENDING.name());

        orderRepository.save(order);
        log.info("Order created: {}", orderId);

        // Publish ORDER_CREATED event to saga-events topic
        SagaEvent event = new SagaEvent(orderId, EventType.ORDER_CREATED, orderDTO);
        kafkaTemplate.send("saga-events", event);
        log.info("Published ORDER_CREATED event for order: {}", orderId);

        return orderDTO;
    }

    // Listen for status updates from orchestrator
    @KafkaListener(topics = "order-status-updates", groupId = "order-service")
    @Transactional
    public void handleStatusUpdate(SagaEvent event) {
        log.info("Received status update for order: {}", event.getOrderId());
        OrderDTO orderDTO = (OrderDTO) event.getPayload();
        updateOrderStatus(orderDTO.getOrderId(), orderDTO.getStatus());
    }

    @Transactional
    public void updateOrderStatus(String orderId, OrderStatus status) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new RuntimeException("Order not found: " + orderId));
        order.setStatus(status.name());
        orderRepository.save(order);
        log.info("Order {} status updated to {}", orderId, status);
    }
}
// OrderController.java
package com.example.orderservice.controller;

import com.example.common.dto.OrderDTO;
import com.example.orderservice.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {

    private final OrderService orderService;

    @PostMapping
    public ResponseEntity<OrderDTO> createOrder(@RequestBody OrderDTO orderDTO) {
        OrderDTO createdOrder = orderService.createOrder(orderDTO);
        return ResponseEntity.ok(createdOrder);
    }
}

Step 4: Payment Service

// Payment.java (Entity)
package com.example.paymentservice.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Entity
@Table(name = "payments")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Payment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String orderId;
    private String userId;
    private Double amount;
    private String status; // COMPLETED, REFUNDED
}
// PaymentRepository.java
package com.example.paymentservice.repository;

import com.example.paymentservice.entity.Payment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface PaymentRepository extends JpaRepository<Payment, Long> {
    Optional<Payment> findByOrderId(String orderId);
}
// PaymentService.java
package com.example.paymentservice.service;

import com.example.common.dto.OrderDTO;
import com.example.common.event.EventType;
import com.example.common.event.SagaEvent;
import com.example.paymentservice.entity.Payment;
import com.example.paymentservice.repository.PaymentRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Slf4j
public class PaymentService {

    private final PaymentRepository paymentRepository;
    private final KafkaTemplate<String, SagaEvent> kafkaTemplate;

    /**
     * Listen for payment commands from the orchestrator
     */
    @KafkaListener(topics = "payment-commands", groupId = "payment-service")
    @Transactional
    public void processPayment(SagaEvent event) {
        OrderDTO orderDTO = (OrderDTO) event.getPayload();

        try {
            log.info("Processing payment for order: {}", orderDTO.getOrderId());

            // Simulate payment processing logic
            // In real scenario, integrate with payment gateway

            // Save payment record
            Payment payment = new Payment();
            payment.setOrderId(orderDTO.getOrderId());
            payment.setUserId(orderDTO.getUserId());
            payment.setAmount(orderDTO.getAmount());
            payment.setStatus("COMPLETED");
            paymentRepository.save(payment);

            log.info("Payment completed for order: {}", orderDTO.getOrderId());

            // Send SUCCESS response back to orchestrator via saga-events
            SagaEvent responseEvent = new SagaEvent(
                orderDTO.getOrderId(), 
                EventType.PAYMENT_PROCESSED, 
                orderDTO
            );
            kafkaTemplate.send("saga-events", responseEvent);
            log.info("Published PAYMENT_PROCESSED event for order: {}", orderDTO.getOrderId());

        } catch (Exception e) {
            log.error("Payment failed for order: {}", orderDTO.getOrderId(), e);

            // Send FAILURE response back to orchestrator via saga-events
            SagaEvent failureEvent = new SagaEvent(
                orderDTO.getOrderId(), 
                EventType.PAYMENT_FAILED, 
                orderDTO
            );
            kafkaTemplate.send("saga-events", failureEvent);
            log.info("Published PAYMENT_FAILED event for order: {}", orderDTO.getOrderId());
        }
    }

    /**
     * Listen for compensation commands from the orchestrator
     * This rolls back the payment by refunding
     */
    @KafkaListener(topics = "payment-compensate", groupId = "payment-service")
    @Transactional
    public void compensatePayment(SagaEvent event) {
        String orderId = event.getOrderId();
        log.info("Compensating payment for order: {}", orderId);

        // Find and refund the payment
        Payment payment = paymentRepository.findByOrderId(orderId)
            .orElseThrow(() -> new RuntimeException("Payment not found for order: " + orderId));

        payment.setStatus("REFUNDED");
        paymentRepository.save(payment);

        log.info("Payment refunded for order: {}", orderId);

        // Optional: Notify orchestrator that compensation is complete
        SagaEvent compensationEvent = new SagaEvent(
            orderId, 
            EventType.COMPENSATE_PAYMENT, 
            null
        );
        kafkaTemplate.send("saga-events", compensationEvent);
    }
}

Step 5: Inventory Service

// Inventory.java (Entity)
package com.example.inventoryservice.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Entity
@Table(name = "inventory")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Inventory {
    @Id
    private String productId;
    private Integer availableQuantity;
    private Integer reservedQuantity;
}
// InventoryRepository.java
package com.example.inventoryservice.repository;

import com.example.inventoryservice.entity.Inventory;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface InventoryRepository extends JpaRepository<Inventory, String> {
}
// InventoryService.java
package com.example.inventoryservice.service;

import com.example.common.dto.OrderDTO;
import com.example.common.event.EventType;
import com.example.common.event.SagaEvent;
import com.example.inventoryservice.entity.Inventory;
import com.example.inventoryservice.repository.InventoryRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Slf4j
public class InventoryService {

    private final InventoryRepository inventoryRepository;
    private final KafkaTemplate<String, SagaEvent> kafkaTemplate;

    /**
     * Listen for inventory commands from the orchestrator
     */
    @KafkaListener(topics = "inventory-commands", groupId = "inventory-service")
    @Transactional
    public void reserveInventory(SagaEvent event) {
        OrderDTO orderDTO = (OrderDTO) event.getPayload();

        try {
            log.info("Reserving inventory for order: {}", orderDTO.getOrderId());

            // Find product inventory
            Inventory inventory = inventoryRepository.findById(orderDTO.getProductId())
                .orElseThrow(() -> new RuntimeException("Product not found: " + orderDTO.getProductId()));

            // Check if sufficient quantity is available
            if (inventory.getAvailableQuantity() >= orderDTO.getQuantity()) {
                // Reserve inventory
                inventory.setAvailableQuantity(
                    inventory.getAvailableQuantity() - orderDTO.getQuantity()
                );
                inventory.setReservedQuantity(
                    inventory.getReservedQuantity() + orderDTO.getQuantity()
                );
                inventoryRepository.save(inventory);

                log.info("Inventory reserved for order: {}", orderDTO.getOrderId());

                // Send SUCCESS response back to orchestrator via saga-events
                SagaEvent responseEvent = new SagaEvent(
                    orderDTO.getOrderId(), 
                    EventType.INVENTORY_RESERVED, 
                    orderDTO
                );
                kafkaTemplate.send("saga-events", responseEvent);
                log.info("Published INVENTORY_RESERVED event for order: {}", orderDTO.getOrderId());

            } else {
                throw new RuntimeException("Insufficient inventory for product: " + orderDTO.getProductId());
            }

        } catch (Exception e) {
            log.error("Inventory reservation failed for order: {}", orderDTO.getOrderId(), e);

            // Send FAILURE response back to orchestrator via saga-events
            SagaEvent failureEvent = new SagaEvent(
                orderDTO.getOrderId(), 
                EventType.INVENTORY_FAILED, 
                orderDTO
            );
            kafkaTemplate.send("saga-events", failureEvent);
            log.info("Published INVENTORY_FAILED event for order: {}", orderDTO.getOrderId());
        }
    }

    /**
     * Listen for compensation commands from the orchestrator
     * This rolls back the inventory reservation
     */
    @KafkaListener(topics = "inventory-compensate", groupId = "inventory-service")
    @Transactional
    public void compensateInventory(SagaEvent event) {
        OrderDTO orderDTO = (OrderDTO) event.getPayload();
        log.info("Compensating inventory for order: {}", orderDTO.getOrderId());

        // Find and release inventory
        Inventory inventory = inventoryRepository.findById(orderDTO.getProductId())
            .orElseThrow(() -> new RuntimeException("Product not found: " + orderDTO.getProductId()));

        inventory.setAvailableQuantity(
            inventory.getAvailableQuantity() + orderDTO.getQuantity()
        );
        inventory.setReservedQuantity(
            inventory.getReservedQuantity() - orderDTO.getQuantity()
        );
        inventoryRepository.save(inventory);

        log.info("Inventory released for order: {}", orderDTO.getOrderId());

        // Optional: Notify orchestrator that compensation is complete
        SagaEvent compensationEvent = new SagaEvent(
            orderDTO.getOrderId(), 
            EventType.COMPENSATE_INVENTORY, 
            null
        );
        kafkaTemplate.send("saga-events", compensationEvent);
    }
}

Step 6: Saga Orchestrator (The Brain)

// SagaOrchestrator.java
package com.example.orchestrator.service;

import com.example.common.dto.OrderDTO;
import com.example.common.dto.OrderStatus;
import com.example.common.event.EventType;
import com.example.common.event.SagaEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

/**
 * The Saga Orchestrator is the brain of the saga pattern.
 * It coordinates all services and handles the workflow logic.
 * 
 * Responsibilities:
 * 1. Listen to all saga events from services
 * 2. Decide the next step based on current state
 * 3. Send commands to appropriate services
 * 4. Trigger compensations on failures
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class SagaOrchestrator {

    private final KafkaTemplate<String, SagaEvent> kafkaTemplate;

    /**
     * Central event handler - listens to all saga events
     * This is the coordination logic
     */
    @KafkaListener(topics = "saga-events", groupId = "saga-orchestrator")
    public void handleSagaEvent(SagaEvent event) {
        log.info("=== Orchestrator received event: {} for order: {} ===", 
                 event.getEventType(), event.getOrderId());

        switch (event.getEventType()) {

            case ORDER_CREATED:
                // Step 1: Order created, now process payment
                log.info("Step 1: Order created, initiating payment...");
                kafkaTemplate.send("payment-commands", event);
                break;

            case PAYMENT_PROCESSED:
                // Step 2: Payment successful, now reserve inventory
                log.info("Step 2: Payment successful, reserving inventory...");
                kafkaTemplate.send("inventory-commands", event);
                break;

            case PAYMENT_FAILED:
                // Payment failed - no compensation needed (nothing to rollback yet)
                log.error("Saga FAILED at payment stage for order: {}", event.getOrderId());
                log.info("No compensation needed - payment was the first step");
                updateOrderStatus(event.getOrderId(), OrderStatus.ORDER_FAILED, 
                                (OrderDTO) event.getPayload());
                break;

            case INVENTORY_RESERVED:
                // Step 3: All steps completed successfully!
                log.info("Step 3: Inventory reserved successfully");
                log.info("βœ… Saga COMPLETED successfully for order: {}", event.getOrderId());
                updateOrderStatus(event.getOrderId(), OrderStatus.ORDER_COMPLETED, 
                                (OrderDTO) event.getPayload());
                break;

            case INVENTORY_FAILED:
                // Inventory failed - need to compensate payment
                log.error("Saga FAILED at inventory stage for order: {}", event.getOrderId());
                log.info("Triggering compensation: Refunding payment...");
                kafkaTemplate.send("payment-compensate", event);
                updateOrderStatus(event.getOrderId(), OrderStatus.ORDER_FAILED, 
                                (OrderDTO) event.getPayload());
                break;

            case COMPENSATE_PAYMENT:
                // Payment compensation completed
                log.info("Payment compensation completed for order: {}", event.getOrderId());
                break;

            case COMPENSATE_INVENTORY:
                // Inventory compensation completed
                log.info("Inventory compensation completed for order: {}", event.getOrderId());
                break;

            default:
                log.warn("Unknown event type: {}", event.getEventType());
        }
    }

    /**
     * Update order status in Order Service
     */
    private void updateOrderStatus(String orderId, OrderStatus status, OrderDTO orderDTO) {
        log.info("Updating order {} status to {}", orderId, status);

        orderDTO.setStatus(status);
        SagaEvent statusEvent = new SagaEvent(orderId, EventType.ORDER_COMPLETED, orderDTO);

        // Send status update to Order Service
        kafkaTemplate.send("order-status-updates", statusEvent);
    }
}

Step 7: Configuration Files

Kafka Configuration (Common for all services)

# application.yml
spring:
  application:
    name: order-service  # Change for each service

  # Kafka Configuration
  kafka:
    bootstrap-servers: localhost:9092
    consumer:
      group-id: ${spring.application.name}
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      properties:
        spring.json.trusted.packages: "*"
        spring.json.type.mapping: sagaEvent:com.example.common.event.SagaEvent
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer

  # Database Configuration
  datasource:
    url: jdbc:h2:mem:orderdb  # Change for each service
    driver-class-name: org.h2.Driver
    username: sa
    password: 

  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true
    properties:
      hibernate:
        format_sql: true

  h2:
    console:
      enabled: true

# Server Port (Different for each service)
server:
  port: 8081  # 8082 for payment, 8083 for inventory, 8084 for orchestrator

Step 8: Testing the Saga

Create Initial Inventory Data

// InventoryDataLoader.java
package com.example.inventoryservice.config;

import com.example.inventoryservice.entity.Inventory;
import com.example.inventoryservice.repository.InventoryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class InventoryDataLoader implements CommandLineRunner {

    private final InventoryRepository inventoryRepository;

    @Override
    public void run(String... args) {
        // Create sample inventory
        Inventory product1 = new Inventory("PROD-001", 100, 0);
        Inventory product2 = new Inventory("PROD-002", 5, 0);

        inventoryRepository.save(product1);
        inventoryRepository.save(product2);

        System.out.println("Sample inventory data loaded");
    }
}

Test Request (Success Scenario)

curl -X POST http://localhost:8081/api/orders \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "USER-001",
    "productId": "PROD-001",
    "quantity": 2,
    "amount": 199.99
  }'

Expected Flow:

1. Order Service: Order created βœ…
2. Payment Service: Payment processed βœ…
3. Inventory Service: Inventory reserved βœ…
4. Orchestrator: Order completed βœ…

Test Request (Failure Scenario)

curl -X POST http://localhost:8081/api/orders \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "USER-002",
    "productId": "PROD-002",
    "quantity": 10,
    "amount": 299.99
  }'

Expected Flow:

1. Order Service: Order created βœ…
2. Payment Service: Payment processed βœ…
3. Inventory Service: Inventory reservation FAILED ❌ (insufficient stock)
4. Orchestrator: Triggering compensation...
5. Payment Service: Payment refunded βœ…
6. Orchestrator: Order marked as FAILED βœ…

How It Works - Complete Walkthrough

Success Flow (Happy Path)

  1. User Creates Order

    • POST request to Order Service

    • Order saved with status PENDING

  2. Order Service β†’ Orchestrator

    • Publishes ORDER_CREATED event to saga-events topic

    • Orchestrator receives and processes

  3. Orchestrator β†’ Payment Service

    • Sends command to payment-commands topic

    • Payment Service listens and processes

  4. Payment Service Processing

    • Processes payment

    • Saves payment record with status COMPLETED

    • Publishes PAYMENT_PROCESSED to saga-events

  5. Orchestrator β†’ Inventory Service

    • Receives PAYMENT_PROCESSED event

    • Sends command to inventory-commands topic

    • Inventory Service listens and processes

  6. Inventory Service Processing

    • Checks available quantity

    • Reserves inventory

    • Publishes INVENTORY_RESERVED to saga-events

  7. Orchestrator Completes Saga

    • Receives INVENTORY_RESERVED event

    • Updates order status to ORDER_COMPLETED

    • Sends status update to Order Service

Failure Flow (Compensating Transactions)

  1. Steps 1-4: Same as success flow

    • Order created βœ…

    • Payment processed βœ…

  2. Inventory Service Fails

    • Insufficient stock detected

    • Publishes INVENTORY_FAILED to saga-events

  3. Orchestrator Triggers Compensation

    • Receives INVENTORY_FAILED event

    • Sends compensation command to payment-compensate topic

  4. Payment Service Compensates

    • Receives compensation command

    • Refunds the payment

    • Updates payment status to REFUNDED

  5. Orchestrator Updates Order

    • Updates order status to ORDER_FAILED

    • Sends status update to Order Service

When to Use Choreography vs Orchestration

Use Choreography When:

  • βœ… Workflow is simple (2-3 steps)

  • βœ… Services need high autonomy

  • βœ… No single point of failure is acceptable

  • βœ… Team prefers event-driven architecture

  • βœ… Services rarely change workflow order

Use Orchestration When:

  • βœ… Workflow is complex (4+ steps)

  • βœ… Need centralized monitoring

  • βœ… Workflow changes frequently

  • βœ… Need clear visibility into saga state

  • βœ… Easier debugging is priority

Real-World Examples

Choreography:

  • User registration (Email β†’ Profile β†’ Welcome)

  • Simple order processing

  • Notification chains

Orchestration:

  • Complex order fulfillment

  • Multi-step booking systems

  • Financial transactions with many validations

Conclusion

Both Choreography and Orchestration patterns have their place in microservices architecture:

  • Choreography is great for simple workflows where service autonomy is key

  • Orchestration excels in complex workflows requiring centralized control

Choose based on your specific needs:

  • Complexity of workflow

  • Team expertise

  • Monitoring requirements

  • Failure handling complexity

The key is understanding both patterns and applying the right one for your use case!


Happy Coding! πŸš€

Have questions or feedback? Feel free to reach out in the comments below.