Spring Modulith - Events
Overview
Spring boot modulith implementation with spring events & persistence with postgres & replay of events.
Github: https://github.com/gitorko/project73
Spring Modulith
Modular Monolith is an architectural style where source code is structured on the concept of modules

Spring Modulith is a module of Spring that helps in organizing large applications into well-structured, manageable, and self-contained modules. It provides various features like module isolation, events, and monitoring to support a modular architecture.
Building a Modern Monolith application, with Spring Modulith lets you avoid the network jumps, serialization & de-serialization. Each service is isolated via package boundary. Eg: OrderService, NotificationService bean won't be injected in all the classes, instead they rely on spring event bus to communicate with each other.
You can structure your code based on domain, Order package deals only with processing the order, notification package deals only with sending notifications etc. We can split the core of the monolith into modules by identifying the domains of our application and defining bounded contexts. We can consider the domain or business modules of our application as direct sub-packages of the application’s main package.
Since the application becomes a monolith, you cant individually scale out individual services, so if a particular service needs more scale you can move only that module to a separate service (microservice architecture).
Spring events ensures loose coupling in an application, it allows inter-module interaction. Instead of injecting different beans and invoking them in the directly you now publish an event and all other places that need to process it will implement a listener.
Tightly coupled with single commit transaction boundary
1@Transactional 2public void complete(Order order) { 3 orderService.save(order); 4 inventoryService.update(order); 5 auditService.add(order); 6 rewardService.update(order); 7 notificationService.update(order); 8} Loosely coupled but still single commit transaction boundary
1@Transactional 2public void complete(Order order) { 3 applicationEventPublisher.publishEvent(order); 4} Service can be developed without all the implementations. Eg: Audit logging service is being developed and not ready, hence instead of being blocked on developing the core customer service class, just publish an event and when the service is ready add a listener to process that event.
Spring events are in-memory so if the server restarts all events published will be lost. With Spring Modulith library you can now persist such event and process them after a restart.
- A module can access the content of any other module but can't access sub-packages of other modules.
- A module also cant access content that is not public
By default @EventListener run on the same thread as the caller, to run it asynchronously use @Async
@ApplicationModuleListener by default comes with @Transactional, @Async & @TransactionalEventListener annotation enabled.
@Externalized will publish the events to queues like RabbitMQ/Kafka.
To process the events on restart enable this flag.
1spring: 2 modulith: 3 republish-outstanding-events-on-restart: true 

