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
- Start Kafka and Zookeeper: Ensure Kafka and Zookeeper are running using the Docker command mentioned above.
- Start
order-service
: Run theOrderServiceApplication
class. - Start
payment-service
: Run thePaymentServiceApplication
class.
Step 6: Test the Saga Pattern
-
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 }
- URL:
-
Verify the order is created and a payment event is generated. Check the logs of
order-service
andpayment-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
Post a Comment