Event Sourcing with Spring Modulith
Introduction
In the ever-evolving application development landscape, a concept redefines how we build modular applications with the Spring Framework.
At its core, Spring Modulith strongly advocates for a modular structure for our applications. The framework enforces the division of applications into distinct, loosely coupled modules. These modules have clearly defined responsibilities and well-structured boundaries.
The implications of this approach are profound. By enforcing the isolation of modules and promoting a clear separation of concerns, Spring Modulith helps to create maintainable, scalable, and testable applications. The complexities of big balls of mud that often plague developers are prevented right from the start, thanks to the independence and autonomy of each module. This not only facilitates seamless evolution and testing but also culminates in the creation of cohesive and flexible applications.
By default, Modules continue communicating directly with each other synchronously through events. Let’s take Spring Modulith a step further by embracing the concept of event sourcing, thereby enforcing decoupling even further.
Before we dive into the practical aspects, let’s take a moment to grasp the essence of event sourcing.
Event Sourcing
At its core, Event Sourcing flips the traditional approach to data management, opening up new possibilities for robustness, traceability, and scalability.
Imagine if your application’s data were treated as a sequence of meaningful events, like plot points in a captivating story. Rather than storing only the current state of the application, Event Sourcing captures every individual action or decision as an immutable event in a historical log. This shift in mindset holds profound implications for application development.
Developers gain insight into the application’s evolution by preserving a detailed record of events. Bugs become easier to understand and fix since they are woven into the narrative of events. This historical log also offers resilience against data loss, as the entire history can be replayed to reconstruct states at any time.
The scalability of your application greatly profits from event sourcing. The sequence of events can be processed and analyzed in parallel, facilitating efficient handling of large workloads. This opens doors to crafting applications that seamlessly accommodate growing user bases and demanding operational needs. This is a good starting point if you want to dive deeper into event sourcing.
Event sourcing implementation
After understanding event sourcing in theory, we must find a way to implement it in Spring Modulith best.
Among others, PostgreSQL and EventStore offer options for implementing event sourcing in Spring, each with advantages and drawbacks.
PostgreSQL is especially suitable for applications where familiarity with relational databases is crucial, and the complexity of the data model is relatively low. Chris Richardson successfully used it for event sourcing in his distributed data management solution eventuate.
However, EventStore excels in scenarios where performance, scalability, and advanced query capabilities are key concerns. Still, it might require a steeper learning curve and offer less mature integration with the Spring framework.
PostgreSQL is a type of relational database, while EventStore serves as a messaging solution, as well. Having EventStore can be useful if we plan to divide our modulith in the future, as it eliminates the need to figure out how to manage messaging.
The choice between these options depends on the specific requirements and trade-offs of your project.
In the following hands-on session, I decided to use PostgreSQL, since I believe that most people reading this are more familiar with PostgreSQL, and I did not want to add an additional layer of complexity.
Hands-on
Let’s implement a simple Webshop where products can be ordered and shipped, and the inventory, containing our stored products, is updated accordingly. The source code can be found here.
When a new Order is created or canceled (1), we want to update our inventory accordingly (2) and initiate or stop shipment (3). Additionally, we have to update our inventory when new items are added (4).
Set up
First of all, we need to set up our Spring Modulith project. We can do this with spring initializr or add the spring-modulith-bom to an existing spring project.
Create application modules
The main concept of Spring Modulith is the application module. An application module is a unit of functionality that exposes an API to other modules. It has some internal implementations that aren’t supposed to be accessed by other modules. When we design our application, we consider an application module for each subdomain.
Spring Modulith provides different ways of expressing modules. We can consider our application’s domain or business modules as direct sub-packages of the application’s main package.
We have four application modules:
- order
- shipment
- inventory
- eventsourcing
After setting up the modules, our structure looks like this:
1. Module encapsulation
Encapsulation is key to managing the scalability, maintainability, and testability of our application. We don’t want our modules to communicate with each other directly, and we want to take it a step further by ensuring that they’re not even aware of each other’s existence. This approach allows us to avoid complex dependencies and identify issues in our modules with ease.
While order, shipment, and inventory modules operate independently, there is one module that they all need to be aware of: the eventsourcing module. We can use the @ApplicationModule annotation in the package-info.java type to declare the allowed dependencies of each module:
This ensures that we can keep tight control over the dependencies of our application and maintain the level of encapsulation necessary for a robust, scalable system.
2. PostgreSQL setup
Next, we need to set up PostgreSQL.
- First, we have to add the necessary dependencies:
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.27</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.27</version>
</dependency>
- Second, we have to configure the connection in our application.yml
3. Event sourcing logic
Now that we have a working database connection, we can add the evensourcing logic to our application.
4. Define Event Entity
Before we can track changes in our application, we need to define the event that serves as a starting point and represents an entity that is stored in the database. Retrieving events in the order they were fired is important, so we require a timestamp. We’ve added an automatically generated UUID to ensure that each event is unique.
However, having a timestamp and UUID alone is insufficient to track application changes or monitor inventory updates. For instance, if we add or remove products from the inventory, we need a comprehensive set of events that contain relevant data for each action.
For instance, an inventory event would need to contain product types, quantities, and changes in inventory levels. By including such data in the event, we can track important changes in our application and keep our inventory system up to date.
5. Add Event Repository
To simplify communicating with the database, we need to add a repository. Additionally, let’s include a method that helps to retrieve events in the correct order. This will allow us to compose the application state from previous events effectively.
6. Implement Event Service
To keep our application running smoothly, we must retrieve the current application state when it first starts up. To accomplish this, we’ve added an EventService that is responsible for adding events to the database through the repository, as well as replaying events when the application starts to initialize the cache of the modules.
To publish events, we utilize the ApplicationEventPublisher that comes with the Spring Modulith module by default. With this added functionality, our application services can reliably publish events and keep the data in our database up to date.
7. Add Event Listener
To keep our data up to date, we must listen to published events and update the current state whenever an event is fired, regardless of whether these changes happen at startup or during runtime. To accomplish this, we’ve included event handlers in the modules that need to listen for specific events.
We are able to listen to events using the Spring Modulith application module listener, which makes it simple to add event handlers. To ensure that the communication remains asynchronous, even with the @Async annotation on the listener, we must add @EnableAsync to our main class.
Summary & Next Steps
We implemented event sourcing in a Spring Modulith application to facilitate communication between services. By using an event service, we ensure that the modules interact with each other through a single mediator. This way, we can easily scale the modules independently, if required.
Presently, we’re storing all events in the database, and it works well enough for our small-scale application. This would not work in a microservice architecture since we did not want to share a database between services. In such a case, we needed to implement some messaging mechanisms.
Additionally, if our application grew larger and we needed to be able to handle millions of events, constantly writing to the database would cause issues.
As our user base grows, we need to plan to store only relevant events.
Snapshots are a great mechanism that we can leverage to optimize event storage (check this article by EventStore).
All our modules are only aware of the EventModule. So If we want to outsource a module into a single deployable, we need to listen directly to the database (you can find out more about directly listening to events from PostgreSQL here).
Utilizing event sourcing aligns well with the concept of Spring Modulith for constructing applications with decoupled, cohesive modules. I firmly believe that integrating these two approaches is a solid foundation, helping avoid a chaotic codebase over time, especially amidst tight deadlines. Implementing features like the application module listener significantly streamlines the initial setup process. Leveraging Spring Modulith inherent logic, such as the application module listener, simplifies the initial setup process.
If you have feedback or questions or are interested in exchanging ideas, please feel free to contact me. I’m always excited to hear from you and engage in meaningful discussions.