Code
1package com.demo.project73.common; 2 3import lombok.extern.slf4j.Slf4j; 4import org.springframework.boot.context.event.ApplicationReadyEvent; 5import org.springframework.context.event.EventListener; 6import org.springframework.stereotype.Service; 7 8@Service 9@Slf4j 10public class ApplicationEventListener { 11 /** 12 * ApplicationStartingEvent - fired at the start of a run but before any processing 13 * ApplicationEnvironmentPreparedEvent - fired when the Environment to be used in the context is available 14 * ApplicationContextInitializedEvent- fired when the ApplicationContext is ready 15 * ApplicationPreparedEvent - fired when ApplicationContext is prepared but not refreshed 16 * ContextRefreshedEvent - fired when an ApplicationContext is refreshed 17 * WebServerInitializedEvent - fired after the web server is ready 18 * ApplicationStartedEvent - fired after the context has been refreshed but before any application and command-line runners have been called 19 * ApplicationReadyEvent - fired to indicate that the application is ready to service 20 * ApplicationFailedEvent - fired if there is an exception and the application fails to start 21 */ 22 @EventListener(ApplicationReadyEvent.class) 23 public void onStart() { 24 log.info("Triggered when application ready!"); 25 } 26} 1package com.demo.project73.audit.internal.listener; 2 3import com.demo.project73.common.OrderEvent; 4import lombok.extern.slf4j.Slf4j; 5import org.springframework.modulith.events.ApplicationModuleListener; 6import org.springframework.stereotype.Component; 7 8@Component 9@Slf4j 10public class AuditEventListener { 11 12 @ApplicationModuleListener 13 public void processOrderEvent(OrderEvent orderEvent) { 14 log.info("[Audit] Order Event Received: {}", orderEvent); 15 } 16 17} 1package com.demo.project73.order.internal.service; 2 3import java.util.UUID; 4 5import com.demo.project73.common.CustomEvent; 6import com.demo.project73.common.PrimeReward; 7import com.demo.project73.common.SeasonReward; 8import com.demo.project73.order.internal.domain.Order; 9import com.demo.project73.common.OrderEvent; 10import lombok.RequiredArgsConstructor; 11import lombok.extern.slf4j.Slf4j; 12import org.springframework.context.ApplicationEventPublisher; 13import org.springframework.stereotype.Service; 14import org.springframework.transaction.annotation.Transactional; 15 16@Service 17@Slf4j 18@RequiredArgsConstructor 19public class OrderService { 20 21 final ApplicationEventPublisher applicationEventPublisher; 22 23 /** 24 * @ApplicationModuleListener need a transactional boundary else won't run. 25 */ 26 @Transactional 27 public Order placeOrder(Order order) { 28 for (String item : order.getItems()) { 29 OrderEvent orderEvent = OrderEvent.builder() 30 .orderId(order.getOrderId()) 31 .item(item) 32 .orderDate(order.getOrderDate()) 33 .build(); 34 log.info("Publishing Order: {}", orderEvent); 35 applicationEventPublisher.publishEvent(orderEvent); 36 } 37 38 PrimeReward coupon1 = PrimeReward.builder() 39 .id(UUID.randomUUID()) 40 .couponCode("coupon-code-" + UUID.randomUUID()) 41 .build(); 42 SeasonReward coupon2 = SeasonReward.builder() 43 .id(UUID.randomUUID()) 44 .couponCode("coupon-code-" + UUID.randomUUID()) 45 .build(); 46 CustomEvent<PrimeReward> customEvent1 = new CustomEvent(this, coupon1); 47 CustomEvent<SeasonReward> customEvent2 = new CustomEvent(this, coupon2); 48 log.info("Publishing CustomEvent: {}", customEvent1); 49 applicationEventPublisher.publishEvent(customEvent1); 50 log.info("Publishing CustomEvent: {}", customEvent2); 51 applicationEventPublisher.publishEvent(customEvent2); 52 return order; 53 } 54} 1package com.demo.project73.reward.internal.listener; 2 3import com.demo.project73.common.CustomEvent; 4import com.demo.project73.common.PrimeReward; 5import com.demo.project73.common.SeasonReward; 6import lombok.SneakyThrows; 7import lombok.extern.slf4j.Slf4j; 8import org.springframework.context.event.EventListener; 9import org.springframework.scheduling.annotation.Async; 10import org.springframework.stereotype.Component; 11import org.springframework.transaction.event.TransactionPhase; 12import org.springframework.transaction.event.TransactionalEventListener; 13 14@Component 15@Slf4j 16public class RewardListener { 17 /** 18 * Processes the custom event 19 */ 20 @Async 21 @SneakyThrows 22 @EventListener 23 public void processEvent(CustomEvent myEvent) { 24 log.info("Processing CustomEvent {}", myEvent); 25 if (myEvent.getEntity() instanceof PrimeReward) { 26 log.info("PrimeReward Event: {}", ((PrimeReward) myEvent.getEntity()).getCouponCode()); 27 } 28 if (myEvent.getEntity() instanceof SeasonReward) { 29 log.info("SeasonReward Event: {}", ((SeasonReward) myEvent.getEntity()).getCouponCode()); 30 } 31 } 32 33 /** 34 * AFTER_COMMIT: The event will be handled when the transaction gets committed successfully. 35 * AFTER_COMPLETION: The event will be handled when the transaction commits or is rolled back. 36 * AFTER_ROLLBACK: The event will be handled after the transaction has rolled back. 37 * BEFORE_COMMIT: The event will be handled before the transaction commit. 38 */ 39 @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) 40 void afterAuditEventProcessed(CustomEvent myEvent) { 41 log.info("After CustomEvent processed: {}", myEvent); 42 } 43} Setup
1# Project 73 2 3Spring Events 4 5[https://gitorko.github.io/spring-events/](https://gitorko.github.io/spring-events/) 6 7### Version 8 9Check version 10 11```bash 12$java --version 13openjdk 21.0.3 2024-04-16 LTS 14``` 15 16### Modulith Documentation 17 18```bash 19brew install graphviz 20``` 21 22### Postgres DB 23 24```bash 25docker run -p 5432:5432 --name pg-container -e POSTGRES_PASSWORD=password -d postgres:14 26docker ps 27docker exec -it pg-container psql -U postgres -W postgres 28CREATE USER test WITH PASSWORD 'test@123'; 29CREATE DATABASE "test-db" WITH OWNER "test" ENCODING UTF8 TEMPLATE template0; 30grant all PRIVILEGES ON DATABASE "test-db" to test; 31 32docker stop pg-container 33docker start pg-container 34``` 35 36### RabbitMQ 37 38Run the docker command to start a rabbitmq instance 39 40```bash 41docker run -d --hostname my-rabbit --name my-rabbit -e RABBITMQ_DEFAULT_USER=guest -e RABBITMQ_DEFAULT_PASS=guest -p 8085:15672 -p 5672:5672 rabbitmq:3-management 42``` 43 44Open the rabbitmq console 45 46[http://localhost:8085](http://localhost:8085) 47 48``` 49user:guest 50pwd: guest 51``` 52 53### Dev 54 55To run the code. 56 57```bash 58./gradlew clean build 59./gradlew bootRun 60``` References
https://spring.io/blog/2015/02/11/better-application-events-in-spring-framework-4-2
https://spring.io/projects/spring-modulith