Understanding the Saga Pattern in Spring Boot: A Complete Guide
π 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:
No Distributed ACID Transactions: Each microservice has its own database, making traditional two-phase commit (2PC) protocols impractical or impossible.
Data Consistency: Maintaining consistency across multiple services without a distributed transaction manager is complex.
Service Independence: Services need to remain loosely coupled and independent, which conflicts with tightly coordinated transactions.
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:
Order Service: Creates an order
Payment Service: Processes payment
Inventory Service: Reserves inventory
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 Name | Publisher | Listener(s) | Purpose |
order-events | Order Service | Payment Service | Notify when order is created |
payment-events | Payment Service | Inventory Service, Order Service | Notify payment status |
inventory-events | Inventory Service | Order Service, Payment Service | Notify 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)
User Creates Order
POST request to Order Service
Order saved with status
PENDING
Order Service β Orchestrator
Publishes
ORDER_CREATEDevent tosaga-eventstopicOrchestrator receives and processes
Orchestrator β Payment Service
Sends command to
payment-commandstopicPayment Service listens and processes
Payment Service Processing
Processes payment
Saves payment record with status
COMPLETEDPublishes
PAYMENT_PROCESSEDtosaga-events
Orchestrator β Inventory Service
Receives
PAYMENT_PROCESSEDeventSends command to
inventory-commandstopicInventory Service listens and processes
Inventory Service Processing
Checks available quantity
Reserves inventory
Publishes
INVENTORY_RESERVEDtosaga-events
Orchestrator Completes Saga
Receives
INVENTORY_RESERVEDeventUpdates order status to
ORDER_COMPLETEDSends status update to Order Service
Failure Flow (Compensating Transactions)
Steps 1-4: Same as success flow
Order created β
Payment processed β
Inventory Service Fails
Insufficient stock detected
Publishes
INVENTORY_FAILEDtosaga-events
Orchestrator Triggers Compensation
Receives
INVENTORY_FAILEDeventSends compensation command to
payment-compensatetopic
Payment Service Compensates
Receives compensation command
Refunds the payment
Updates payment status to
REFUNDED
Orchestrator Updates Order
Updates order status to
ORDER_FAILEDSends 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.