Choreography-Based Saga Architecture and Spring Boot Microservices Implementation Guide

Choreography-Based Saga Architecture:

Here's a detailed walkthrough:


Steps in the Saga

  1. Step 1: Initiating the Saga (POST /orders)

    • A client sends a POST request to the Order Service to create an order.
    • The Order Service processes this request, creates an Order entity (aggregate), and publishes an Order Created event to the Order Events Channel.
  2. Step 2: Order Created Event

    • The Order Created event flows through the Order Events Channel to notify interested services (e.g., Customer Service) about the creation of the order.
    • The event contains details about the order, such as its ID, total amount, and customer ID.
  3. Step 3: Reserve Credit (POST /customer)

    • The Customer Service receives the Order Created event and attempts to reserve the required credit for the order.
    • If the credit limit is sufficient:
      • It updates the Customer entity (aggregate) to reserve the credit.
      • It publishes a Credit Reserved event on the Customer Events Channel.
  4. Step 4: Insufficient Credit

    • If the customer’s credit limit is exceeded:
      • The Customer Service publishes a Credit Limit Exceeded event on the Customer Events Channel.
      • No credit is reserved.
  5. Step 5: Finalizing the Order

    • The Order Service listens for responses on the Customer Events Channel:
      • If Credit Reserved is received, the Order Service approves the order and updates the order state (e.g., approved).
      • If Credit Limit Exceeded is received, the Order Service rejects the order and updates the order state (e.g., rejected).

Key Features of the Architecture

Event Channels

  • Order Events Channel: Handles events related to orders, such as Order Created.
  • Customer Events Channel: Handles customer-related events, such as Credit Reserved and Credit Limit Exceeded.
  • These channels decouple services, enabling asynchronous communication.

Aggregates

  • Aggregates represent the core business logic and state for each service.
  • Order Aggregate: Manages the lifecycle of orders.
  • Customer Aggregate: Manages the customer’s credit state and reservations.

Transactional Guarantees in the Saga

Distributed Transactions

  • Each service (e.g., Order Service, Customer Service) performs local transactions within its boundary.
  • Changes are committed to the respective database independently.

Eventual Consistency

  • While the saga executes, the system might be temporarily inconsistent.
  • Consistency is achieved once the saga is completed, either successfully (order approved) or unsuccessfully (order rejected).

Compensations

  • If the order is rejected, any reserved credit in the Customer Service must be released through a compensating action (e.g., removing the credit reservation).

Advantages of This Approach

  1. Decoupled Services:

    • Services communicate indirectly via events, reducing dependencies and improving scalability.
  2. Asynchronous Processing:

    • Services process events at their own pace, improving system responsiveness.
  3. Fault Tolerance:

    • Failures in one service (e.g., credit reservation) do not directly impact others, and compensating actions can handle partial failures.

Challenges

  1. Event Tracking:

    • Monitoring and debugging event flows across multiple services can be challenging without proper tooling.
  2. Race Conditions:

    • Services must handle potential race conditions, such as duplicate or out-of-order events.
  3. Error Handling:

    • Designing compensating transactions for every failure scenario requires careful planning.

Here’s an example of implementing the above choreography-based saga with two Spring Boot microservices (Order Service and Customer Service) from scratch. This will include:

  • REST APIs for creating orders and reserving credit.
  • Event-driven communication using Kafka (or RabbitMQ).
  • Basic implementation of choreography logic.

1. Setup Project Structure

You’ll need two Spring Boot projects:

  • Order Service: Manages order creation and state.
  • Customer Service: Manages customer credit and reservations.

2. Order Service Implementation

Add Dependencies

Add the following dependencies to the pom.xml:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka</artifactId>
    </dependency>
</dependencies>

Application Properties

Configure Kafka settings in application.properties:

spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=order-group

Order Entity

public class Order {
    private String orderId;
    private String customerId;
    private Double total;
    private String state; // "PLACED", "APPROVED", "REJECTED"

