Multi-Entity Migration
In Domain-Driven Design (DDD), an Aggregate is a cluster of domain objects that can be treated as a single unit. Every aggregate has an Aggregate Root, which is an Entity that is responsible for maintaining the consistency of the entire cluster. All access to the aggregate’s members is strictly through the aggregate root.
In Axon Framework 4, this concept was explicitly modeled using the @AggregateRoot (or @Aggregate) annotation for the root entity, and @AggregateMember for any child entities. This structure enforced a clear hierarchy where the root entity acted as the primary entry point for commands and the orchestrator of state changes.
However, dealing with hierarchical entity models and handling and correctly routing messages to the appropriate entity within an aggregate could become complex, especially in scenarios involving multiple child entities. This was one main reason for Axon Framework 5 towards the more flexible Dynamic Context Boundaries (DCB).
Axon Framework 5 continues to support this hierarchical model but shifts the terminology towards a more generic Entity model, where the distinction between the "root" and "members" is expressed through @EventSourcedEntity and @EntityMember.
@EntityMember instead of @AggregateMember
When defining child entities within an aggregate (now entity), use @EntityMember instead of @AggregateMember.
The following example shows a GiftCard aggregate with a collection of Transaction entities.
-
Axon Framework 5
-
Axon Framework 4
@EventSourcedEntity
public class GiftCard {
private String cardId;
@EntityMember(routingKey = "transactionId") (1)
private List<Transaction> transactions = new ArrayList<>();
@CommandHandler
public void handle(RedeemCardCommand cmd, EventAppender eventAppender) {
// ... validation logic
eventAppender.append(new CardRedeemedEvent(cardId, cmd.amount(), cmd.transactionId()));
}
@EventSourcingHandler
public void on(CardRedeemedEvent event) {
this.transactions.add(new Transaction(event.transactionId(), event.amount())); (2)
}
}
public class Transaction {
private String transactionId; (3)
private int amount;
public Transaction(String transactionId, int amount) {
this.transactionId = transactionId;
this.amount = amount;
}
}
| 1 | @EntityMember replaces @AggregateMember. |
| 2 | The pattern for managing child entities in @EventSourcingHandler remains the same. |
| 3 | No specific annotation is required on the field if it’s managed via the constructor or sourcing handlers. |
@AggregateRoot
public class GiftCard {
@AggregateIdentifier
private String cardId;
@AggregateMember (1)
private List<Transaction> transactions = new ArrayList<>();
@CommandHandler
public void handle(RedeemCardCommand cmd) {
// ... validation logic
AggregateLifecycle.apply(new CardRedeemedEvent(cardId, cmd.getAmount(), cmd.getTransactionId()));
}
@EventSourcingHandler
public void on(CardRedeemedEvent event) {
this.transactions.add(new Transaction(event.getTransactionId(), event.getAmount())); (2)
}
}
public class Transaction {
@EntityId (3)
private String transactionId;
private int amount;
public Transaction(String transactionId, int amount) {
this.transactionId = transactionId;
this.amount = amount;
}
}
| 1 | @AggregateMember marks the collection of child entities. |
| 2 | Child entities are typically instantiated and added to the collection in an @EventSourcingHandler. |
| 3 | @EntityId is used to identify the child entity within the collection. |
|
Maps are not supported
The |