Reactive Event Sourcing in Java, Part 6: Empty State
So far, our implementation of a Show
aggregate started in the first part is very minimalistic. This is fine for learning purposes, but it's time to focus more on modeling.
Domain
For some aggregates, an initial state (also known as an empty state) is very natural and straightforward, e.g. empty order book (for a trading application). In reality, this is rarely the case. Usually, you need to create or initialize the state manually with the first command, e.g. CreateUser
, StartPayment
, CreateAccount
, etc.
Let's refactor our existing code to create the Show
aggregate with a given title and a maximum number of seats. In a real use case, this would be a very complex operation, but you will be able to grab the overall concept and adjust it to your case.
We start with the domain, which should be extended with the CreateShow
command and the ShowCreated
event.
record CreateShow(ShowId showId, String title, int maxSeats) implements ShowCommand {
}
///////
record ShowCreated(ShowId showId, Instant createdAt, InitialShow initialShow) implements ShowEvent {
}
The command is rather obvious, but the event is more interesting. Instead of using only information from the command, we are enriching the event with an InitialShow
record that contains all the seats. The logic behind the seat generation might change in the future, e.g. for some seats, prices might be higher (right now it's a constant value). It's safer to add a collection of all the seats to the first event. This way, we will be able to process the old events without worrying about implementation changes. We could feel a temptation to add the Show
aggregate to the event instead of InitialShow
(at this point, they look the same). I don't recommend this solution since any further changes to the Show
aggregate would need to be backward compatible. We need to be able to read the old events. With an explicit separation (via InitialShow
), we can refactor our aggregate however we want. It is only a projection from events. This is a huge advantage in comparison to the classic approach to state persistence, where we are constrained with the model changes by the persistence mechanism. That's why with Event Sourcing, we should put more attention to the events modeling because this is the key for long-term elasticity and extensibility.
This time, we cannot use the show.process(...)
method to handle the CreateShow
command because there is no state yet. We can create a separate utility for that:
public class ShowCreator {
public static Either<ShowCommandError, ShowCreated> create(CreateShow createShow, Clock clock) {
//more domain validation here
if (createShow.maxSeats() > 100) {
return left(TOO_MANY_SEATS);
} else {
var initialShow = new InitialShow(createShow.showId(), createShow.title(), createSeats(INITIAL_PRICE, createShow.maxSeats()));
var showCreated = new ShowCreated(createShow.showId(), clock.now(), initialShow);
return right(showCreated);
}
}
}
The create
method signature is very similar to the process
method signature and we will use that fact later.
Since the first ShowCreated
event should be the start of aggregate life, we can use it as a factory method parameter:
public static Show create(ShowCreated showCreated) {
InitialShow initialShow = showCreated.initialShow();
return new Show(initialShow.id(), initialShow.title(), initialShow.seats());
}
As you noticed, both state.process(...)
and state.apply(...)
methods, described as entry points to our domain in the first first part, are not very handy during the creation of the state, that's why we need to extend our entry points set and provide a separate path than for normal commands/events processing.
Another approach would be to switch from an object-oriented to a more functional domain design. Instead of state.process(command)
, we can refactor this to process(state, command)
and apply(state, event)
. This way, we will end up with only 2 entry points (as before) but the state must be an optional parameter. Which strategy is better? Both are fine, for some aggregates, object-oriented code will look nicer, and for others, functional might be better.
Event sourced entity
Some changes are required also in our ShowEntity
implementation. Remember that EventSourcedBehavior
must be parameterized with an explicit state/aggregate type. First, let's think about how we could model the "empty" Show. There are several options:
- type hierarchy,
- using null as an empty Show representation,
- wrapping the
Show
with anOptional
/Option
monadic container.
Type hierarchy
We could refactor our domain to something like this:
public interface Show {}
record ExistingShow(/*some fields*/) implements Show {}
record NotExistingShow(/*no fields*/) implements Show {}
It looks interesting, but from my experience, it will be quite painful to manage such a hierarchy since we need NotExisitngShow
only once and after that we will never go back to this state again. Our code base will be full of casting and instanceof
statements. I would consider the hierarchical approach when this is driven by domain modeling, e.g. many possible states, with different responsibilities and a different set of data. An empty state is more of a technical problem.
null state
Good, old-fashioned Java null
. We all hate nulls because of the NullPointerException
but sometimes they are just good enough. Let's verify what this could look like. The empty state is set to null explicitly.
@Override
public Show emptyState() {
return null;
}
Our command handler builder is split into two sections:
when the state is null then process only CreateShowCommand
or return a none/empty
response.
otherwise, process all commands as before.
public CommandHandlerWithReply<ShowEntityCommand, ShowEvent, Show> commandHandler() {
var builder = newCommandHandlerWithReplyBuilder();
builder.forNullState()
.onCommand(ShowEntityCommand.GetShow.class, this::returnEmptyState)
.onCommand(ShowEntityCommand.ShowCommandEnvelope.class, this::handleShowCreation);
builder.forStateType(Show.class)
.onCommand(ShowEntityCommand.GetShow.class, this::returnState)
.onCommand(ShowEntityCommand.ShowCommandEnvelope.class, this::handleShowCommand);
return builder.build();
}
A similar situation happens in the eventHandler
method. For a null state, create it - Show::create
, and then continue with the Show.apply(...)
method.
public EventHandler<Show, ShowEvent> eventHandler() {
EventHandlerBuilder<Show, ShowEvent> builder = newEventHandlerBuilder();
builder.forNullState()
.onEvent(ShowCreated.class, Show::create);
builder.forStateType(Show.class)
.onAnyEvent(Show::apply);
return builder.build();
}
The rest of the changes are not very significant. The ShowService
exposes a new method for creating the show. Finding a show by id might return Option.none()
, which should be handled in the HTTP controller. A full diff is available here [like].
Optional state
Wrapping the Show
with Optional
(Java) or Option
(Vavr) monadic container is also a pretty elegant solution. This time, it will be your homework to refactor
EventSourcedBehaviorWithEnforcedReplies<ShowEntityCommand, ShowEvent, Show>
into
EventSourcedBehaviorWithEnforcedReplies<ShowEntityCommand, ShowEvent, Option<Show>>
Just follow the types and compiler errors and you should be able to implement ShowEntity
without nulls. The eventHandler
method could be refactored to something like this:
public EventHandler<Option<Show>, ShowEvent> eventHandler2() {
EventHandlerBuilder<Option<Show>, ShowEvent> builder = newEventHandlerBuilder();
builder.forState(Option::isEmpty)
.onEvent(ShowCreated.class, event -> Option.of(Show.create(event)));
builder.forState(Option::isDefined)
.onAnyEvent((show, event) -> Option.of(show.get().apply(event)));
return builder.build();
}
If you feel that this show.get()
invocation is shameless, well... I agree with you :) Unfortunately, using monadic types in Java is not so pleasant as in e.g. Scala. In the Scala codebase, I would definitely go with an Option
wrapper but in Java, it's a matter of readability and convenience. I prefer to be more pragmatic than dogmatic.
Summary
Handling an empty state might be confusing at the beginning of the Event Sourcing journey. I hope that this post will clarify a lot for you. You can experiment with the above solutions and choose the best one for your use case. I'm planning to introduce another aggregate in the future. I will probably use another strategy to model the empty state just to give you more examples.
However, the next part will be about something different. We will leave the write side of Event Sourcing and focus on the read side. We will talk about CQRS and Akka Persistent Query library for events streaming. Stay tuned, join our SoftwareMill Academy mailing list and analyze the part_6 tag.