Implementation of SAGA with Axon FrameWork with SpringBoot Part – 2

Table of contents
Reading Time: 5 minutes

In Part 1 (this post), we will continue with our implementation and connect our services to the Axon Server.

Common Service Implementation:

Structure of Common Service:

CancelOrderCommand Class:

package com.knoldus.commonservice.commands;

import lombok.Value;

import org.axonframework.modelling.command.TargetAggregateIdentifier;

@Value

public class CancelOrderCommand {

    @TargetAggregateIdentifier

    private String orderId;

    private String orderStatus = "CANCELLED";
}

CancelPaymentCommand class :

package com.knoldus.commonservice.commands;

import lombok.Value;

import org.axonframework.modelling.command.TargetAggregateIdentifier;

@Value

public class CancelPaymentCommand {

    @TargetAggregateIdentifier

    private String paymentId;

    private String orderId;

    private String paymentStatus = "CANCELLED";
}

CompleteOrderCommand class:

package com.knoldus.commonservice.commands;

import lombok.Builder;

import lombok.Data;

import org.axonframework.modelling.command.TargetAggregateIdentifier;

@Data

@Builder

public class CompleteOrderCommand {

    @TargetAggregateIdentifier

    private String orderId;

    private String orderStatus;
}

ShipOrderCommand :
package com.knoldus.commonservice.commands;

import lombok.Builder;

import lombok.Data;

import org.axonframework.modelling.command.TargetAggregateIdentifier;

@Data

@Builder

public class ShipOrderCommand {

    @TargetAggregateIdentifier

    private String shipmentId;

    private String orderId;
}

ValidatePaymentCommand :

package com.knoldus.commonservice.commands;

import com.knoldus.commonservice.model.CardDetails;

import lombok.Builder;

import lombok.Data;

import org.axonframework.modelling.command.TargetAggregateIdentifier;

@Data

@Builder

public class ValidatePaymentCommand {

    @TargetAggregateIdentifier

    private String paymentId;

    private String orderId;

    private CardDetails cardDetails;
}

OrderCancelledEvent :

package com.knoldus.commonservice.events;

import lombok.Data;

@Data

public class OrderCancelledEvent {

    private String orderId;

    private String orderStatus;
}
 
OrderCompletedEvent Class :

package com.knoldus.commonservice.events;

import lombok.Builder;

import lombok.Data;

@Data

@Builder

public class OrderCompletedEvent {

    private String orderId;

    private String orderStatus;
}

OrderShippedEvent class :

package com.knoldus.commonservice.events;

import lombok.Builder;

import lombok.Data;

@Data

@Builder

public class OrderShippedEvent {

    private String shipmentId;

    private String orderId;

    private String shipmentStatus;
}

PaymentCancelledEvent Class :

package com.knoldus.commonservice.events;

import lombok.Data;

@Data

public class PaymentCancelledEvent {

    private String paymentId;

    private String orderId;

    private String paymentStatus;
}

PaymentProcessedEvent Class:

package com.knoldus.commonservice.events;

import lombok.AllArgsConstructor;

import lombok.Data;

import lombok.NoArgsConstructor;

@Data

@AllArgsConstructor

@NoArgsConstructor

public class PaymentProcessedEvent {

    private String paymentId;

    private String orderId;
}

Card Details Class :

package com.knoldus.commonservice.model;

import lombok.Builder;

import lombok.Data;

@Data

@Builder

public class CardDetails {

    private String name;

    private String cardNumber;

    private Integer validUntilMonth;

    private Integer validUntilYear;

    private Integer cvv;

}

User Detail Class :

package com.knoldus.commonservice.model;


import lombok.Builder;

import lombok.Data;

@Data

@Builder

public class User {

    private String userId;

    private String firstName;

    private String lastName;

    private CardDetails cardDetails;

}

GetUserPaymentDetailsQuery Class :

package com.knoldus.commonservice.queries;

import lombok.AllArgsConstructor;

import lombok.Data;

import lombok.NoArgsConstructor;

@Data

@AllArgsConstructor

@NoArgsConstructor

public class GetUserPaymentDetailsQuery {

    private String userId;
}

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>common-service</artifactId>

	<version>0.0.1-SNAPSHOT</version>

	<name>CommonService</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</artifactId>

		</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>

	</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>

Payment Service Implementation:

Payment Service Structure :

Payment Aggregate: This service will receive the event and process it accordingly.

PaymentAggregate Class :
package com.knoldus.paymentservice.command.api.aggregate;

import com.knoldus.commonservice.commands.CancelPaymentCommand;

import com.knoldus.commonservice.commands.ValidatePaymentCommand;

import com.knoldus.commonservice.events.PaymentCancelledEvent;

import com.knoldus.commonservice.events.PaymentProcessedEvent;

import lombok.extern.slf4j.Slf4j;

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

@Slf4j

public class PaymentAggregate {

    @AggregateIdentifier

    private String paymentId;

    private String orderId;

    private String paymentStatus;

    public PaymentAggregate() {

    }

    @CommandHandler

