Entity Inheritance
This page explains how Axon runs the event sourcing handlers declared on an entity class and its supertypes. For a parent entity that owns child entities, see Entity hierarchies.
How the handlers run
An overriding method counts as one handler and runs where the handler it overrides runs; call super to run the overridden one.
A method that becomes a handler only in a subclass, because only its override is annotated, runs at that subclass’s position.
Separate methods for the same event all run, parent handlers first, each receiving the entity returned by the previous one.
Within one class the order is unspecified.
If a handler returns an entity the next handler cannot run on, evolving fails with a StateEvolvingException.
Handlers of the returned type do not run for the same event.
A handler matches an event by its event name, so a handler accepting Object does not receive other events.
-
Override
-
Override calling super
-
Separate methods
-
Immutable entity
-
Changing the concrete type
SavingsAccount.on overrides Account.on.
Only the override runs.
class Account {
int balance;
@EventSourcingHandler
void on(Deposited event) {
balance += event.amount();
}
}
class SavingsAccount extends Account {
@Override
@EventSourcingHandler
void on(Deposited event) {
balance += event.amount();
}
}
Deposited(10) on a SavingsAccount with balance 0
1. SavingsAccount.on balance 0 -> 10
BonusAccount.on overrides Account.on and calls super.on.
Both bodies run, the override once.
class BonusAccount extends Account {
@Override
@EventSourcingHandler
void on(Deposited event) {
super.on(event);
balance += event.amount();
}
}
Deposited(10) on a BonusAccount with balance 0
1. BonusAccount.on balance 0 -> 20
super.on (Account.on) balance 0 -> 10
rest of BonusAccount.on balance 10 -> 20
BonusWallet.onBonusDeposit does not override Wallet.onDeposit.
Both run, parent first.
class Wallet {
int balance;
@EventSourcingHandler
void onDeposit(Deposited event) {
balance += event.amount();
}
}
class BonusWallet extends Wallet {
@EventSourcingHandler
void onBonusDeposit(Deposited event) {
balance += 2 * event.amount();
}
}
Deposited(10) on a BonusWallet with balance 0
1. Wallet.onDeposit balance 0 -> 10
2. BonusWallet.onBonusDeposit balance 10 -> 30
VipCard is the entity type; Card is a plain interface that supplies a default handler.
Each handler returns the new entity, and the next handler receives it.
The result depends on the parent-first order.
interface Card {
int balance();
Card withBalance(int balance);
@EventSourcingHandler
default Card onDeposit(Deposited event) {
return withBalance(balance() + event.amount());
}
}
record VipCard(int balance) implements Card {
public Card withBalance(int balance) {
return new VipCard(balance);
}
@EventSourcingHandler
VipCard onVipDeposit(Deposited event) {
return new VipCard(balance * 2);
}
}
Deposited(10) on a VipCard with balance 0
1. Card.onDeposit VipCard(0) -> VipCard(10)
2. VipCard.onVipDeposit VipCard(10) -> VipCard(20)
InitialCourse.onCreated may change the concrete type: as the most specific handler it runs last, so no handler follows it.
sealed interface Course permits InitialCourse, CreatedCourse {
int touches();
Course withTouches(int touches);
@EventSourcingHandler
default Course countTouch(CourseCreated event) {
return withTouches(touches() + 1);
}
}
record InitialCourse(int touches) implements Course {
public Course withTouches(int touches) {
return new InitialCourse(touches);
}
@EventSourcingHandler
CreatedCourse onCreated(CourseCreated event) {
return new CreatedCourse(event.name(), touches);
}
}
record CreatedCourse(String name, int touches) implements Course {
public Course withTouches(int touches) {
return new CreatedCourse(name, touches);
}
}
CourseCreated("Axon") on an InitialCourse with 0 touches
1. Course.countTouch InitialCourse(0) -> InitialCourse(1)
2. InitialCourse.onCreated InitialCourse(1) -> CreatedCourse("Axon", 1)
A handler of a more general type that changes the concrete type to a sibling is rejected when a handler of the previous type follows, because that handler cannot run on the sibling.
sealed interface Pass permits GiftPass, VipPass {
int balance();
@EventSourcingHandler
default Pass onDeposit(Deposited event) {
return new GiftPass(balance() + event.amount());
}
}
record GiftPass(int balance) implements Pass {
}
record VipPass(int balance) implements Pass {
@EventSourcingHandler
VipPass onVipDeposit(Deposited event) {
return new VipPass(balance * 2);
}
}
Deposited(10) on a VipPass with balance 0
1. Pass.onDeposit VipPass(0) -> GiftPass(10)
2. VipPass.onVipDeposit rejected
Axon throws a StateEvolvingException.
Its cause names the handler that returned the new type, the next handler, and the type that handler requires.