Saga Design Pattern in Spring Boot Microservices

In this tutorial, we will create a microservices architecture using Spring Boot that implements the Saga pattern for distributed transactions. We will use the Event-Driven approach with Kafka to coordinate transactions between microservices.

Introduction to Saga Pattern

The Saga Pattern is a microservices architectural pattern to manage data consistency across multiple microservices. Instead of a distributed transaction, a saga is a sequence of local transactions. Each local transaction updates the data within a single microservice and publishes an event or message to trigger the next transaction step in the saga. If one of the steps fails, the saga executes a series of compensating transactions to undo the changes made by the previous steps.

Prerequisites

  • JDK 17 or later
  • Maven or Gradle
  • Docker
  • Docker Compose
  • Apache Kafka
  • IDE (IntelliJ IDEA, Eclipse, etc.)

Step 1: Set Up Apache Kafka with Docker Compose

Create a docker-compose.yml file to set up Kafka and Zookeeper.

version: '3.7'
services:
  zookeeper:
    image: wurstmeister/zookeeper:3.4.6
    ports:
      - "2181:2181"

  kafka:
    image: wurstmeister/kafka:2.12-2.2.1
    ports:
      - "9092:9092"
    environment:
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_CREATE_TOPICS: "order-events:1:1, payment-events:1:1"

Start Kafka and Zookeeper:

docker-compose up -d

Step 2: Create the order-service

2.1 Create the Project

Use Spring Initializr to create a new project with the following dependencies:

  • Spring Web
  • Spring Boot Actuator
  • Spring Kafka
  • Spring Data JPA
  • H2 Database

2.2 Configure application.properties

Set up the application properties for order-service.

server.port=8081
spring.application.name=order-service

# H2 Database settings
spring.datasource.url=jdbc:h2:mem:orderdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.h2.console.enabled=true
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

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

2.3 Create Order Model

Create a simple Order model to represent the order data.

package com.example.orderservice;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class Order {
    @Id
    private String id;
    private String product;
    private double price;
    private String status;

    // Getters and setters
}

2.4 Create Order Repository

Create a repository interface to manage Order entities.

package com.example.orderservice;

import org.springframework.data.jpa.repository.JpaRepository;

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

2.5 Create Order Service

Create a service to handle order-related business logic.

package com.example.orderservice;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private KafkaTemplate<String, OrderEvent> kafkaTemplate;

    public Order createOrder(Order order) {
        order.setStatus("CREATED");
        orderRepository.save(order);
        kafkaTemplate.send("order-events", new OrderEvent(order.getId(), "ORDER_CREATED"));
        return order;
    }

    public void updateOrderStatus(String orderId, String status) {
        Order order = orderRepository.findById(orderId).orElseThrow(() -> new RuntimeException("Order not found"));
        order.setStatus(status);
        orderRepository.save(order);
    }
}

2.6 Create Order Controller

Create a controller to handle HTTP requests.

package com.example.orderservice;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

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

    @Autowired
    private OrderService orderService;

    @PostMapping
    public Order createOrder(@RequestBody Order order) {
        return orderService.createOrder(order);
    }

    @PutMapping("/{orderId}")
    public void updateOrderStatus(@PathVariable String orderId, @RequestParam String status) {
        orderService.updateOrderStatus(orderId, status);
    }
}

2.7 Create Order Event

Create a class to represent order events.

package com.example.orderservice;

public class OrderEvent {
    private String orderId;
    private String eventType;

    public OrderEvent(String orderId, String eventType) {
        this.orderId = orderId;
        this.eventType = eventType;
    }

    // Getters and setters
}

Step 3: Create the payment-service

3.1 Create the Project

Use Spring Initializr to create a new project with the following dependencies:

  • Spring Web
  • Spring Boot Actuator
  • Spring Kafka
  • Spring Data JPA
  • H2 Database

3.2 Configure application.properties

Set up the application properties for payment-service.

server.port=8082
spring.application.name=payment-service

# H2 Database settings
spring.datasource.url=jdbc:h2:mem:paymentdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.h2.console.enabled=true
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

# Kafka settings
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=payment-group

3.3 Create Payment Model

Create a simple Payment model to represent the payment data.