    public PaymentAggregate(ValidatePaymentCommand validatePaymentCommand) {

        //Validate the Payment Details

        // Publish the Payment Processed event

        log.info("Executing ValidatePaymentCommand for " +

                "Order Id: {} and Payment Id: {}",

                validatePaymentCommand.getOrderId(),

                validatePaymentCommand.getPaymentId());

        PaymentProcessedEvent paymentProcessedEvent

                = new PaymentProcessedEvent(

                validatePaymentCommand.getPaymentId(), 

validatePaymentCommand.getOrderId()

        );

        AggregateLifecycle.apply(paymentProcessedEvent);

        log.info("PaymentProcessedEvent Applied");
    }

    @EventSourcingHandler

    public void on(PaymentProcessedEvent event) {

        this.paymentId = event.getPaymentId();

        this.orderId = event.getOrderId();

    }

    @CommandHandler

    public void handle(CancelPaymentCommand cancelPaymentCommand) {

        PaymentCancelledEvent paymentCancelledEvent

                = new PaymentCancelledEvent();

        BeanUtils.copyProperties(cancelPaymentCommand,

                paymentCancelledEvent);

        AggregateLifecycle.apply(paymentCancelledEvent);
    }

    @EventSourcingHandler

    public void on(PaymentCancelledEvent event) {

        this.paymentStatus = event.getPaymentStatus();

    }
}

Payment Class :

package com.knoldus.paymentservice.command.api.data;

import lombok.AllArgsConstructor;

import lombok.Builder;

import lombok.Data;

import lombok.NoArgsConstructor;

import javax.persistence.Entity;

import javax.persistence.Id;

import java.util.Date;

@Data

@Entity

@Builder

@AllArgsConstructor

@NoArgsConstructor

public class Payment {

    @Id

    private String paymentId;

    private String orderId;

    private Date timeStamp;

    private String paymentStatus;
}

package com.knoldus.paymentservice.command.api.data;

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

public interface PaymentRepository extends JpaRepository<Payment, String> {

}

PaymentsEventHandler Class :

package com.knoldus.paymentservice.command.api.events;

import com.knoldus.commonservice.events.PaymentCancelledEvent;

import com.knoldus.commonservice.events.PaymentProcessedEvent;

import com.knoldus.paymentservice.command.api.data.Payment;

import com.knoldus.paymentservice.command.api.data.PaymentRepository;

import org.axonframework.eventhandling.EventHandler;

import org.springframework.stereotype.Component;

import java.util.Date;

@Component

public class PaymentsEventHandler {

    private PaymentRepository paymentRepository;

    public PaymentsEventHandler(PaymentRepository paymentRepository) {

        this.paymentRepository = paymentRepository;
    }

    @EventHandler

    public void on(PaymentProcessedEvent event) {

        Payment payment

                = Payment.builder()

                .paymentId(event.getPaymentId())

                .orderId(event.getOrderId())

                .paymentStatus("COMPLETED")

                .timeStamp(new Date())

                .build();

        paymentRepository.save(payment);

    }


    @EventHandler

    public void on(PaymentCancelledEvent event) {

        Payment payment

                = paymentRepository.findById(event.getPaymentId()).get();

        payment.setPaymentStatus(event.getPaymentStatus());

        paymentRepository.save(payment);
    }
}

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>payment-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>

Shipment Service Implementation:

Shipment Service Structure :

ShipmentAggregate: Will handle the event and process the incoming event.

ShipmentAggregate Class :

package com.knoldus.shipmentservice.command.api.aggregate;


import com.knoldus.commonservice.commands.ShipOrderCommand;

import com.knoldus.commonservice.events.OrderShippedEvent;

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;

@Aggregate

public class ShipmentAggregate {

    @AggregateIdentifier

    private String shipmentId;

    private String orderId;

    private String shipmentStatus;


    public ShipmentAggregate() {

    }

    @CommandHandler

    public ShipmentAggregate(ShipOrderCommand shipOrderCommand) {

        //Validate the Command

        // Publish the Order Shipped event

        OrderShippedEvent orderShippedEvent

                = OrderShippedEvent

                .builder()

                .shipmentId(shipOrderCommand.getShipmentId())

                .orderId(shipOrderCommand.getOrderId())

                .shipmentStatus("COMPLETED")

                .build();

        AggregateLifecycle.apply(orderShippedEvent);

    }

    @EventSourcingHandler

    public void on(OrderShippedEvent event) {

        this.orderId = event.getOrderId();

        this.shipmentId = event.getShipmentId();

        this.shipmentStatus = event.getShipmentStatus();

    }

}
Shipment Class :

package com.knoldus.shipmentservice.command.api.data;


import lombok.Data;

import javax.persistence.Entity;

import javax.persistence.Id;


@Data

@Entity

public class Shipment {


    @Id

    private String shipmentId;

    private String orderId;

    private String shipmentStatus;

}

ShipmentRepository Class :

package com.knoldus.shipmentservice.command.api.data;

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

public interface ShipmentRepository extends JpaRepository<Shipment, String> {

}

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>shipment-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 = 9094

spring.datasource.url=jdbc:h2:file:~/data/shipmentDB

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
docker-compose.yml
version: "3.2"
services:
  axonserver:
    image: axoniq/axonserver
    hostname: axonserver
    ports:
      - '8024:8024'
      - '8124:8124'
      - '8224:8224'