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. |
Event forwarding mode
In Axon Framework 4, @AggregateMember had an eventForwardingMode attribute controlling how events were delivered to child entities:
-
ForwardAll(default): every event was forwarded to every child. -
ForwardMatchingInstances: events were forwarded only to children whose@EntityIdmatched a field on the event. -
ForwardNone: no events were forwarded automatically.
In Axon Framework 5, this attribute does not exist.
The default behaviour is equivalent to ForwardAll: every event is offered to every child’s @EventSourcingHandler.
To replicate ForwardMatchingInstances, configure eventTargetMatcher on @EntityMember:
@EntityMember(
routingKey = "transactionId",
eventTargetMatcher = RoutingKeyEventTargetMatcherDefinition.class (1)
)
private List<Transaction> transactions = new ArrayList<>();
| 1 | RoutingKeyEventTargetMatcherDefinition routes events only to the child whose routing key matches a field on the event payload, preventing every child from receiving every event. |
There is no direct replacement for ForwardNone.
If you need to suppress event forwarding entirely, do not declare @EventSourcingHandler methods on the child entity. Axon will offer events but nothing will handle them.
See also
-
Entity Hierarchies: full reference for
@EntityMember, routing key configuration, and event routing in Axon Framework 5
|
Maps are not supported
The |