Introduction
In Part 1 (this post), we will understand what Saga Pattern really is. We will start building the sample application using Saga Pattern.
Continuing in Part 2, we will continue with our implementation.
Saga Pattern proposes implementing distributed transactions in the form of Sagas. Whenever a local transaction occurs, it publishes a message or an event. Such an event is responsible for triggering the next transaction in the Saga. If a single transaction in the Saga sequence fails the Saga executes a series of compensating transactions. These transactions undo the changes made by the preceding transactions. There are two types of SAGA i.e Choreography-Based Saga and Orchestration-Based Saga. We will be using Orchestration-Based Implementation.
Some of the tools and frameworks we would be using are as follows:
- Axon Framework
- Axon Server
- SpringBoot
- H2 Database
Implementation:
As we discussed above we are going to use Orchestration-Based Implementation. A typical example of this approach is shown in the below illustration.

High-Level Component of the project:
Order Service – This service exposes APIs that help create an Order in the system. Also, the service manages the Order Aggregate. However, the Order Service also acts as the home for the actual Order Processing Saga implementation.
Payment Service – The Payment-Service acts upon the Create Payment completed Command issued by the Order Processing Saga. Once it finishes its job, it publishes an event that pushes the Saga forward
Shipping Service– This service takes care of creating an Order shipment command issued by the Order Processing Saga. Once it does its job, it also publishes an event that pushes the Saga forward
Common Service- This is not service it acts as the integration glue between various services that form a part of the Saga. It has common commands and events which will be used by various services.
Axon Server– Axon Server is part of the Axon Platform. We will use Axon Framework to manage our Aggregates such as Order, Payment, and Shipping.
Overall Application Structure:



Order Service Implementation:
Order Service Structure:



Order Aggregate
Order Aggregate is one of the most important pieces of our Saga Pattern implementation. It forms the base around which the Order Management Saga will work. The major things to note here are the Axon-specific annotations @Aggregate and @AggregateIdentifier. These annotations allow Axon Framework to manage the Order Aggregate instances.
package com.knoldus.orderservice.command.aggregate;
import com.knoldus.commonservice.commands.CancelOrderCommand;
import com.knoldus.commonservice.commands.CompleteOrderCommand;
import com.knoldus.commonservice.events.OrderCancelledEvent;
import com.knoldus.commonservice.events.OrderCompletedEvent;
import com.knoldus.orderservice.command.command.CreateOrderCommand;
import com.knoldus.orderservice.command.events.OrderCreatedEvent;
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.eventsourcing.EventSourcingHandler;
import org.axonframework.modelling.command.AggregateIdentifier;
import org.axonframework.modelling.command.AggregateLifecycle;
import org.axonframework.spring.stereotype.Aggregate;
import org.springframework.beans.BeanUtils;
@Aggregate
public class OrderAggregate {
@AggregateIdentifier
private String orderId;
private String productId;
private String userId;
private String addressId;
private Integer quantity;
private String orderStatus;
public OrderAggregate() {
}
@CommandHandler
public OrderAggregate(CreateOrderCommand createOrderCommand) {
//Validate The Command
OrderCreatedEvent orderCreatedEvent
= new OrderCreatedEvent();
BeanUtils.copyProperties(createOrderCommand,
orderCreatedEvent);
AggregateLifecycle.apply(orderCreatedEvent);
}
@EventSourcingHandler
public void on(OrderCreatedEvent event) {
this.orderStatus = event.getOrderStatus();
this.userId = event.getUserId();
this.orderId = event.getOrderId();
this.quantity = event.getQuantity();
this.productId = event.getProductId();
this.addressId = event.getAddressId();
}
@CommandHandler
public void handle(CompleteOrderCommand completeOrderCommand) {
//Validate The Command
// Publish Order Completed Event
OrderCompletedEvent orderCompletedEvent
= OrderCompletedEvent.builder()
.orderStatus(completeOrderCommand.getOrderStatus())
.orderId(completeOrderCommand.getOrderId())
.build();
AggregateLifecycle.apply(orderCompletedEvent);
}
@EventSourcingHandler
public void on(OrderCompletedEvent event) {
this.orderStatus = event.getOrderStatus();
}
@CommandHandler
public void handle(CancelOrderCommand cancelOrderCommand) {
OrderCancelledEvent orderCancelledEvent
= new OrderCancelledEvent();
BeanUtils.copyProperties(cancelOrderCommand,
orderCancelledEvent);
AggregateLifecycle.apply(orderCancelledEvent);
}
@EventSourcingHandler
public void on(OrderCancelledEvent event) {
this.orderStatus = event.getOrderStatus();
}
}
Order Controller
The Order Controller class is the place where we create our API end-points. At this point, for the purposes of our demo, we have only one end-point.
package com.knoldus.orderservice.command.controller;
import com.knoldus.orderservice.command.command.CreateOrderCommand;
import com.knoldus.orderservice.command.model.OrderRestModel;
import org.axonframework.commandhandling.gateway.CommandGateway;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
@RestController
@RequestMapping("/orders")
public class OrderCommandController {
private CommandGateway commandGateway;
public OrderCommandController(CommandGateway commandGateway) {
this.commandGateway = commandGateway;
}
@PostMapping
public String createOrder(@RequestBody OrderRestModel orderRestModel) {
String orderId = UUID.randomUUID().toString();
CreateOrderCommand createOrderCommand
= CreateOrderCommand.builder()
.orderId(orderId)
.addressId(orderRestModel.getAddressId())
.productId(orderRestModel.getProductId())
.quantity(orderRestModel.getQuantity())
.orderStatus("CREATED")
.build();
commandGateway.sendAndWait(createOrderCommand);
return "Order Created";
}
}
Commands
The Create Order Command is triggered when a new Order is created in our application. This command is handled by the Order Aggregate.
package com.knoldus.orderservice.command.command;
import lombok.Builder;
import lombok.Data;
import org.axonframework.modelling.command.TargetAggregateIdentifier;
@Data
@Builder
public class CreateOrderCommand {
@TargetAggregateIdentifier
private String orderId;
private String productId;
private String userId;
private String addressId;
private Integer quantity;
private String orderStatus;
}
Order Created Event :
package com.knoldus.orderservice.command.events;
import lombok.Data;
@Data
public class OrderCreatedEvent {
private String orderId;
private String productId;
private String userId;
private String addressId;
private Integer quantity;
private String orderStatus;
}
Order Event Handler:
This is class is going to handle the event from different events.
package com.knoldus.orderservice.command.events;
import com.knoldus.commonservice.events.OrderCancelledEvent;
import com.knoldus.commonservice.events.OrderCompletedEvent;
import com.knoldus.orderservice.command.data.Order;
import com.knoldus.orderservice.command.data.OrderRepository;
import org.axonframework.eventhandling.EventHandler;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
@Component
public class OrderEventsHandler {
private OrderRepository orderRepository;
public OrderEventsHandler(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@EventHandler
public void on(OrderCreatedEvent event) {
Order order = new Order();
BeanUtils.copyProperties(event,order);
orderRepository.save(order);
}
@EventHandler
public void on(OrderCompletedEvent event) {
Order order = orderRepository.findById(event.getOrderId()).get();
order.setOrderStatus(event.getOrderStatus());
orderRepository.save(order);
}
@EventHandler
public void on(OrderCancelledEvent event) {
Order order= orderRepository.findById(event.getOrderId()).get();
order.setOrderStatus(event.getOrderStatus());
orderRepository.save(order);
}
}
Data Members:
In this, we are going to store the details in a local database
package com.knoldus.orderservice.command.data;
import org.springframework.data.jpa.repository.JpaRepository;
public interface OrderRepository extends JpaRepository<Order, String> {
}
Order Class:
package com.knoldus.orderservice.command.data;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Data
@Entity
@Table(name = "orders")
public class Order {
@Id
private String orderId;
private String productId;
private String userId;
private String addressId;
private Integer quantity;
private String orderStatus;
}
Order Rest Model :
package com.knoldus.orderservice.command.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderRestModel {
private String productId;
private String userId;
private String addressId;
private Integer quantity;
}
Order Processing Saga:
The heart of the Saga Pattern implementation is the Order Management Saga. In a nutshell, this is also a typical Java class that describes the various handlers for the individual Saga steps.
The individual Saga steps can be managed in a declarative manner. In other words, this makes it extremely easy for a developer to understand the flow of the Saga at a single glance.
package com.knoldus.orderservice.command.saga;
import com.knoldus.commonservice.commands.*;
import com.knoldus.commonservice.events.*;
import com.knoldus.commonservice.model.CardDetails;
import com.knoldus.commonservice.model.User;
import com.knoldus.commonservice.queries.GetUserPaymentDetailsQuery;
import com.knoldus.orderservice.command.events.OrderCreatedEvent;
import lombok.extern.slf4j.Slf4j;
import org.axonframework.commandhandling.gateway.CommandGateway;
import org.axonframework.messaging.responsetypes.ResponseTypes;
import org.axonframework.modelling.saga.EndSaga;
import org.axonframework.modelling.saga.SagaEventHandler;
import org.axonframework.modelling.saga.StartSaga;
import org.axonframework.queryhandling.QueryGateway;
import org.axonframework.spring.stereotype.Saga;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.UUID;
@Saga
@Slf4j
public class OrderProcessingSaga {
@Autowired
private transient CommandGateway commandGateway;
public OrderProcessingSaga() {
}
@StartSaga
@SagaEventHandler(associationProperty = "orderId")
private void handle(OrderCreatedEvent event) {
log.info("OrderCreatedEvent in Saga for Order Id : {}",
event.getOrderId());
// we can use QueryGateway to get the details from other service.
// Added the detail we can integrate other service for fetching the detail
CardDetails cardDetails= CardDetails.builder()
.name("Anshuman")
.validUntilYear(2022)
.validUntilMonth(01)
.cardNumber("123456789")
.cvv(111)
.build();
User user = User.builder().cardDetails(cardDetails).build();
ValidatePaymentCommand validatePaymentCommand
= ValidatePaymentCommand
.builder()
.cardDetails(user.getCardDetails())
.orderId(event.getOrderId())
.paymentId(UUID.randomUUID().toString())
.build();
commandGateway.sendAndWait(validatePaymentCommand);
}
private void cancelOrderCommand(String orderId) {
CancelOrderCommand cancelOrderCommand
= new CancelOrderCommand(orderId);
commandGateway.send(cancelOrderCommand);
}
@SagaEventHandler(associationProperty = "orderId")
private void handle(PaymentProcessedEvent event) {
log.info("PaymentProcessedEvent in Saga for Order Id : {}",
event.getOrderId());
try {
//uncomment this to see the the flow of cancelled order
/*if(true)
throw new Exception();
*/
ShipOrderCommand shipOrderCommand
= ShipOrderCommand
.builder()
.shipmentId(UUID.randomUUID().toString())
.orderId(event.getOrderId())
.build();
commandGateway.send(shipOrderCommand);
} catch (Exception e) {
log.error(e.getMessage());
// Start the compensating transaction
cancelPaymentCommand(event);
}
}
private void cancelPaymentCommand(PaymentProcessedEvent event) {
CancelPaymentCommand cancelPaymentCommand
= new CancelPaymentCommand(
event.getPaymentId(), event.getOrderId()
);
commandGateway.send(cancelPaymentCommand);
}
@SagaEventHandler(associationProperty = "orderId")
public void handle(OrderShippedEvent event) {
log.info("OrderShippedEvent in Saga for Order Id : {}",
event.getOrderId());
CompleteOrderCommand completeOrderCommand
= CompleteOrderCommand.builder()
.orderId(event.getOrderId())
.orderStatus("APPROVED")
.build();
commandGateway.send(completeOrderCommand);
}
@SagaEventHandler(associationProperty = "orderId")
@EndSaga
public void handle(OrderCompletedEvent event) {
log.info("OrderCompletedEvent in Saga for Order Id : {}",
event.getOrderId());
}
@SagaEventHandler(associationProperty = "orderId")
@EndSaga
public void handle(OrderCancelledEvent event) {
log.info("OrderCancelledEvent in Saga for Order Id : {}",
event.getOrderId());
}
@SagaEventHandler(associationProperty = "orderId")
public void handle(PaymentCancelledEvent event) {
log.info("PaymentCancelledEvent in Saga for Order Id : {}",
event.getOrderId());
cancelOrderCommand(event.getOrderId());
}
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.knoldus</groupId>
<artifactId>order-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>PaymentService</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.axonframework/axon-spring-boot-starter -->
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-spring-boot-starter</artifactId>
<version>4.5.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
<dependency>
<groupId>com.knoldus</groupId>
<artifactId>common-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.properties
server.port = 9091
spring.datasource.url=jdbc:h2:file:~/data/orderDB
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto =update
spring.h2.console.settings.web-allow-others=true