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.

  1. A module can access the content of any other module but can't access sub-packages of other modules.
  2. 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

https://github.com/xmolecules/jmolecules

https://www.youtube.com/watch?v=Pae2D4XcEIg

comments powered by Disqus