Skip to content

Allow handling events that are part of a sealed event hierarchy #4177

@hjohn

Description

@hjohn

Feature Description

With sealed types being available in Java 21, we should make it possible to use this information to write concise focused event sourcing handlers and entity evolvers which can easily handle multiple event types in a single implementation.

Current Behaviour

Currently in the declarative approach one must match events based on QualifiedName, and in the annotated approach one must provide an @EventSourcingHandler per event type to handle.

Wanted Behaviour

Let's say we have these events:

sealed interface AccountEvent {
    String id();  // optional to allow reading the id before pattern match or cast
}
record AccountCreated(@EventTag(key = "account") String id, String name) implements AccountEvent {}
record FundsWithdrawn(@EventTag(key = "account") String id, long amount) implements AccountEvent {}
record FundsDeposited(@EventTag(key = "account") String id, long amount) implements AccountEvent {}

In a declarative approach, we should be able to register an evolver that looks like this:

Account evolve(Account input, AccountEvent event) {
    return switch(event) {
        case AccountCreated ac -> new Account(ac.id, ac.name, 0);
        case FundsWithdrawn fw -> input.withBalance(input.balance - fw.amount);
        case FundsDeposited fd -> input.withBalance(input.balance + fd.amount);
    };
}

Whether the creational event is handled here as well is up to the way AccountEvent is structured, and the declarative approach should allow for either option by allowing to omit specifying the entity factory (suggested is to make a method noEntityFactory, skipEntityFactory, createdByModel or evolverCreated etc.. as a way to skip the entity factory).

This is trivial to implement in EventSourcingRepository.

For the annotated approach, there are two options that probably should both be supported. The annotation inspector first should be smarter and see that the accepted type is a sealed type with known subtypes (reflection will provide these very nicely). It should also check if the method annotated is static or not. If it is static it should not create an entity factory, but instead leave creation up to the event sourcing handler; if it is non-static, it should automatically configure an entity factory (if it is a record) or look for a specific entity factory annotation.

Static Variant

record Account(String id, String name, long balance) {
    Account withBalance(long balance) {
        return new Account(id, name, balance);
    }
    
    @EventSourcingHandler
    static Account onAccountEvent(@Nullable Account input, @NonNull AccountEvents event) {
        return switch(event) {
            case AccountCreated ac -> new Account(ac.id, ac.name, 0);
            case FundsWithdrawn fw -> input.withBalance(input.balance - fw.amount);
            case FundsDeposited fd -> input.withBalance(input.balance + fd.amount);
        };
    }
}

Non-static variant

record Account(String id, String name, long balance) {
    Account withBalance(long balance) {
        return new Account(id, name, balance);
    }
    
    @EventSourcingHandler
    Account onAccountEvent(@NonNull Account input, @NonNull AccountEvents event) {
        return switch(event) {
            case FundsWithdrawn fw -> input.withBalance(input.balance - fw.amount);
            case FundsDeposited fd -> input.withBalance(input.balance + fd.amount);
        };
    }
}

Possible Workarounds

It is currently not possible to skip the entity factory configuration completely, although one may get away with a dummy implementation. The evolver is always called, even with the creational event, and so combining this with a dummy factory will allow this pattern.

The evolver can then be replaced with a sealed type evolver, that knows about sealed types and can call a user provided handler. For example for a static evolve method:

    .registerEntity(
        EventSourcedEntityModule.declarative(String.class, Account.class)
            .messagingModel((c, model) -> model.entityEvolver(new SealedEntityEvolver<>(AccountEvent.class, Account::onAccountEvent)).build())
            .entityFactory(c -> null)
            .criteriaResolver(c -> (id, ctx) -> EventCriteria.havingTags(Tag.of("account", id)))
            .build()
    )

Metadata

Metadata

Assignees

No one assigned

    Labels

    Priority 2: ShouldHigh priority. Ideally, these issues are part of the release they’re assigned to.Status: DuplicateUse to signal this issue is a duplicate of another. Please refer to the other issue.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions