Generic Top

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

In this article, we'll be looking into how Axon supports Aggregates with multiple entities.

We consider this article to be an expansion of our main guide on Axon. As such, we'll utilize both Axon Framework and Axon Server again. We'll use the former in the code of this article, and the latter is the Event Store and Message Router.

As this is an expansion, let's elaborate a bit on the Order domain that we presented in the base article.

2. Aggregates and Entities

The Aggregates and Entities that Axon supports stem from Domain-Driven Design. Prior to diving into code, let's first establish what an entity is within this context:

  • An object that is not fundamentally defined by its attributes, but rather by a thread of continuity and identity

An entity is thus identifiable, but not through the attributes it contains. Furthermore, changes occur on the entity, as it maintains a thread of continuity.

Knowing this, we can take the following step, by sharing what an Aggregate means in this context (distilled from Domain-Driven Design: Tackling Complexity in the Heart of Software):

  • An Aggregate is a group of associated objects acting as a single unit to data changes
  • References regarding the Aggregate are restricted towards a single member, the Aggregate Root
  • A set of consistency rules apply within the Aggregate boundary

As the first point dictates, an Aggregate is not a single thing, but a group of objects. Objects can be value objects but, more importantly, they can also be entities. Axon supports modeling the aggregate as a group of associated objects rather than a single object, as we'll see later on.

3. Order Service API: Commands and Events

As we're dealing with a message-driven application, we start with defining new commands when expanding the Aggregate to contain multiple entities.

Our Order domain currently contains an OrderAggregate. A logical concept to include in this Aggregate is the OrderLine entity. An order line refers to a specific product that is being ordered, including the total number of product entries.

Knowing this, we can expand the command API – which consisted of a PlaceOrderCommand, ConfirmOrderCommand, and ShipOrderCommand – with three additional operations:

  • Adding a product
  • Incrementing the number of products for an order line
  • Decrementing the number of products for an order line

These operations translate to the classes AddProductCommand, IncrementProductCountCommand, and DecrementProductCountCommand:

public class AddProductCommand {

    @TargetAggregateIdentifier
    private final String orderId;
    private final String productId;

    // default constructor, getters, equals/hashCode and toString
}
 
public class IncrementProductCountCommand {

    @TargetAggregateIdentifier
    private final String orderId;
    private final String productId;

    // default constructor, getters, equals/hashCode and toString
}
 
public class DecrementProductCountCommand {

    @TargetAggregateIdentifier
    private final String orderId;
    private final String productId;

    // default constructor, getters, equals/hashCode and toString
}

The TargetAggregateIdentifier is still present on the orderId, since the OrderAggregate remains the Aggregate within the system.

Remember from the definition, the entity also has an identity. This is why the productId is part of the command. Later in this article, we'll show how these fields refer to an exact entity.

Events will be published as a result of command handling, notifying that something relevant has happened. So, the event API should also be expanded as a result of the new command API.

Let's looks at the POJOs that reflect the enhanced thread of continuity — ProductAddedEvent, ProductCountIncrementedEvent, ProductCountDecrementedEvent, and ProductRemovedEvent:

public class ProductAddedEvent {

    private final String orderId;
    private final String productId;

    // default constructor, getters, equals/hashCode and toString
}
 
public class ProductCountIncrementedEvent {

    private final String orderId;
    private final String productId;

    // default constructor, getters, equals/hashCode and toString
}
 
public class ProductCountDecrementedEvent {

    private final String orderId;
    private final String productId;

    // default constructor, getters, equals/hashCode and toString
}
 
public class ProductRemovedEvent {

    private final String orderId;
    private final String productId;

    // default constructor, getters, equals/hashCode and toString
}

4. Aggregates and Entities: Implementation

The new API dictates that we can add a product and increment or decrement its count. As this occurs per product added to the Order, we need to define distinct order lines allowing these operations. This signals the requirement to add an OrderLine entity that is part of the OrderAggregate

Axon doesn't know, without guidance, if an object is an entity in an Aggregate. We should place the AggregateMember annotation on a field or method exposing the entity to mark it as such.

We can use this annotation for single objects, collections of objects, and maps. In the Order domain, we're better off using a map of the OrderLine entity on the OrderAggregate. 

4.1. Aggregate Adjustments

Knowing this, let's enhance the OrderAggregate:

@Aggregate
public class OrderAggregate {

    @AggregateIdentifier
    private String orderId;
    private boolean orderConfirmed;

    @AggregateMember
    private Map<String, OrderLine> orderLines;

    @CommandHandler
    public void handle(AddProductCommand command) {
        if (orderConfirmed) {
            throw new OrderAlreadyConfirmedException(orderId);
        }
        
        String productId = command.getProductId();
        if (orderLines.containsKey(productId)) {
            throw new DuplicateOrderLineException(productId);
        }
        
        AggregateLifecycle.apply(new ProductAddedEvent(orderId, productId));
    }

    // previous command- and event sourcing handlers left out for conciseness

    @EventSourcingHandler
    public void on(OrderPlacedEvent event) {
        this.orderId = event.getOrderId();
        this.orderConfirmed = false;
        this.orderLines = new HashMap<>();
    }

    @EventSourcingHandler
    public void on(ProductAddedEvent event) {
        String productId = event.getProductId();
        this.orderLines.put(productId, new OrderLine(productId));
    }

