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()
)
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@EventSourcingHandlerper event type to handle.Wanted Behaviour
Let's say we have these events:
In a declarative approach, we should be able to register an evolver that looks like this:
Whether the creational event is handled here as well is up to the way
AccountEventis structured, and the declarative approach should allow for either option by allowing to omit specifying the entity factory (suggested is to make a methodnoEntityFactory,skipEntityFactory,createdByModelorevolverCreatedetc.. 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
staticor not. If it isstaticit 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 arecord) or look for a specific entity factory annotation.Static Variant
Non-static variant
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
staticevolve method: