· 7 min read
5 Step Implementation of Custom Clean Event Sourcing in Spring Boot (without AggregateRoot)
Implementing event sourcing in Spring Boot from scratch is easy. You just need to understand some core concepts.
In the last post, I described what event sourcing is and what the benefits of doing it are. Event Sourcing helps you:
- Be prepared for the future by creating excellent analytics that are out of the box.
- Leads to multiple smaller pieces of code over one large piece.
- It’s easier overall compared to regular code.
- It’s cleaner, much more precise, and infinitely reduces the chances of bugs.
Event sourcing is one of the best pieces of code I have written, something I have been very proud of. I learnt ES from YouTube and implementing a well-architected redux on the front end (which is also message-based). (TODO: Add an article here).
Implementing Event Sourcing From Scratch
ES’s core is small, but there are pieces to understand precisely. Below, I’ll attempt my best to describe ES. Here’s what I’ll explain:
- Understanding Basic Flow of the application
- Creating an Event Store
- Creation of main Event Sourcing Service
- Creation of Commands
- Creation of Command Handler
Note that I did not use existing Spring Related Frameworks and instead created something of my own.
The following is inspired by Axon Framework and multiple YouTube videos.
Step 1: Understand the basic flow of the Event Sourcing
The basic flow of Event sourcing is this.
- You have a command you want to run.
- This could be Create, Update, or Delete of some kind.
- You validate if the Command can be run
- If the Command can be executed, you save it as an event in the Event Store.
- After storing it in the Event Store, apply that Command over the domain object.
- Here, we do not validate since ES includes validated events.
For example, suppose you have a Movie
Domain Object and a command to UpdateMovieName
. Here’s the flow.
These are typical parts of the above flow.
For any command, if the Command is valid,
- we’d validate
- we’d store it in the event store
- we’d apply
I shifted these common concerns to an EventSourcingService
. This would include the main framework for facilitating the flow of ES and talking to the Event Store.
What’s different for each Command is how validation would be carried out and how it’d be applied.
I abstract this into a common. The CommandHandler
interface deals with validation and applying commands. Each Command
would have a corresponding CommandHandler
When a Command
comes to EventStoreService.
- it’d first call the corresponding
CommandHandler
to get the Command validated. - If valid, save it in the event store.
- Then,
CommandHandler
would apply the Command again.
Step 2: Making a simple event store
Consider the table below as an event store.
UserId | StreamId | StreamVersion | EntityType | EventName | Command |
---|---|---|---|---|---|
1 | 1 | 1 | Movie | MovieCreated | {movieName: “James Bond”, movieId: “7”} |
1 | 1 | 2 | Movie | MovieNameUpdated | {movieName: “Skyfall”} |
1 | 2 | 1 | Movie | MovieCreated | {movieName: “Top Gun”, movieId: “8”} |
2 | 1 | 3 | Movie | MovieReleaseDateUpdated | {releaseDate: 2012} |
This includes all the information we need related to the MovieObject.
We also track which user is updating. We also keep different Entity types. For example, in a movie booking system, we may have entities such as Movie
, Theatre
, Booking
, etc.
The EventName
can be used to show a rich activity log or in analytics.
I’ll describe how we could enhance data in the event store for better analytics and debugging.
:todo: link to an article related to the event store.
Next, I will describe how I built the CommandHandler
.
Step 3: Building CommandHandler interface and implementations
I created two things that would help me.
Command: This would be an interface for every Command
CommandHandler: This would be an interface for CommandHandlers.
CommandHandler has two roles: validation and applying.
This is how CommandHandler
looked.
interface <T extends Command, Entity extends EventSourcingEntity> CommandHandler{
void validateCommand(ESDetails esDetails, Optional<Entity> entity, T command);
Entity applyCommand(Event event, Optional<Entity> entity);
}
Note: As described in the naming convention article: todo add link: initially, the methods had different names. isValid
and generate
. Over the iterations of coming up with good variable names and discussion with a colleague, I came to the above as it describes the best intent, validateCommand
and applyCommand
.
For example, consider a UpdateMovieNameCommand
@AllArgsContructor
@Data
class UpdateMovieNameCommand extends Command {
private final String movieName;
}
UpdateMovieNameCommandHandler.java
class UpdateMovieNameHandler extends CommandHandler<UpdateMovieNameCommand, Movie> {
void validateCommand(ESDetails esDetails, Optional<Movie> movie, UpdateMovieNameCommand command){
Movie movie = movie.getOrThrow();
if(!movie.isReleased()){
throw new CommandException("A Released Movie's Name cannot be updated");
}
if(StringUtils.isBlank(command.getMovieName())){
throw new CommandException("Movie Name cannot be blank");
}
}
Movie applyCommand(Event event, Optional<Movie> movie, UpdateMovieNameCommand command){
movie.get().setMovieName(command.getMovieName())
}
}
Note that all validations use the validateCommand
method, and none use the applyCommand
method. The Rules of Event Sourcing blog post will discuss this in detail. :todo: attach
Here’s how the Command would run
@Autowired
MovieService movieService;
void updateMovieCommandTest(){
// given: a movie in the event store
CreateMovieCommand createMovieCommand = new CreateMovieCommand(
7, "James Bond"
);
EventSourcingDetails eventSourcingDetails = EventSourcingDetails.builder()
.userId("thor-odinson")
.build();
Event event = movieService.put(createMovieCommand); // put movie in store
// when
UpdateMovieNameCommand updateMovieNameCommand = new UpdateMovieNameCommand("Skyfall");
movieService.put(updateMovieNameCommand);
// then
Movie movie = movieService.getOneFromStore(event.getStreamId());
assertThat(movie.getMovieName())
.isEqualTo(updateMovieNameCommand.getMovieName())
}
Step 4: Building EventSourcingService
The EventSourcingService is the one which manages the event store and calls the appropriate CommandHandler.
Here’s my implementation of the event store.
abstract <Entity extends EventSourcingEntity, T extends Command> class EventStore<Entity> {
Event putCommand(EventSourcingDetails eventSourcingDetails, T command);
Event canPutCommand(EventSourcingDetails eventSourcingDetails, T command)
Entity getOneFromStore(String streamId);
Entity getAllFromStore(String parentId);
Entity generateSnapshot(String streamId);
Entity generateAllSnapshot(String parentId);
}
Note: I have a parentId
in getAllFromStore
and generateAllSnapshot
. This is because I had an embedded object relationship in the code, which was mapped using parentId. For this event, the store would have one more column, parentId
, along with streamId
Here’s a description of what each method does.
putCommand
- Call the appropriate command handler and call it the
validateCommand
method. - If it’s valid, generate an event and save it in the event store table.
- Creation commands like
CreateMovieCommand
will not require data from the Event Store because the Movie hasn’t been added yet. - Updation commands like
UpdateMovieNameCommand
might require theMovie
entity to perform validation. EvenDeleteMovieCommand
would require aMovie
entity. For this,putCommand
can use thegetOneFromStore
method to get an entity from the store.
- Call the appropriate command handler and call it the
canPutCommand
- call the appropriate
CommandHandler
and call itsvalidateCommand.
- This is just used to check if Command can be put. This is useful when providing validations on the front end.
- call the appropriate
getOneFromStore
- iterate through event store events for the particular
streamId
and call the Command’sapplyCommand
method. - The output of each
applyCommand
method is an entity. This output entity is passed as input to the next event’s CommandHandler in a chain fashion. - To save time, check Snapshot to see if the entity exists; if it does, check its streamVersion and get events where streamVersion is greater than the snapshot streamVersion from the event store.
- iterate through event store events for the particular
getAllFromStore
- get all streamIds for a particular parentId from the event store.
- repeatedly call
getOneFromStore
generateSnapshot
getOneFromStore
and save it into the Snapshot repo.
generateSnapshots
gellAllFromStore
and then bulk save into the Snapshot repo.
Step 5: Calling the appropriate handler
In the putCommand
and getOneFromStore
sections, we must find a command’s appropriate CommandHandler
. This can be quickly done in Spring Boot by finding a bean for a generic type.
Source https://stackoverflow.com/a/69813143
@Autowired
ApplicationContext applicationContext;
public static <T> CommandHandler<T> getCommandHandler(Class<Entity> entityClass, Class<T> commandHandlerClass){
ResolvableType type = ResolvableType.forClassWithGenerics(CommandHandler.class, entityClass.getClass(), commandHandlerClass.getClass());
return (CommandHandler<T>) context.getBeanProvider(type).getObject();
}
Conclusion
Like any design pattern, it may take time first to understand all the pieces of Event Sourcing. But once you get a mental picture of it. You can create it fast!
Once the framework has been created, it rarely gets modified as much. The framework remains the same, and the functionality of what you want to do gets extended via the Command and Command Handlers you create. (Open Close Principle).
Once you have created a framework like the above, you only need to create a bunch of Commands and CommandHandlers to handle them. There is no need ever to touch the Event Sourcing base code. It’ll just work. Read my article on how you can decide what commands to create.
Also, there are specific rules to follow when developing event sourcing. Which I’ll share in an upcoming post. (TODO)
How did you implement your Event Sourcing?
Next
There are certain parts of ES that I’d describe later in different blogposts (TODO)
- Read Rules of Event Sourcing (TODO)
- Why I love event sourcing. (TODO)
- Unit Testing Event Sourcing (TODO)
- Chained Commands (Similar to the concept of Redux Thunks)
- Combine Commands
- Enhancing data in the event store
- Resourcing for learning Event Soucing and Message Driven Architecture.
- How do we implement REST over Event Store? (Including Benefits)