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.