    @EventSourcingHandler
    public void on(ProductRemovedEvent event) {
        this.orderLines.remove(event.getProductId());
    }
}

Marking the orderLines field with the AggregateMember annotation tells Axon it's part of the domain model. Doing this allows us to add CommandHandler and EventSourcingHandler annotated methods in the OrderLine object, just as in the Aggregate.

As the OrderAggregate holds the OrderLine entities, it's in charge of adding and removing the products and, thus, the respective OrderLines. The application uses Event Sourcing, so there's a ProductAddedEvent and ProductRemovedEvent EventSourcingHandler that respectively add and remove an OrderLine.

The OrderAggregate decides when to add a product or decline the addition since it holds the OrderLines. This ownership dictates that the AddProductCommand command handler lies within the OrderAggregate.

A successful addition is notified through publishing the ProductAddedEvent. Unsuccessful addition follows from throwing the DuplicateOrderLineException, if the product is already present, and an OrderAlreadyConfirmedException if the OrderAggregate has already been confirmed.

Lastly, we set the orderLines map in the OrderPlacedEvent handler because it's the first event in the OrderAggregate‘s event stream. We can set the field globally in the OrderAggregate or in a private constructor, but this would mean state changes are no longer the sole domain of the event sourcing handlers.

4.2. Entity Introduction

With our updated OrderAggregate, we can start taking a look at the OrderLine:

public class OrderLine {

    @EntityId
    private final String productId;
    private Integer count;
    private boolean orderConfirmed;

    public OrderLine(String productId) {
        this.productId = productId;
        this.count = 1;
    }

    @CommandHandler
    public void handle(IncrementProductCountCommand command) {
        if (orderConfirmed) {
            throw new OrderAlreadyConfirmedException(orderId);
        }
        
        apply(new ProductCountIncrementedEvent(command.getOrderId(), productId));
    }

    @CommandHandler
    public void handle(DecrementProductCountCommand command) {
        if (orderConfirmed) {
            throw new OrderAlreadyConfirmedException(orderId);
        }
        
        if (count <= 1) {
            apply(new ProductRemovedEvent(command.getOrderId(), productId));
        } else {
            apply(new ProductCountDecrementedEvent(command.getOrderId(), productId));
        }
    }

    @EventSourcingHandler
    public void on(ProductCountIncrementedEvent event) {
        this.count++;
    }

    @EventSourcingHandler
    public void on(ProductCountDecrementedEvent event) {
        this.count--;
    }

    @EventSourcingHandler
    public void on(OrderConfirmedEvent event) {
        this.orderConfirmed = true;
    }
}

The OrderLine should be identifiable, as is defined in section 2. The entity is identifiable through the productId field, which we marked with the EntityId annotation.

Marking a field with the EntityId annotation tells Axon which field identifies the entity instance inside an aggregate.

Since the OrderLine reflects a product that is being ordered, it's in charge of handling the IncrementProductCountCommand and DecrementProductCountCommand. We can use the CommandHandler annotation inside an entity to directly route these commands to the appropriate entity.

As Event Sourcing is used, the state of the OrderLine needs to be set based on events. The OrderLine can simply include the EventSourcingHandler annotation for the events it requires to set the state, similar to the OrderAggregate.

Routing a command to the correct OrderLine instance is done by using the EntityId annotated field. To be routed correctly, the name of the annotated field should be identical to one of the fields contained in the command. In this sample, that's reflected by the productId field present in the commands and in the entity.

Correct command routing makes the EntityId a hard requirement whenever the entity is stored in a collection or map. This requirement is loosened to a recommendation if only a single instance of an aggregate member is defined.

We should adjust the routingKey value of the EntityId annotation whenever the name in the command differs from the annotated field. The routingKey value should reflect an existing field on the command to allow command routing to be successful.

Let's explain it through an example:

public class IncrementProductCountCommand {

    @TargetAggregateIdentifier
    private final String orderId;
    private final String productId;

    // default constructor, getters, equals/hashCode and toString
}
...
public class OrderLine {

    @EntityId(routingKey = "productId")
    private final String orderLineId;
    private Integer count;
    private boolean orderConfirmed;

    // constructor, command and event sourcing handlers
}

The IncrementProductCountCommand has stayed the same, containing the orderId aggregate identifier and productId entity identifier. In the OrderLine entity, the identifier is now called orderLineId.

Since there's no field called orderLineId in the IncrementProductCountCommand, this would break the automatic command routing based on the field name.

Hence, the routingKey field on the EntityId annotation should reflect the name of a field in the command to maintain this routing ability. 

5. Conclusion

In this article, we've looked at what it means for an aggregate to contain multiple entities and how Axon Framework supports this concept.

We've enhanced the Order application to allow Order Lines as separate entities to belong to the OrderAggregate.

Axon's aggregate modeling support provides the AggregateMember annotation, enabling users to mark objects to be entities of a given aggregate. Doing so allows command routing towards an entity directly, as well as keeping event sourcing support in place.

The implementation of all these examples and the code snippets can be found over on GitHub.

For any additional questions on this topic, also check out Discuss AxonIQ.

Generic bottom

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE
Generic footer banner
Comments are closed on this article!