diff --git a/spring-modulith-api/pom.xml b/spring-modulith-api/pom.xml
index dc8392ef..13001a3e 100644
--- a/spring-modulith-api/pom.xml
+++ b/spring-modulith-api/pom.xml
@@ -23,6 +23,20 @@
+ * It is advisable that you use these integration listeners in combination with the Spring Modulith Event Publication + * Registry to make sure that the event publication does not get lost in case of an application or listener failure. + * + * @author Oliver Drotbohm + * @see Spring + * Modulith Event Publication Registry - Reference Documentation + */ +@Async +@Transactional(propagation = Propagation.REQUIRES_NEW) +@TransactionalEventListener +@Documented +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApplicationModuleIntegrationListener { + + /** + * Whether the transaction to be run for the event listener is supposed to be read-only (default {@literal false}). + */ + @AliasFor(annotation = Transactional.class, attribute = "readOnly") + boolean readOnlyTransaction() default false; +} diff --git a/spring-modulith-example/src/main/java/example/inventory/InventoryManagement.java b/spring-modulith-example/src/main/java/example/inventory/InventoryManagement.java index 6d301534..78d33f96 100644 --- a/spring-modulith-example/src/main/java/example/inventory/InventoryManagement.java +++ b/spring-modulith-example/src/main/java/example/inventory/InventoryManagement.java @@ -19,9 +19,8 @@ import example.order.OrderCompleted; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Async; +import org.springframework.modulith.ApplicationModuleIntegrationListener; import org.springframework.stereotype.Service; -import org.springframework.transaction.event.TransactionalEventListener; /** * A Spring {@link Service} exposed by the inventory module. @@ -35,8 +34,7 @@ public class InventoryManagement { private final InventoryInternal dependency; - @Async - @TransactionalEventListener + @ApplicationModuleIntegrationListener void on(OrderCompleted event) throws InterruptedException { var orderId = event.getOrderId(); diff --git a/spring-modulith-example/src/test/java/example/order/EventPublicationRegistryTests.java b/spring-modulith-example/src/test/java/example/order/EventPublicationRegistryTests.java index c93b8895..b971a641 100644 --- a/spring-modulith-example/src/test/java/example/order/EventPublicationRegistryTests.java +++ b/spring-modulith-example/src/test/java/example/order/EventPublicationRegistryTests.java @@ -22,11 +22,10 @@ import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; +import org.springframework.modulith.ApplicationModuleIntegrationListener; import org.springframework.modulith.events.EventPublicationRegistry; import org.springframework.modulith.test.ApplicationModuleTest; -import org.springframework.scheduling.annotation.Async; import org.springframework.test.annotation.DirtiesContext; -import org.springframework.transaction.event.TransactionalEventListener; /** * A show case for how the Spring Modulith application event publication registry keeps track of incomplete publications @@ -57,8 +56,7 @@ class EventPublicationRegistryTests { static class FailingAsyncTransactionalEventListener { - @Async - @TransactionalEventListener + @ApplicationModuleIntegrationListener void foo(OrderCompleted event) { throw new IllegalStateException(); } diff --git a/src/docs/asciidoc/40-events.adoc b/src/docs/asciidoc/40-events.adoc index 2fc516f1..64e3c4e4 100644 --- a/src/docs/asciidoc/40-events.adoc +++ b/src/docs/asciidoc/40-events.adoc @@ -78,6 +78,43 @@ This now effectively decouples the original transaction from the execution of th While this avoids the expansion of the original business transaction, it also creates a risk: if the listener fails for whatever reason, the event publication is lost, unless each listener actually implements its own safety net. Even worse, that doesn't even fully work, as the system might fail before the method is even invoked. +[[events.amil]] +== Application Module Integration Listener + +To run a transactional event listener in a transaction itself, it would need to be annotated with `@Transactional` itself. + +.An async, transactional event listener running in a transaction itself +[source, java] +---- +@Service +@RequiredArgsConstructor +public class InventoryManagement { + + @Async + @Transactional + @TransactionalEventListener + void on(OrderCompleted event) { + + } +} +---- + +To ease the declaration of what is supposed to describe the default way of integrating modules via events, Spring Modulith provides `@ApplicationModuleIntegrationListener` to shortcut the declaration + +.An application module integration listener +[source, java] +---- +@Service +@RequiredArgsConstructor +public class InventoryManagement { + + @ApplicationModuleIntegrationListener + void on(OrderCompleted event) { + + } +} +---- + [[events.publication-registry]] == The Event Publication Registry