eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

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

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

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 code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.

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

Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

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

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook Jackson – NPI EA – 3 (cat = Jackson)