Implementation of SAGA with AxonFrameWork with SpringBoot Part.1

Reading Time: 6 minutes

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




Leave a Reply