Orchestration-Based Saga Architecture and Spring Boot Microservices Implementation Guide


The image illustrates an orchestration-based saga pattern for handling distributed transactions across two microservices: Order Service and Customer Service. This approach uses a central orchestrator (the saga) to coordinate the interactions between services and ensure consistency. Here’s an end-to-end explanation of the flow:


1. Order Creation Request

  • A client sends an HTTP POST /orders request to the Order Controller in the Order Service.
  • The Order Controller is responsible for receiving the request and delegating it to the Order Service.

2. Order Creation in the Saga

  • The Order Service invokes the create() method in the Create Order Saga.
  • The Create Order Saga is the orchestrator responsible for coordinating the steps involved in completing the transaction.

3. Order Aggregate Creation

  • The Create Order Saga initiates the creation of the Order aggregate in the Order Service.
  • The Order aggregate represents the state and business logic of the order.

4. Reserve Credit Command

  • After creating the Order aggregate, the Create Order Saga sends a Reserve Credit Command to the Customer Service through a message broker.
  • The message broker decouples the communication between the services, ensuring reliable message delivery.
    • The Customer Service Command Channel in the broker is responsible for routing the Reserve Credit Command to the Customer Service.

5. Handling the Reserve Credit Command

  • In the Customer Service, a Command Handler listens for incoming commands from the message broker.
  • When the Reserve Credit Command is received, the handler invokes the reserve() method in the Customer Service.

6. Customer Aggregate Update

  • The reserve() method interacts with the Customer aggregate to reserve the required credit for the order.
  • The Customer aggregate ensures that the customer has sufficient credit to reserve.

7. Reserve Credit Response

  • Once the credit reservation is completed (or fails), the Customer Service sends a Reserve Credit Response back to the Create Order Saga via the Create Order Saga Reply Channel in the message broker.

8. Order Approval

  • If the Reserve Credit Response indicates success, the Create Order Saga invokes the approve() method on the Order aggregate in the Order Service.
  • The Order aggregate updates its state to reflect that the order has been approved and is ready for further processing.

Key Components and Interactions

  1. Order Service:

    • Handles the initial order creation request and coordinates the saga.
    • Maintains the Order aggregate.
  2. Customer Service:

    • Responsible for reserving credit for the customer.
    • Maintains the Customer aggregate.
  3. Create Order Saga:

    • Acts as the orchestrator for the entire transaction.
    • Coordinates the steps and handles responses to ensure eventual consistency.
  4. Message Broker:

    • Facilitates asynchronous communication between services.
    • Decouples the services, enabling scalability and fault tolerance.

Error Handling (Implied)

  • If any step fails (e.g., credit reservation fails), the Create Order Saga can trigger compensating actions (e.g., cancel the order or roll back changes).
  • The orchestration ensures that the system maintains consistency even in the event of partial failures.

This design ensures:

  • Scalability: Services communicate asynchronously, allowing independent scaling.
  • Resilience: Decoupling through the message broker makes the system tolerant to failures in individual services.
  • Consistency: The saga orchestrates the distributed transaction to maintain data integrity.


Below is an example of implementing the orchestration-based saga with two Spring Boot microservices (Order Service and Customer Service) based on the architecture provided. The communication is event-driven using Kafka.


1. Setup

Dependencies

Add the following dependencies to both services' pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

2. Kafka Configuration

In both services, configure Kafka:

application.yml:

spring:
  kafka:
    bootstrap-servers: localhost:9092
    consumer:
      group-id: order-group
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    consumer:
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer

3. Order Service Implementation

Entities

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String status; // PENDING, APPROVED, REJECTED
    private Double amount;

    // Getters and setters
}

Repository

public interface OrderRepository extends JpaRepository<Order, Long> {}

Service

@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    public Order createOrder(Double amount) {
        Order order = new Order();
        order.setAmount(amount);
        order.setStatus("PENDING");
        orderRepository.save(order);

        // Send Reserve Credit Command
        kafkaTemplate.send("reserve-credit", String.valueOf(order.getId()));

        return order;
    }

    public void approveOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setStatus("APPROVED");
        orderRepository.save(order);
    }

    public void rejectOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setStatus("REJECTED");
        orderRepository.save(order);
    }
}

Controller

@RestController
@RequestMapping("/orders")
public class OrderController {
    @Autowired
    private OrderService orderService;

    @PostMapping
    public ResponseEntity<Order> createOrder(@RequestParam Double amount) {
        Order order = orderService.createOrder(amount);
        return ResponseEntity.ok(order);
    }
}

Kafka Listener

@Service
public class OrderEventListener {
    @Autowired
    private OrderService orderService;

    @KafkaListener(topics = "reserve-credit-response", groupId = "order-group")
    public void handleReserveCreditResponse(String message) {
        String[] data = message.split(":");
        Long orderId = Long.parseLong(data[0]);
        String status = data[1];

        if ("APPROVED".equals(status)) {
            orderService.approveOrder(orderId);
        } else {
            orderService.rejectOrder(orderId);
        }
    }
}

4. Customer Service Implementation

Entities

@Entity
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private Double creditLimit;
    private Double creditReserved;

    // Getters and setters
}

Repository

public interface CustomerRepository extends JpaRepository<Customer, Long> {}

Service

@Service
public class CustomerService {
    @Autowired
    private CustomerRepository customerRepository;

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    public void reserveCredit(Long orderId, Double amount) {
        Customer customer = customerRepository.findAll().get(0); // Simplified for example
        String response;

        if (customer.getCreditLimit() - customer.getCreditReserved() >= amount) {
            customer.setCreditReserved(customer.getCreditReserved() + amount);
            customerRepository.save(customer);
            response = orderId + ":APPROVED";
        } else {
            response = orderId + ":REJECTED";
        }

        // Send response
        kafkaTemplate.send("reserve-credit-response", response);
    }
}

Kafka Listener

@Service
public class CustomerEventListener {
    @Autowired
    private CustomerService customerService;

    @KafkaListener(topics = "reserve-credit", groupId = "customer-group")
    public void handleReserveCredit(String orderId) {
        // Simplified logic with a hardcoded amount for example
        customerService.reserveCredit(Long.parseLong(orderId), 100.0);
    }
}

5. Testing

  1. Start Kafka on localhost:9092.
  2. Run the Order Service and Customer Service.
  3. Use POST /orders with an amount to create an order.
  4. Observe the event-driven communication:
    • Order Service sends a "Reserve Credit" command to Kafka.
    • Customer Service processes the command and responds with success/failure.
    • Order Service updates the order's status based on the response.

Buy Now – Unlock Your Microservices Mastery for Only $9!

Get your copy now for just $9! and start building resilient and scalable microservices with the help of Microservices with Spring Boot 3 and Spring Cloud.

Popular posts from this blog

Learn Java 8 streams with an example - print odd/even numbers from Array and List

Java Stream API - How to convert List of objects to another List of objects using Java streams?

Registration and Login with Spring Boot + Spring Security + Thymeleaf

Java, Spring Boot Mini Project - Library Management System - Download

ReactJS, Spring Boot JWT Authentication Example

Top 5 Java ORM tools - 2024

Java - Blowfish Encryption and decryption Example

Spring boot video streaming example-HTML5

Google Cloud Storage + Spring Boot - File Upload, Download, and Delete