package com.example.paymentservice;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class Payment {
    @Id
    private String id;
    private String orderId;
    private double amount;
    private String status;

    // Getters and setters
}

3.4 Create Payment Repository

Create a repository interface to manage Payment entities.

package com.example.paymentservice;

import org.springframework.data.jpa.repository.JpaRepository;

public interface PaymentRepository extends JpaRepository<Payment, String> {
}

3.5 Create Payment Service

Create a service to handle payment-related business logic.

package com.example.paymentservice;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    @Autowired
    private PaymentRepository paymentRepository;

    @Autowired
    private KafkaTemplate<String, PaymentEvent> kafkaTemplate;

    public void processPayment(OrderEvent orderEvent) {
        Payment payment = new Payment();
        payment.setOrderId(orderEvent.getOrderId());
        payment.setAmount(100); // Assume a fixed amount for simplicity
        payment.setStatus("COMPLETED");
        paymentRepository.save(payment);
        kafkaTemplate.send("payment-events", new PaymentEvent(payment.getOrderId(), "PAYMENT_COMPLETED"));
    }
}

3.6 Create Payment Listener

Create a listener to handle order events.

package com.example.paymentservice;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;

@Service
public class OrderEventListener {

    @Autowired
    private PaymentService paymentService;

    @KafkaListener(topics = "order-events")
    public void handleOrderEvent(OrderEvent orderEvent) {
        if ("ORDER_CREATED".equals(orderEvent.getEventType())) {
            paymentService.processPayment(orderEvent);
        }
    }
}

3.7 Create Payment Event

Create a class to represent payment events.

package com.example.paymentservice;

public class PaymentEvent {
    private String orderId;
    private String eventType;

    public PaymentEvent(String orderId, String eventType) {
        this.orderId = orderId;
        this.eventType = eventType;
    }

    // Getters and setters
}

Step 4: Handle Compensation in order-service

Update order-service to handle compensation logic.

4.1 Update Order Service

Update the OrderService to handle payment failure.

package com.example.orderservice;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private KafkaTemplate<String, OrderEvent> kafkaTemplate;

    public Order createOrder(Order order) {
        order.setStatus("CREATED");
        orderRepository.save(order);
        kafkaTemplate.send("order-events", new OrderEvent(order.getId(), "ORDER_CREATED"));
        return order;
    }

    public void updateOrderStatus(String orderId, String status) {
        Order order = orderRepository.findById(orderId).orElseThrow(() -> new RuntimeException("Order not found"));
        order.setStatus(status);
        orderRepository.save(order);
    }

    @KafkaListener(topics = "payment-events")
    public void handlePaymentEvent(PaymentEvent paymentEvent) {
        if ("PAYMENT_COMPLETED".equals(paymentEvent.getEventType())) {
            updateOrderStatus(paymentEvent.getOrderId(), "COM

PLETED");
        } else if ("PAYMENT_FAILED".equals(paymentEvent.getEventType())) {
            updateOrderStatus(paymentEvent.getOrderId(), "FAILED");
        }
    }
}

4.2 Create Payment Event Class

Create a PaymentEvent class to represent payment events.

package com.example.orderservice;

public class PaymentEvent {
    private String orderId;
    private String eventType;

    public PaymentEvent(String orderId, String eventType) {
        this.orderId = orderId;
        this.eventType = eventType;
    }

    // Getters and setters
}

Step 5: Run the Microservices

  1. Start Kafka and Zookeeper: Ensure Kafka and Zookeeper are running using the Docker command mentioned above.
  2. Start order-service: Run the OrderServiceApplication class.
  3. Start payment-service: Run the PaymentServiceApplication class.

Step 6: Test the Saga Pattern

  1. Open your browser or use a tool like Postman to create an order:

    • URL: http://localhost:8081/orders
    • Method: POST
    • Body:
      {
        "id": "1",
        "product": "Sample Product",
        "price": 99.99
      }
      
  2. Verify the order is created and a payment event is generated. Check the logs of order-service and payment-service to see the events.

Conclusion

You have successfully set up a microservices architecture using Spring Boot and implemented the Saga pattern for distributed transactions. This setup allows you to build scalable and maintainable microservices that handle data consistency across services using event-driven communication. This example can be expanded to include more complex business logic and additional microservices.


Comments