DEV Community

loading...
Cover image for CQRS using Java and Axon - Command module

CQRS using Java and Axon - Command module

fabiothiroki profile image Fabio Hiroki ・4 min read

Introduction

In this second part of this article series, we will implement the Command module, responsible for application state changes. Final code is in Github.

REST API Layer

We will effectively start coding our application from the external layer and keep going internally. The ProductController class will be responsible for exposing the endpoints to request state changes.

The only dependency of this class will be Axon's CommandGateway responsible for dispatching command objects. The initial structure will be:

@RestController
@RequestMapping("/products")
public class ProductController {

    @Autowired
    public ProductController(final CommandGateway commandGateway) {
        this.commandGateway = commandGateway;
    }

    private CommandGateway commandGateway;

    @PostMapping
    public CompletableFuture<String> create(@RequestBody ProductDTO dto) {
        return null;
    }

    @PutMapping
    public CompletableFuture<String> update(@RequestBody ProductDTO dto) {
        return null;
    }
}

Where ProductDTO class is just a POJO to map the json request.

public class ProductDTO {

    private Long id;

    private String name;

    private int quantity;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getQuantity() {
        return quantity;
    }

    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }
}

Command model

Command instances on this application will represent intents of state change. The subset of application state will be represented by an Aggregate object. For example, the intent for adding a new product to cart is represented by AddProductCommand class:

public class AddProductCommand {

    public AddProductCommand(
            final Long id,
            final String name,
            final int quantity) {
        this.id = id;
        this.name = name;
        this.quantity = quantity;
    }

    @TargetAggregateIdentifier
    private final Long id;
    private final String name;
    private final int quantity;

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getQuantity() {
        return quantity;
    }
}

Where TargetAggregateIdentifier annotation is used to identify which instance of an Aggregate type should be handled by this command.

Now to dispatch this command from our RestController we just need to instantiate it and pass as argument through CommandGateway send method:

@RestController
@RequestMapping("/products")
public class ProductController {

    @Autowired
    public ProductController(final CommandGateway commandGateway) {
        this.commandGateway = commandGateway;
    }

    private CommandGateway commandGateway;

    @PostMapping
    public CompletableFuture<String> create(@RequestBody ProductDTO dto) {
        AddProductCommand command = new AddProductCommand(
                dto.getId(),
                dto.getName(),
                dto.getQuantity());
        return commandGateway.send(command);
    }

    // ....
}

Aggregate

As mentioned before, the Aggregate class will be responsible for representing part of the application state plus the Command handling for that Aggregate.

An example of an Aggregate could be:

{
  "id": 1,
  "name": "iPhone",
  "quantity": 2
}

Translating into code, results in:

@Aggregate
public class ProductAggregate {

    @AggregateIdentifier
    private Long id;
    private String name;
    private int quantity;

    @CommandHandler
    public ProductAggregate(AddProductCommand cmd) {
        // Verifies state consistency and applies events
    }
}

CommandHandler annotation on constructor means the AddProductCommand command is used to create a new Aggregate.

At this point, the state of your application hasn't changed yet. Command handling is the location to perform business logic (i.e.: check if quantity is a positive value) and possibly apply Events that will result in state change.

Event model

First we create the AddProductEvent class with attributes needed to trigger our desired state change. In this case specifically it will be very similar to its respective Command model.

Now we will change the ProductAggregate constructor to dispatch an AddProductEvent whenever a AddProductCommand is sent. This same class can also act as the event sourcing handler of AddProductEvent, performing the change of the application state.

import static org.axonframework.modelling.command.AggregateLifecycle.apply;


@Aggregate
public class ProductAggregate {

    @AggregateIdentifier
    private Long id;
    private String name;
    private int quantity;

    @CommandHandler
    public ProductAggregate(AddProductCommand cmd) {
        apply(new AddProductEvent(cmd.getId(), cmd.getName(), cmd.getQuantity()));
    }

    @EventSourcingHandler
    public void on(AddProductEvent event) {
        this.id = event.getId();
        this.name = event.getName();
        this.quantity = event.getQuantity();
    }
}

So far we have established a checkpoint that allow us to test the application and observe what's happening. Follow the terminal commands to run the commandside application:

docker-compose up -d
./gradlew clean assemble
java -jar commandside/build/libs/commandside.jar

Now we can test the endpoint by adding a new product:

curl -X POST http://localhost:8080/products -H 'Content-Type: application/json' -d '{"id": 1, "name": "iPhone", "quantity": 7}'

We can verify in the mongo database, in domainevents collection that there's a new event stored there:

{
   "_id":"5e0a2924b813b63783e1e092",
   "aggregateIdentifier":"1",
   "type":"ProductAggregate",
   "sequenceNumber":"0",
   "serializedPayload":"<com.example.project.command.addproduct.AddProductEvent><id>1</id><name>iPhone</name><quantity>7</quantity></com.example.project.command.addproduct.AddProductEvent>",
   "timestamp":"2019-12-30T16:43:16.851862731Z",
   "payloadType":"com.example.project.command.addproduct.AddProductEvent",
   "payloadRevision":null,
   "serializedMetaData":"<meta-data><entry><string>traceId</string><string>e62c8e0d-7505-4e99-ab7e-84b4619ee159</string></entry><entry><string>correlationId</string><string>e62c8e0d-7505-4e99-ab7e-84b4619ee159</string></entry></meta-data>",
   "eventIdentifier":"6eef19d8-b22a-4be6-9fd9-7681a31580b8"
}

For each new product added through POST request, we will have a new entry on domainevents from now on. That acts as a history of what happened on our application.

Aggregate persistence

Besides the individual event persistence, we also want to persist the aggregate on each change (State-Stored Aggregate). To achieve this we just need to add JPA annotations to turn our aggregate class into an Entity:

@Aggregate
@Entity // This class can now be mapped to a table 
public class ProductAggregate {

    @AggregateIdentifier
    @Id // Defines the primary key
    private Long id;

    @Column // Map to a column with same name
    private String name;

    @Column // Map to a column with same name
    private int quantity;

    @CommandHandler
    public ProductAggregate(AddProductCommand cmd) {
        apply(new AddProductEvent(cmd.getId(), cmd.getName(), cmd.getQuantity()));
    }

    @EventSourcingHandler
    public void on(AddProductEvent event) {
        this.id = event.getId();
        this.name = event.getName();
        this.quantity = event.getQuantity();
    }
}

Now add a new product on your cart using the endpoint above, and check the product_table on your Postgres database to verify a new entry stored matching the desired aggregate. Your Mongo database should also contain the new event.

Conclusion

On Mongo database, we have now the history of all events which we can use to understand how the application reached its current state. On the other side, Postgres database has the data we can use to display to final users, on a checkout screen, for example.

We could go back all the way and implement the CQRS and event sourcing by ourselves but thankfully we can achieve the same result using a couple of annotations from Axon.

Discussion (1)

pic
Editor guide
Collapse
placidmasvidal profile image
Plàcid Masvidal

Hi Fabio Hiroki, thank you so much for this tutorial.

I'm having troubles, because the AxonConfig class of your project's source code on github isn't working for me, I'm getting Could not autowire, no MongoClient beans found.

So I've tried to create a bean for MongoClient but it doesn't work. I realize that the MongoClient bean that the storageEngine method expects appears to be com.mongodb.client.MongoClient and not the com.mongodb.MongoClient that you are using in your code, so I tried to use that MongoClient with the same AxonConfig class code, and then it compiles but fails to instantiate the bean, here I posted the problem stackoverflow.com/questions/666012...

Do you know what is happening and how can I solve that?

Thank you