    // Getters and Setters
}

Order Controller

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

    private final KafkaTemplate<String, String> kafkaTemplate;

    public OrderController(KafkaTemplate<String, String> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    @PostMapping
    public ResponseEntity<String> placeOrder(@RequestBody Order order) {
        // Set initial state
        order.setState("PLACED");

        // Publish Order Created event
        kafkaTemplate.send("order-events", new ObjectMapper().writeValueAsString(order));

        return ResponseEntity.ok("Order Placed");
    }
}

Kafka Listener for Customer Events

@Service
public class OrderEventListener {

    @KafkaListener(topics = "customer-events", groupId = "order-group")
    public void handleCustomerEvents(String message) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode event = objectMapper.readTree(message);

        String eventType = event.get("type").asText();
        String orderId = event.get("orderId").asText();

        if ("CREDIT_RESERVED".equals(eventType)) {
            System.out.println("Order Approved: " + orderId);
            // Update order state to APPROVED
        } else if ("CREDIT_LIMIT_EXCEEDED".equals(eventType)) {
            System.out.println("Order Rejected: " + orderId);
            // Update order state to REJECTED
        }
    }
}

3. Customer Service Implementation

Add Dependencies

Add the same dependencies as the Order Service.


Application Properties

Configure Kafka settings in application.properties:

spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=customer-group

Customer Entity

public class Customer {
    private String customerId;
    private Double creditLimit;
    private Double reservedCredit;

    // Getters and Setters
}

Customer Service Logic

@Service
public class CustomerService {

    private final KafkaTemplate<String, String> kafkaTemplate;

    public CustomerService(KafkaTemplate<String, String> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public void processOrder(JsonNode order) {
        String customerId = order.get("customerId").asText();
        Double total = order.get("total").asDouble();

        // Retrieve customer from DB (mocked here)
        Customer customer = getCustomerById(customerId);

        if (customer.getCreditLimit() >= total) {
            customer.setReservedCredit(customer.getReservedCredit() + total);

            // Publish Credit Reserved event
            ObjectNode event = new ObjectMapper().createObjectNode();
            event.put("type", "CREDIT_RESERVED");
            event.put("orderId", order.get("orderId").asText());
            kafkaTemplate.send("customer-events", event.toString());
        } else {
            // Publish Credit Limit Exceeded event
            ObjectNode event = new ObjectMapper().createObjectNode();
            event.put("type", "CREDIT_LIMIT_EXCEEDED");
            event.put("orderId", order.get("orderId").asText());
            kafkaTemplate.send("customer-events", event.toString());
        }
    }

    private Customer getCustomerById(String customerId) {
        // Mock customer data
        return new Customer(customerId, 1000.0, 0.0);
    }
}

Kafka Listener for Order Events

@Service
public class CustomerEventListener {

    private final CustomerService customerService;

    public CustomerEventListener(CustomerService customerService) {
        this.customerService = customerService;
    }

    @KafkaListener(topics = "order-events", groupId = "customer-group")
    public void handleOrderEvents(String message) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode order = objectMapper.readTree(message);

        customerService.processOrder(order);
    }
}

4. Kafka Topics

Create Kafka topics:

  • order-events
  • customer-events
kafka-topics --create --topic order-events --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1
kafka-topics --create --topic customer-events --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1

5. Testing the Saga

  1. Run Kafka:
    • Start a Kafka broker and Zookeeper instance.
  2. Start the Services:
    • Run both the Order Service and Customer Service.
  3. Place an Order:
    • Use Postman or curl to send a POST request to /orders.
  4. Monitor Events:
    • Observe the logs to see the events being processed.

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.

Comments

Popular posts from this blog

Spring Boot OpenAI Integration: Step-by-Step Guide

Orchestration-Based Saga Architecture and Spring Boot Microservices Implementation Guide

Spring Boot 3 + Angular 15 + Material - Full Stack CRUD Application Example