--- title: Implementing CQRS with Spring Modulith image: /assets/belgium_canal.JPG layout: post date: 2025-03-17 20:00:00 +0100 categories: - Java - Design-Patterns - SpringBoot subtitle: Spring Modulith provides excellent support for implementing CQRS excerpt: In modern enterprise applications, separating read and write concerns often becomes essential as systems grow in complexity. The Command Query Responsibility Segregation (CQRS) pattern addresses this need. --- In modern enterprise applications, separating read and write concerns often becomes essential as systems grow in complexity. The Command Query Responsibility Segregation (CQRS) pattern addresses this need. When implemented well, this separation can dramatically improve scalability, performance, and maintainability. **[Spring Modulith](https://spring.io/projects/spring-modulith)**, a relatively new addition to the Spring ecosystem, **provides excellent support for implementing CQRS** in a clean, modular way. In this example, we are going to implement a simple application that handles a Product Catalog. Let's dive right in! {% include image.html src="/assets/belgium_canal.JPG" alt="A canal in Amsterdam" caption="A canal in Amsterdam (2018)" %} ## 1. Project Structure We need two main modules, one for reads named `query` and the other for writes named `command`. Each module will be connected to a different database. (Note: It's not a prerequisite of CQRS using different databases for read and writes - we could have used a different schema or even just a different table for simplest cases) The folder structure of our application will look like this: ```text gae.piaz.modulith.cqrs/ ├── command/ │ ├── api/ │ ├── config/ │ ├── domain/ │ ├── events/ │ │ ├── ProductCreatedEvent.java │ │ ├── ProductUpdatedEvent.java │ │ ├── ProductReviewEvent.java │ │ └── package-info.java │ ├── service/ │ └── package-info.java ├── query/ │ ├── api/ │ ├── application/ │ ├── config/ │ ├── domain/ │ └── package-info.java ├── shared/ │ ├── config/ │ └── package-info.java └── CQRSApplication.java ``` - The `query` folder contains a CLOSED module with `reads` business logic - The `command` folder contains a CLOSED module with `writes` business logic - The `shared` folder contains an OPEN module, with shared configuration We need to consider CLOSED modules as completely decoupled applications. This means that it should not be possible for the `command` module to access any class of the `query` module and vice-versa. The only point of contact is the classes inside the `command/event` folder. We can explicit the type of module, what to expose, and what not, using the **package-info.java** file in each folder: ```java @ApplicationModule(type = ApplicationModule.Type.CLOSED) package gae.piaz.modulith.cqrs.query; import org.springframework.modulith.ApplicationModule; ``` ```java @ApplicationModule(type = ApplicationModule.Type.CLOSED) package gae.piaz.modulith.cqrs.command; import org.springframework.modulith.ApplicationModule; ``` ```java @ApplicationModule(type = ApplicationModule.Type.OPEN) package gae.piaz.modulith.cqrs.shared; import org.springframework.modulith.ApplicationModule; ``` ```java @NamedInterface("Events") package gae.piaz.modulith.cqrs.command.events; import org.springframework.modulith.NamedInterface; ``` The structure is well summarized by the UML generated automatically by the [Modulith Documenter](https://github.com/GaetanoPiazzolla/cqrs-spring-modulith/blob/master/src/test/java/gae/piaz/modulith/cqrs/ModulithStructureTest.java): {% include image.html src="/assets/cqrs-components.svg" alt="CQRS Component as generated by Modulith" caption="CQRS Components UML generated by Modulith" %} ## 2. Basic Application Config We'll now set up the basic configurations and scaffolding of the Application. ### 2.1. Dependencies To implement CQRS with Spring Modulith, we need the following dependencies: ```kotlin implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.modulith:spring-modulith-starter-core") implementation("org.springframework.modulith:spring-modulith-events-api") implementation("org.springframework.modulith:spring-modulith-starter-jpa") ``` These dependencies provide the standard JPA support and Web Capabilities for Rest Endpoints, other than Core Spring Modulith functionality. ### 2.2. Enabling Modulith The main SpringBoot Application is decorated with the [@Modulithic](https://docs.spring.io/spring-modulith/docs/current/api/org/springframework/modulith/Modulithic.html) annotation to enable the Modulith structuring conventions: ```java import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.modulith.core.ApplicationModules; @SpringBootApplication @Modulithic public class CQRSApplication { public static void main(String[] args) { SpringApplication.run(CQRSApplication.class, args); } } ``` ### 2.3. Databases Configurations The shared module configuration [here](https://github.com/GaetanoPiazzolla/cqrs-spring-modulith/blob/master/src/main/java/gae/piaz/modulith/cqrs/shared/config/CqrsConfig.java) sets up two separate data sources and entity managers, one for each module. This separation allows us to optimize each database for its specific purpose. In the application.yaml we can define the connection terminals: ```yaml spring: application: name: modulith.cqrs datasource: command: jdbc-url: jdbc:h2:mem:commanddb username: sa password: driver-class-name: org.h2.Driver query: jdbc-url: jdbc:h2:mem:querydb username: sa password: driver-class-name: org.h2.Driver ``` We then need separate JPA repository configurations to ensure each module uses its own transaction manager: ```java package gae.piaz.modulith.cqrs.query.config; @Configuration @EnableTransactionManagement @EnableJpaRepositories( basePackages = "gae.piaz.modulith.cqrs.query.domain", entityManagerFactoryRef = "queryEntityManagerFactory", transactionManagerRef = "queryTransactionManager" ) public class QueryJpaConfig { } ``` ```java package gae.piaz.modulith.cqrs.command.config; @Configuration @EnableTransactionManagement @EnableJpaRepositories( basePackages = {"gae.piaz.modulith.cqrs.command.domain", "org.springframework.modulith.events.jpa"}, entityManagerFactoryRef = "entityManagerFactory", transactionManagerRef = "transactionManager" ) public class CommandJpaConfig { } ``` As you can see, in the `CommandJpaConfig` we have also added the _org.springframework.modulith.events.jpa_ package. **This is to tell Spring to use the same transaction manager of the command module when handling the persistence of EVENTS.** More details about this in the next chapter. ### 2.4. Event Persistence and Retry Logic Spring Modulith provides robust event persistence through its JPA module. It's enough to include the `spring-modulith-starter-jpa` library, and a simple configuration: ```yaml modulith: events: completion-mode: archive republish-outstanding-events-on-restart: true ``` This configuration creates a table to store the events and retry them if the application crashes. The `archive` completion mode ensures that events are stored in the database after they are processed, providing an audit trail of all domain events. When the application restarts, any events that weren't successfully processed will be republished automatically, ensuring data consistency between the command and query sides. In the following image, we can see a series of ProductCreatedEvent that are stored in the `EVENT_PUBBLICATION_ARCHIVE` table in the `command` database after being correctly received by the `query` listener: {% include image.html src="/assets/event_pubblication_cqrs.png" alt="H2 console showing Events persisted" caption="H2 Console with EVENT_PUBBLICATION table" %} ## 3. Implementation Let's now first define the Command and Query domains and the business logic of our application. ### 3.1. Domain Entities Let's now define domain entities for the Command module. The `Product` Command domain object is optimized for write operations, and includes a `OneToMany` relationship with the `Reviews` Object: ```java package gae.piaz.modulith.cqrs.command.domain; @Entity @Table(name = "product") public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String description; private BigDecimal price; private Integer stock; private String category; @OneToMany(mappedBy = "product") private List reviews = new ArrayList<>(); } @Entity @Table(name = "review") @Getter @Setter @NoArgsConstructor public class Review { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Integer vote; private String comment; } ``` The Query domain object instead is optimized for Read operations, hence it contains denormalized data in order to make specific reads easier: ```java package gae.piaz.modulith.cqrs.command.query; @Entity @Table(name = "product_views") @Getter @Setter @NoArgsConstructor public class ProductView { // Replicates Command fields @Id private Long id; private String name; private String description; private BigDecimal price; private Integer stock; private String category; // Specific to Query (Denormalized data) @Column(name = "average_rating") private Double averageRating = 0.0; @Column(name = "review_count") private Integer reviewCount = 0; } ``` So we have included the `reviewCount` and the `averageRating` because we want to fetch products based on the review votes that it has received. ### 3.2. Event Records The Event records are simple Java records that we need to use to keep the `ProductView` updated: ```java package gae.piaz.modulith.cqrs.command.events; public record ProductCreatedEvent(Long id, String name, String description, BigDecimal price, Integer stock, String category) { } public record ProductUpdatedEvent(Long id, String name, String description, BigDecimal price, Integer stock, String category) { } public record ProductReviewEvent(Long productId, Long reviewId, Integer vote, String comment) { } ``` We need to update the Query `ProductView` domain object in 3 cases: when the product is created, when it's updated, and when we receive a new review. Let's now see how to trigger those events from the Command module. ### 3.3. Command API and Service The APIs of the command module are pretty straightforward, you can check them [here](https://github.com/GaetanoPiazzolla/cqrs-spring-modulith/blob/master/src/main/java/gae/piaz/modulith/cqrs/command/api/ProductCommandController.java). It's the `ProductCommandService` where the magic happens and the interesting bit. In here, we'll rely on the Spring [ApplicationEventPublisher](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/ApplicationEventPublisher.html) bean to trigger the events: ```java package gae.piaz.modulith.cqrs.command.service; @Service @Transactional @AllArgsConstructor public class ProductCommandService { private final ProductRepository productRepository; private final ReviewRepository reviewRepository; private final ApplicationEventPublisher eventPublisher; public Long createProduct(...) { Product product = ... Product saved = productRepository.save(product); eventPublisher.publishEvent(new ProductCreatedEvent(...)); return saved.getId(); } public void updateProduct(...) { Product product = ... productRepository.save(product); eventPublisher.publishEvent(new ProductUpdatedEvent(...)); } public Long addReview(...) { Review review = ... Review saved = reviewRepository.save(review); eventPublisher.publishEvent(new ProductReviewEvent(...); return saved.getId(); } } ``` Note: It's also possible to leverage the entity lifecycle to trigger the events using `@PostUpdate`, `@PostInsert` etc, as we have shown in this article: [Event Notification Pattern with Spring Data](https://gaetanopiazzolla.github.io/spring/design-patterns/2024/12/15/event-spring.html) ### 3.4. Query Event Handler The `ProductEventHandler` will then receive the events and update the ProductView accordingly: ```java package gae.piaz.modulith.cqrs.query.service; @Service @AllArgsConstructor public class ProductEventHandler { private final ProductViewRepository viewRepository; @ApplicationModuleListener public void on(ProductCreatedEvent event) { ProductView view = ... viewRepository.save(view); } @ApplicationModuleListener public void on(ProductUpdatedEvent event) { viewRepository.findById(event.id()).ifPresent(view -> { ... viewRepository.save(view); }); } @ApplicationModuleListener public void on(ProductReviewEvent event) { viewRepository.findById(event.productId()).ifPresent(view -> { double currentTotal = view.getAverageRating() * view.getReviewCount(); int newCount = view.getReviewCount() + 1; double newAverage = (currentTotal + event.vote()) / newCount; view.setReviewCount(newCount); view.setAverageRating(newAverage); viewRepository.save(view); }); } } ``` The annotation [@ApplicationModuleListener](https://docs.spring.io/spring-modulith/docs/current/api/org/springframework/modulith/ApplicationModuleListener.html "annotation interface in org.springframework.modulith"): "_...specifies a [`Async`](https://docs.spring.io/spring/docs/6.2.3/javadoc-api/org/springframework/scheduling/annotation/Async.html "class or interface in org.springframework.scheduling.annotation") Spring [`TransactionalEventListener`](https://docs.spring.io/spring/docs/6.2.3/javadoc-api/org/springframework/transaction/event/TransactionalEventListener.html "class or interface in org.springframework.transaction.event") that runs in a transaction itself. Thus, the annotation serves as syntactic sugar for the generally recommend setup to integrate application modules via events. The setup makes sure that an original business transaction completes successfully and the integration asynchronously runs in a transaction itself to decouple the integration as much as possible from the original unit of work._" In practice, this annotation includes: [@Async](https://docs.spring.io/spring/docs/6.2.3/javadoc-api/org/springframework/scheduling/annotation/Async.html "class or interface in org.springframework.scheduling.annotation"),[@Transactional](https://docs.spring.io/spring/docs/6.2.3/javadoc-api/org/springframework/transaction/annotation/Transactional.html "class or interface in org.springframework.transaction.annotation"), and [@TransactionalEventListener](https://docs.spring.io/spring/docs/6.2.3/javadoc-api/org/springframework/transaction/event/TransactionalEventListener.html "class or interface in org.springframework.transaction.event"). More on the Transactional behavior of this solution in the following chapters. ### 3.5. Query API and Service The Query Controller and Service, include the trivial code to fetch Products ordered by the average rating: ```java @RestController @RequestMapping("/api/products/queries") @AllArgsConstructor public class ProductQueryController { private final ProductQueryService queryService; ... @GetMapping("/by-rating") public List getProductsByRating() { return queryService.findAllOrderByRating(); } } @Service @Transactional(readOnly = true) public class ProductQueryService { private final ProductViewRepository viewRepository; ... public List findAllOrderByRating() { return viewRepository.findAllOrderByRatingDesc(); } } @Repository public interface ProductViewRepository extends JpaRepository { ... @Query("SELECT pv FROM ProductView pv WHERE pv.reviewCount > 0 ORDER BY pv.averageRating DESC") List findAllOrderByRatingDesc(); } ``` The denormalized data makes us able to fetch products ordered by the average rating in a simple and convenient way, without doing any joins with the `Review` entity. ## 4. Testing Spring Modulith provides also some good utilities to test event publishing and modules external communication. It's enough to annotate the test with [@ApplicationModuleTest](https://docs.spring.io/spring-modulith/docs/current/api/org/springframework/modulith/test/ApplicationModuleTest.html), and include the [PublishedEvents](https://docs.spring.io/spring-modulith/docs/current/api/org/springframework/modulith/test/PublishedEvents.html) bean as a method argument: ```java @ApplicationModuleTest class CommandModuleIntegrationTest { @Autowired private ProductCommandService commandService; @Autowired private ProductRepository productRepository; @Test void whenAddingReview_eventIsSent(PublishedEvents events) { // Given Product product = ... product = productRepository.save(product); // When Long productId = commandService.addReview(product.getId(), 5, "Test Review"); // Then PublishedEvents.TypedPublishedEvents matchingMapped = events.ofType(ProductReviewEvent.class); Iterator eventIterator = matchingMapped.iterator(); ProductReviewEvent event = eventIterator.next(); Assertions.assertEquals(productId, event.productId()); ... } } ``` ## 5. Transactional Behavior One of the key benefits of the CQRS pattern with Spring Modulith is the transaction isolation between modules. If an exception occurs in the query module while processing an event, it won't affect the command transaction that triggered the event. For example, if a `ProductCreatedEvent` is published after successfully saving a product in the command module, but the query module fails to process this event (perhaps due to a validation error or database issue), the original command transaction will still be committed. The product will be created in the command database, and Spring Modulith will handle the retry of the event processing. This behavior is particularly valuable in high-throughput systems where you don't want read-side issues to impact write operations. ## 6. Conclusion In this article, we've seen how to implement **CQRS with Spring Modulith**. This approach offers several advantages: 1. **Clear Separation of Concerns**: The command and query responsibilities are cleanly separated into different modules. The Modulith test ensures that we stay within the boundaries 2. **Independent Scalability**: Each module can be optimized and scaled according to its specific needs. The events can be [externalized](https://spring.io/blog/2023/09/22/simplified-event-externalization-with-spring-modulith) automagically to an event broker like Kafka 3. **Resilience**: Issues in one module don't directly impact the other, improving overall system stability 4. **Event Auditing and Retry:** Modulith automatically includes the functionality of [event persistence](https://docs.spring.io/spring-modulith/reference/events.html#publication-registry.starters) and automatic retry for failed events 5. **Easy Testing:** cool utilities make the testing of events communication trivial (which is not always the case) 6. **Optimized Data Models**: Each side can use data models optimized for its specific use cases 7. **Event-Driven Architecture**: The use of events for communication promotes loose coupling and extensibility As always, all the code is available for you to copy/paste/critique as you like: [https://github.com/GaetanoPiazzolla/cqrs-spring-modulith/](https://github.com/GaetanoPiazzolla/cqrs-spring-modulith/) Thank you very much for reading! ## 7. Update: A Better Approach In this [repository](https://github.com/odrotbohm/cqrs-spring-modulith), Oliver Drotbohm, the lead of SpringModulith, took the opportunity to refactor the sample application from this blog post. **A key flaw in my initial approach was that I treated the module primarily as a technical and architectural construct rather than using it to encapsulate business capabilities, as it should be. CQRS is a technical concern and should be considered an implementation detail within a module, rather than defining its boundaries.** He restructured the application to align with SpringModulith’s idiomatic code organization while demonstrating how to implement CQRS effectively WITHIN a module. As part of this refactor, he introduced jMolecules to improve context separation and domain modeling. Additionally, he streamlined the architecture to use a single datasource, reducing complexity. However, while this approach simplifies the setup, and mitigates some inefficiencies, I believe that maintaining separate databases remains a strong choice for CQRS-based applications.