diff --git a/spring-boot-modules/spring-boot-libraries-3/pom.xml b/spring-boot-modules/spring-boot-libraries-3/pom.xml
index 988ce0bafe..e88ae4c078 100644
--- a/spring-boot-modules/spring-boot-libraries-3/pom.xml
+++ b/spring-boot-modules/spring-boot-libraries-3/pom.xml
@@ -17,7 +17,6 @@
org.springframework.boot
spring-boot-starter-data-jpa
-
org.springframework.kafka
spring-kafka
@@ -28,20 +27,27 @@
postgresql
${postgresql.version}
+
org.springframework.modulith
spring-modulith-events-api
- ${spring-modulith-events-kafka.version}
+ ${spring-modulith.version}
org.springframework.modulith
spring-modulith-events-kafka
- ${spring-modulith-events-kafka.version}
+ ${spring-modulith.version}
org.springframework.modulith
spring-modulith-starter-jpa
- ${spring-modulith-events-kafka.version}
+ ${spring-modulith.version}
+
+
+ org.springframework.modulith
+ spring-modulith-starter-test
+ ${spring-modulith.version}
+ test
@@ -54,7 +60,6 @@
spring-boot-testcontainers
test
-
org.testcontainers
kafka
@@ -73,7 +78,6 @@
${testcontainers.version}
test
-
org.testcontainers
postgresql
@@ -87,16 +91,23 @@
${awaitility.version}
test
+
+ com.h2database
+ h2
+ ${h2.version}
+ test
+
17
3.1.5
- 1.1.2
+ 1.1.3
1.19.3
4.2.0
42.3.1
+ 2.2.224
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/application/events/orders/Order.java b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/application/events/orders/Order.java
new file mode 100644
index 0000000000..c448bd44dd
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/application/events/orders/Order.java
@@ -0,0 +1,12 @@
+package com.baeldung.springmodulith.application.events.orders;
+
+import java.time.Instant;
+import java.util.List;
+
+record Order(String id, String customerId, List productIds, Instant timestamp) {
+
+ public Order(String customerId, List productIds) {
+ this(null, customerId, productIds, Instant.now());
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/application/events/orders/OrderCompletedEvent.java b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/application/events/orders/OrderCompletedEvent.java
new file mode 100644
index 0000000000..4344b336ac
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/application/events/orders/OrderCompletedEvent.java
@@ -0,0 +1,6 @@
+package com.baeldung.springmodulith.application.events.orders;
+
+import java.time.Instant;
+
+public record OrderCompletedEvent(String orderId, String customerId, Instant timestamp) {
+}
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/application/events/orders/OrderRepository.java b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/application/events/orders/OrderRepository.java
new file mode 100644
index 0000000000..7c159e3582
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/application/events/orders/OrderRepository.java
@@ -0,0 +1,27 @@
+package com.baeldung.springmodulith.application.events.orders;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.springframework.stereotype.Component;
+
+@Component
+class OrderRepository {
+ private final List orders = new ArrayList<>();
+
+ public Order save(Order order) {
+ order = new Order(UUID.randomUUID()
+ .toString(), order.customerId(), order.productIds(), order.timestamp());
+ orders.add(order);
+ return order;
+ }
+
+ public List ordersByCustomer(String customerId) {
+ return orders.stream()
+ .filter(it -> it.customerId()
+ .equals(customerId))
+ .toList();
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/application/events/orders/OrderService.java b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/application/events/orders/OrderService.java
new file mode 100644
index 0000000000..c60792813c
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/application/events/orders/OrderService.java
@@ -0,0 +1,29 @@
+package com.baeldung.springmodulith.application.events.orders;
+
+import java.util.Arrays;
+
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Service;
+
+@Service
+public class OrderService {
+
+ private final OrderRepository repository;
+ private final ApplicationEventPublisher eventPublisher;
+
+ public OrderService(OrderRepository orders, ApplicationEventPublisher eventsPublisher) {
+ this.repository = orders;
+ this.eventPublisher = eventsPublisher;
+ }
+
+ public void placeOrder(String customerId, String... productIds) {
+ Order order = new Order(customerId, Arrays.asList(productIds));
+ // business logic to validate and place the order
+
+ Order savedOrder = repository.save(order);
+
+ OrderCompletedEvent event = new OrderCompletedEvent(savedOrder.id(), savedOrder.customerId(), savedOrder.timestamp());
+ eventPublisher.publishEvent(event);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/application/events/rewards/LoyalCustomersRepository.java b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/application/events/rewards/LoyalCustomersRepository.java
new file mode 100644
index 0000000000..29ba6fa8e2
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/application/events/rewards/LoyalCustomersRepository.java
@@ -0,0 +1,44 @@
+package com.baeldung.springmodulith.application.events.rewards;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.stereotype.Component;
+
+@Component
+public class LoyalCustomersRepository {
+
+ private List customers = new ArrayList<>();
+
+ public Optional find(String customerId) {
+ return customers.stream()
+ .filter(it -> it.customerId()
+ .equals(customerId))
+ .findFirst();
+ }
+
+ public void awardPoints(String customerId, int points) {
+ var customer = find(customerId).orElseGet(() -> save(new LoyalCustomer(customerId, 0)));
+
+ customers.remove(customer);
+ customers.add(customer.addPoints(points));
+ }
+
+ public LoyalCustomer save(LoyalCustomer customer) {
+ customers.add(customer);
+ return customer;
+ }
+
+ public boolean isLoyalCustomer(String customerId) {
+ return find(customerId).isPresent();
+ }
+
+ public record LoyalCustomer(String customerId, int points) {
+
+ LoyalCustomer addPoints(int points) {
+ return new LoyalCustomer(customerId, this.points() + points);
+ }
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/application/events/rewards/LoyaltyPointsService.java b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/application/events/rewards/LoyaltyPointsService.java
new file mode 100644
index 0000000000..8cd1afe329
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/main/java/com/baeldung/springmodulith/application/events/rewards/LoyaltyPointsService.java
@@ -0,0 +1,24 @@
+package com.baeldung.springmodulith.application.events.rewards;
+
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Service;
+
+import com.baeldung.springmodulith.application.events.orders.OrderCompletedEvent;
+
+@Service
+public class LoyaltyPointsService {
+
+ public static final int ORDER_COMPLETED_POINTS = 60;
+ private final LoyalCustomersRepository loyalCustomers;
+
+ public LoyaltyPointsService(LoyalCustomersRepository loyalCustomers) {
+ this.loyalCustomers = loyalCustomers;
+ }
+
+ @EventListener
+ public void onOrderCompleted(OrderCompletedEvent event) {
+ // business logic to award points to loyal customers
+ loyalCustomers.awardPoints(event.customerId(), ORDER_COMPLETED_POINTS);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/application/events/EventListenerUnitTest.java b/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/application/events/EventListenerUnitTest.java
new file mode 100644
index 0000000000..676bc1173b
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/application/events/EventListenerUnitTest.java
@@ -0,0 +1,36 @@
+package com.baeldung.springmodulith.application.events;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Instant;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.ApplicationEventPublisher;
+
+import com.baeldung.springmodulith.application.events.orders.OrderCompletedEvent;
+import com.baeldung.springmodulith.application.events.rewards.LoyalCustomersRepository;
+
+@SpringBootTest
+class EventListenerUnitTest {
+
+ @Autowired
+ private LoyalCustomersRepository customers;
+
+ @Autowired
+ private ApplicationEventPublisher testEventPublisher;
+
+ @Test
+ void whenPublishingOrderCompletedEvent_thenRewardCustomerWithLoyaltyPoints() {
+ OrderCompletedEvent event = new OrderCompletedEvent("order-1", "customer-1", Instant.now());
+
+ testEventPublisher.publishEvent(event);
+
+ assertThat(customers.find("customer-1"))
+ .isPresent().get()
+ .hasFieldOrPropertyWithValue("customerId", "customer-1")
+ .hasFieldOrPropertyWithValue("points", 60);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/application/events/EventPublisherUnitTest.java b/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/application/events/EventPublisherUnitTest.java
new file mode 100644
index 0000000000..f4bdeee90d
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/application/events/EventPublisherUnitTest.java
@@ -0,0 +1,37 @@
+package com.baeldung.springmodulith.application.events;
+
+import com.baeldung.springmodulith.application.events.orders.OrderService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.ComponentScan;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest
+class EventPublisherUnitTest {
+
+ @Autowired
+ OrderService orderService;
+
+ @Autowired
+ TestEventListener testEventListener;
+
+ @BeforeEach
+ void beforeEach() {
+ testEventListener.reset();
+ }
+
+ @Test
+ void whenPlacingOrder_thenPublishApplicationEvent() {
+ orderService.placeOrder("customer1", "product1", "product2");
+
+ assertThat(testEventListener.getEvents())
+ .hasSize(1).first()
+ .hasFieldOrPropertyWithValue("customerId", "customer1")
+ .hasFieldOrProperty("orderId")
+ .hasFieldOrProperty("timestamp");
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/application/events/SpringModulithScenarioApiUnitTest.java b/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/application/events/SpringModulithScenarioApiUnitTest.java
new file mode 100644
index 0000000000..f36a0c30e6
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/application/events/SpringModulithScenarioApiUnitTest.java
@@ -0,0 +1,46 @@
+package com.baeldung.springmodulith.application.events;
+
+import com.baeldung.springmodulith.application.events.orders.OrderCompletedEvent;
+import com.baeldung.springmodulith.application.events.orders.OrderService;
+import com.baeldung.springmodulith.application.events.rewards.LoyalCustomersRepository;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.modulith.test.ApplicationModuleTest;
+import org.springframework.modulith.test.ApplicationModuleTest.BootstrapMode;
+import org.springframework.modulith.test.Scenario;
+
+import java.time.Duration;
+import java.time.Instant;
+
+import static java.time.Duration.ofMillis;
+import static org.assertj.core.api.Assertions.assertThat;
+
+@ApplicationModuleTest
+class SpringModulithScenarioApiUnitTest {
+
+ @Autowired
+ OrderService orderService;
+
+ @Autowired
+ LoyalCustomersRepository loyalCustomers;
+
+ @Test
+ void whenPlacingOrder_thenPublishOrderCompletedEvent(Scenario scenario) {
+ scenario.stimulate(() -> orderService.placeOrder("customer-1", "product-1", "product-2"))
+ .andWaitForEventOfType(OrderCompletedEvent.class)
+ .toArriveAndVerify(evt -> assertThat(evt)
+ .hasFieldOrPropertyWithValue("customerId", "customer-1")
+ .hasFieldOrProperty("orderId")
+ .hasFieldOrProperty("timestamp"));
+ }
+
+ @Test
+ void whenReceivingPublishOrderCompletedEvent_thenRewardCustomerWithLoyaltyPoints(Scenario scenario) {
+ scenario.publish(new OrderCompletedEvent("order-1", "customer-1", Instant.now()))
+ .andWaitForStateChange(() -> loyalCustomers.find("customer-1"))
+ .andVerify(it -> assertThat(it)
+ .isPresent().get()
+ .hasFieldOrPropertyWithValue("customerId", "customer-1")
+ .hasFieldOrPropertyWithValue("points", 60));
+ }
+}
diff --git a/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/application/events/TestEventListener.java b/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/application/events/TestEventListener.java
new file mode 100644
index 0000000000..8973a99355
--- /dev/null
+++ b/spring-boot-modules/spring-boot-libraries-3/src/test/java/com/baeldung/springmodulith/application/events/TestEventListener.java
@@ -0,0 +1,29 @@
+package com.baeldung.springmodulith.application.events;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+import com.baeldung.springmodulith.application.events.orders.OrderCompletedEvent;
+
+@Component
+class TestEventListener {
+
+ private final List events = new ArrayList<>();
+
+ @EventListener
+ void onEvent(OrderCompletedEvent event) {
+ events.add(event);
+ }
+
+ public List getEvents() {
+ return events;
+ }
+
+ public void reset() {
+ events.clear();
+ }
+}
+