diff --git a/cqrs-spring-utils/src/main/java/com/barsifedron/candid/cqrs/spring/DomainEventHandlersRegistry.java b/cqrs-spring-utils/src/main/java/com/barsifedron/candid/cqrs/spring/DomainEventHandlersRegistry.java index 3aa31f8..58f645d 100644 --- a/cqrs-spring-utils/src/main/java/com/barsifedron/candid/cqrs/spring/DomainEventHandlersRegistry.java +++ b/cqrs-spring-utils/src/main/java/com/barsifedron/candid/cqrs/spring/DomainEventHandlersRegistry.java @@ -30,7 +30,6 @@ @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) public class DomainEventHandlersRegistry { - private final Map, List>> map = new HashMap<>(); private final DomainEventHandlerInfoList allEventHandlersInfoList; @Autowired @@ -50,7 +49,6 @@ public DomainEventHandlersRegistry(ApplicationContext applicationContext) { */ public DomainEventHandlersRegistry(ApplicationContext applicationContext, String... packages) { - Stream.of(packages).forEach(System.out::print); registerHandlersToApplicationContext(applicationContext, packages); String[] handlersClassNames = applicationContext.getBeanNamesForType(DomainEventHandler.class); diff --git a/cqrs-spring-utils/src/main/java/com/barsifedron/candid/cqrs/spring/domainevent/ToProcessAfterMainTransaction.java b/cqrs-spring-utils/src/main/java/com/barsifedron/candid/cqrs/spring/domainevent/ToProcessAfterMainTransaction.java index 55d0c33..1113842 100644 --- a/cqrs-spring-utils/src/main/java/com/barsifedron/candid/cqrs/spring/domainevent/ToProcessAfterMainTransaction.java +++ b/cqrs-spring-utils/src/main/java/com/barsifedron/candid/cqrs/spring/domainevent/ToProcessAfterMainTransaction.java @@ -1,4 +1,10 @@ package com.barsifedron.candid.cqrs.spring.domainevent; +/** + * A marker interface. + * + * If the bus allows it, domain event handlers marked with it will be executed AFTER + * the main database transaction + */ public interface ToProcessAfterMainTransaction { } diff --git a/happy-neighbourhood-core/src/main/java/com/barsifedron/candid/cqrs/happy/command/ReturnItemCommandHandler.java b/happy-neighbourhood-core/src/main/java/com/barsifedron/candid/cqrs/happy/command/ReturnItemCommandHandler.java index e2060d5..acf2c22 100644 --- a/happy-neighbourhood-core/src/main/java/com/barsifedron/candid/cqrs/happy/command/ReturnItemCommandHandler.java +++ b/happy-neighbourhood-core/src/main/java/com/barsifedron/candid/cqrs/happy/command/ReturnItemCommandHandler.java @@ -71,11 +71,7 @@ private void validate(ReturnItemCommand command) { throw new UnknownItemException(command.itemId); } - System.out.println("loans.all() = " + loans.all()); List loans = this.loans.forItem(new ItemId(command.itemId), Loan.STATUS.values()); - System.out.println("loans = " + loans); - - List activeLoan = this.loans.forItem(new ItemId(command.itemId), Loan.STATUS.IN_PROGRESS); if (activeLoan.isEmpty() && command.ifNoActiveLoanIsFound == ReturnItemCommand.IF_NO_ACTIVE_LOAN_FOUND.FAIL) { diff --git a/happy-neighbourhood-core/src/main/java/com/barsifedron/candid/cqrs/happy/domainevents/ItemBorrowedDomainEventHandler_InMainTransaction.java b/happy-neighbourhood-core/src/main/java/com/barsifedron/candid/cqrs/happy/domainevents/ItemBorrowedDomainEventHandler_InMainTransaction.java new file mode 100644 index 0000000..71be4c6 --- /dev/null +++ b/happy-neighbourhood-core/src/main/java/com/barsifedron/candid/cqrs/happy/domainevents/ItemBorrowedDomainEventHandler_InMainTransaction.java @@ -0,0 +1,39 @@ +package com.barsifedron.candid.cqrs.happy.domainevents; + +import com.barsifedron.candid.cqrs.domainevent.DomainEventHandler; +import com.barsifedron.candid.cqrs.domainevent.ToProcessAfterMainTransaction; +import com.barsifedron.candid.cqrs.happy.command.BorrowItemCommandHandler; +import com.barsifedron.candid.cqrs.happy.domain.Email; +import com.barsifedron.candid.cqrs.happy.domain.EmailRepository; + +import javax.inject.Inject; +import java.util.logging.Logger; + +public class ItemBorrowedDomainEventHandler_InMainTransaction implements DomainEventHandler { + + static Logger LOGGER = Logger.getLogger(ItemBorrowedDomainEventHandler_InMainTransaction.class.getName()); + + @Inject + public ItemBorrowedDomainEventHandler_InMainTransaction() { + } + + @Override + public void handle(ItemBorrowedDomainEvent event) { + + LOGGER.info("" + + "This event handler executes WITHIN the main transaction" + + "as it makes NO use of the marker interface : ToProcessAfterMainTransaction"); + + // add your code that needs to be processed in the main transaction here. + + // ... + // ... + + + } + + @Override + public Class listenTo() { + return ItemBorrowedDomainEvent.class; + } +} diff --git a/happy-neighbourhood-core/src/main/java/com/barsifedron/candid/cqrs/happy/domainevents/ItemBorrowedDomainEventHandler.java b/happy-neighbourhood-core/src/main/java/com/barsifedron/candid/cqrs/happy/domainevents/ItemBorrowedDomainEventHandler_PostMainTransaction.java similarity index 74% rename from happy-neighbourhood-core/src/main/java/com/barsifedron/candid/cqrs/happy/domainevents/ItemBorrowedDomainEventHandler.java rename to happy-neighbourhood-core/src/main/java/com/barsifedron/candid/cqrs/happy/domainevents/ItemBorrowedDomainEventHandler_PostMainTransaction.java index f5ad0a6..4b8c116 100644 --- a/happy-neighbourhood-core/src/main/java/com/barsifedron/candid/cqrs/happy/domainevents/ItemBorrowedDomainEventHandler.java +++ b/happy-neighbourhood-core/src/main/java/com/barsifedron/candid/cqrs/happy/domainevents/ItemBorrowedDomainEventHandler_PostMainTransaction.java @@ -7,19 +7,27 @@ import com.barsifedron.candid.cqrs.happy.domain.EmailRepository; import javax.inject.Inject; +import java.util.logging.Logger; -public class ItemBorrowedDomainEventHandler implements DomainEventHandler, ToProcessAfterMainTransaction { +public class ItemBorrowedDomainEventHandler_PostMainTransaction implements DomainEventHandler, ToProcessAfterMainTransaction { private final EmailRepository emailRepository; + static Logger LOGGER = Logger.getLogger(ItemBorrowedDomainEventHandler_PostMainTransaction.class.getName()); + @Inject - public ItemBorrowedDomainEventHandler(EmailRepository emailRepository) { + public ItemBorrowedDomainEventHandler_PostMainTransaction(EmailRepository emailRepository) { this.emailRepository = emailRepository; } @Override public void handle(ItemBorrowedDomainEvent event) { + LOGGER.info("" + + "This event handler executes AFTER the main transaction " + + "due to the use of the marker interface : ToProcessAfterMainTransaction " + + "AND the proper configuration of the smart command bus. (@see the CommandBusFactory class)"); + if (event.notification == BorrowItemCommandHandler.NOTIFICATION.NONE) { return; } diff --git a/happy-neighbourhood-core/src/main/java/com/barsifedron/candid/cqrs/happy/domainevents/NewMemberRegisteredDomainEventHandler.java b/happy-neighbourhood-core/src/main/java/com/barsifedron/candid/cqrs/happy/domainevents/NewMemberRegisteredDomainEventHandler.java new file mode 100644 index 0000000..34e8cdf --- /dev/null +++ b/happy-neighbourhood-core/src/main/java/com/barsifedron/candid/cqrs/happy/domainevents/NewMemberRegisteredDomainEventHandler.java @@ -0,0 +1,48 @@ +package com.barsifedron.candid.cqrs.happy.domainevents; + +import com.barsifedron.candid.cqrs.domainevent.DomainEventHandler; +import com.barsifedron.candid.cqrs.happy.domain.Email; +import com.barsifedron.candid.cqrs.happy.domain.EmailRepository; + +import javax.inject.Inject; + +public class NewMemberRegisteredDomainEventHandler implements DomainEventHandler { + + private final EmailRepository emailRepository; + + @Inject + public NewMemberRegisteredDomainEventHandler(EmailRepository emailRepository) { + this.emailRepository = emailRepository; + } + + @Override + public void handle(NewMemberRegisteredDomainEvent event) { + + Email email = Email + .builder() + .email(event.email) + .body(String.format( + "" + + "Hi %s.\n" + + "You are now a member of the happy neighborhood community.\n" + + "Your member id is %s.\n" + + "We are glad to have you in our team!.\\n", + event.firstname, + event.memberId)) + .status(Email.EMAIL_STATUS.TO_BE_SENT) + .build(); + + + /** + * We store the email to be sent. Another process will manage retrieval and actually sending of these + */ + emailRepository.add(email); + + } + + + @Override + public Class listenTo() { + return NewMemberRegisteredDomainEvent.class; + } +} diff --git a/happy-neighbourhood-spring-shell/src/main/java/com/barsifedron/candid/cqrs/happy/shell/StartupApplicationListenerExample.java b/happy-neighbourhood-spring-shell/src/main/java/com/barsifedron/candid/cqrs/happy/shell/StartupApplicationListenerExample.java index 0e489a4..17a6e3f 100644 --- a/happy-neighbourhood-spring-shell/src/main/java/com/barsifedron/candid/cqrs/happy/shell/StartupApplicationListenerExample.java +++ b/happy-neighbourhood-spring-shell/src/main/java/com/barsifedron/candid/cqrs/happy/shell/StartupApplicationListenerExample.java @@ -50,7 +50,7 @@ public void onApplicationEvent(ContextRefreshedEvent event) { .build() ); - commandBusFactory.simpleBus().dispatch( + commandBusFactory.withOutsideTransactionCapability().dispatch( BorrowItemCommand .builder() .itemId("hammerId") @@ -68,7 +68,7 @@ public void onApplicationEvent(ContextRefreshedEvent event) { .build() ); - commandBusFactory.simpleBus().dispatch( + commandBusFactory.withOutsideTransactionCapability().dispatch( BorrowItemCommand .builder() .itemId("hammerId") @@ -86,7 +86,7 @@ public void onApplicationEvent(ContextRefreshedEvent event) { .build() ); - commandBusFactory.simpleBus().dispatch( + commandBusFactory.withOutsideTransactionCapability().dispatch( BorrowItemCommand .builder() .itemId("hammerId") diff --git a/happy-neighbourhood-spring-shell/src/main/java/com/barsifedron/candid/cqrs/happy/shell/controllers/MembersController.java b/happy-neighbourhood-spring-shell/src/main/java/com/barsifedron/candid/cqrs/happy/shell/controllers/MembersController.java index 1b69344..3b1678d 100644 --- a/happy-neighbourhood-spring-shell/src/main/java/com/barsifedron/candid/cqrs/happy/shell/controllers/MembersController.java +++ b/happy-neighbourhood-spring-shell/src/main/java/com/barsifedron/candid/cqrs/happy/shell/controllers/MembersController.java @@ -7,7 +7,6 @@ import com.barsifedron.candid.cqrs.happy.command.RegisterNewMemberCommand; import com.barsifedron.candid.cqrs.happy.command.RegisterNewMemberCommandHandler; import com.barsifedron.candid.cqrs.happy.domain.LoanId; -import com.barsifedron.candid.cqrs.happy.domain.LoanRepository; import com.barsifedron.candid.cqrs.happy.domain.MemberId; import com.barsifedron.candid.cqrs.happy.query.GetMemberQuery; import com.barsifedron.candid.cqrs.happy.query.GetMemberQueryHandler; @@ -68,7 +67,7 @@ public ResponseEntity borrowItem( @RequestBody BorrowItemCommand borrowItemCommand) { CommandResponse commandResponse = commandBusFactory - .complexBus() + .withOutsideTransactionCapability() .dispatch(borrowItemCommand .toBuilder() .loanId(new LoanId().asString()) diff --git a/happy-neighbourhood-spring-shell/src/main/java/com/barsifedron/candid/cqrs/happy/shell/utils/cqrs/command/CommandBusFactory.java b/happy-neighbourhood-spring-shell/src/main/java/com/barsifedron/candid/cqrs/happy/shell/utils/cqrs/command/CommandBusFactory.java index 69d7393..e77b070 100644 --- a/happy-neighbourhood-spring-shell/src/main/java/com/barsifedron/candid/cqrs/happy/shell/utils/cqrs/command/CommandBusFactory.java +++ b/happy-neighbourhood-spring-shell/src/main/java/com/barsifedron/candid/cqrs/happy/shell/utils/cqrs/command/CommandBusFactory.java @@ -6,7 +6,6 @@ import com.barsifedron.candid.cqrs.command.middleware.DomainEventsDispatcher; import com.barsifedron.candid.cqrs.domainevent.DomainEventBus; import com.barsifedron.candid.cqrs.domainevent.DomainEventBusMiddleware; -import com.barsifedron.candid.cqrs.domainevent.ToProcessAfterMainTransaction; import com.barsifedron.candid.cqrs.happy.shell.utils.cqrs.command.middleware.WithErrorLogCommandBusMiddleware; import com.barsifedron.candid.cqrs.happy.shell.utils.cqrs.command.middleware.WithExecutionDurationLogging; import com.barsifedron.candid.cqrs.happy.shell.utils.cqrs.domainevents.DomainEventBusFactory; @@ -51,42 +50,69 @@ public CommandBusFactory( } /** - * A simple bus. All domain events are processed within the main transaction. + * A simple bus. + * The work done by the command handlers and the event handlers happens in the SAME transaction. */ public CommandBus simpleBus() { CommandBusMiddleware transactionalMiddleware = transactionalMiddleware(); - DomainEventBus domainEventBus = domainEventBusFactory.withAllHandlersEventBus(); + DomainEventBus domainEventBus = DomainEventBusMiddleware + .compositeOf( + new ExecutionTimeLoggingDomainEventsBusMiddleware(), + new DomainEventBusInfoMiddleware("" + + "\n Entering main transaction domain event bus." + + "\n Warning : The event handlers execute in the same transaction as the command.")) + .decorate(domainEventBusFactory.basicEventBus()); return CommandBusMiddleware .compositeOf( + new WithErrorLogCommandBusMiddleware(), new WithExecutionDurationLogging(), new DetailedLoggingCommandBusMiddleware(), new ValidatingCommandBusMiddleware(), + + // transactional boundary transactionalMiddleware, + + // events handlers executed within the main transaction new DomainEventsDispatcher(domainEventBus)) + .decorate(new MapCommandBus(commandHandlersRegistry.handlers())); } /** + * A more complex wiring of the command bus to illustrate more advanced behaviors. + * This is similar to what I use in my own projects. + *

* By default, the work done by the command handlers and the event handlers happens in the SAME transaction. - * Perfect if a failure at any point should revert the whole operation. I find this to be ok in most situations. + * Perfect if a failure at any point should revert the whole operation. + * I find this to be ok in most situations. *

* But you might want specific events be processed in their own separate transactions. * For example, if you want to make sure entities are persisted before calling an external * service or if failing to send an email should not compromise what was otherwise successful. - * For this, your domain event handler just need to implement the following specific marker interface : + * For this, your domain event HANDLERS just need to implement the following specific marker interface : * * @see com.barsifedron.candid.cqrs.spring.domainevent.ToProcessAfterMainTransaction *

* In that case, handlers implementing this interface will be executed in a separate transaction than the main one. *

- * Warning : since domain events can have many handlers, for the SAME domain event, + * Warning 1 : since domain events can have many handlers, for the SAME domain event, * some of its handlers can proceed within the main transaction AND others can proceed outside. + *

+ * Warning 2 : The use of the marker interface will only work if you bus if properly configured for it. + * AKA : Using it with the simple bus above + * @see CommandBusFactory#simpleBus() + *

+ * would not change the behavior and all handlers would still be processed in the main transaction. + *

+ * For an example look here and how these are processed in the logs: + * @see com.barsifedron.candid.cqrs.happy.domainevents.ItemBorrowedDomainEventHandler_InMainTransaction + * @see com.barsifedron.candid.cqrs.happy.domainevents.ItemBorrowedDomainEventHandler_PostMainTransaction */ - public CommandBus complexBus() { + public CommandBus withOutsideTransactionCapability() { CommandBusMiddleware transactionalMiddleware = transactionalMiddleware(); @@ -98,18 +124,51 @@ public CommandBus complexBus() { new DetailedLoggingCommandBusMiddleware(), new ValidatingCommandBusMiddleware(), - // events processed after the main transaction + // events handlers executed after the main transaction new DomainEventsDispatcher(afterMainTransactionDomainEventBus()), // transactional boundary transactionalMiddleware, - // events processed within the main transaction + // events handlers executed within the main transaction new DomainEventsDispatcher(inMainTransactionDomainEventBus())) .decorate(new MapCommandBus(commandHandlersRegistry.handlers())); } + /** + * @TODO I leave this one to you as an exercise. The bottom is a minimal working bus. + * Try to customize it to your taste and call it from a controller to see changes + */ + public CommandBus yourCustomCommandBus() { + return CommandBusMiddleware + .compositeOf( + CommandBusMiddleware.neutral(), + + // add your command middlewares here + // ... + + // Also try to customize the domain event bus + new DomainEventsDispatcher(yourCustomDomainEventBus()) + ) + .decorate(new MapCommandBus(commandHandlersRegistry.handlers())); + } + + /** + * @TODO see above method + */ + private DomainEventBus yourCustomDomainEventBus() { + return DomainEventBusMiddleware + .compositeOf( + + DomainEventBusMiddleware.neutral() + + // add your domain events middlewares here + // ... + ) + .decorate(domainEventBusFactory.basicEventBus()); + } + /** * This bus only includes and executes event handlers NOT marked with : @@ -123,7 +182,7 @@ public DomainEventBus inMainTransactionDomainEventBus() { new ExecutionTimeLoggingDomainEventsBusMiddleware(), new DomainEventBusInfoMiddleware("" + "\n Entering main transaction domain event bus." + - "\n Warning : The event handlers execute in the same transaction as the command."), + "\n Warning : The event handlers execute in the SAME transaction as the command."), transactionalEventBusMiddleware()) .decorate(baseEventBus); } @@ -131,7 +190,7 @@ public DomainEventBus inMainTransactionDomainEventBus() { /** * This bus only includes and executes event handlers marked WITH : * - * @see ToProcessAfterMainTransaction + * @see com.barsifedron.candid.cqrs.spring.domainevent.ToProcessAfterMainTransaction */ public DomainEventBus afterMainTransactionDomainEventBus() { DomainEventBus baseBus = domainEventBusFactory.afterMainTransactionDomainEventBus(); @@ -140,7 +199,7 @@ public DomainEventBus afterMainTransactionDomainEventBus() { new ExecutionTimeLoggingDomainEventsBusMiddleware(), new DomainEventBusInfoMiddleware("" + "\n Entering AFTER main transaction domain event bus. " + - "\n Warning : Event handlers executes in a separate transactional unit than the main one."), + "\n Warning : Event handlers executes in a SEPARATE transactional unit than the command."), transactionalEventBusMiddleware()) .decorate(baseBus); } diff --git a/happy-neighbourhood-spring-shell/src/main/java/com/barsifedron/candid/cqrs/happy/shell/utils/cqrs/domainevents/DomainEventBusFactory.java b/happy-neighbourhood-spring-shell/src/main/java/com/barsifedron/candid/cqrs/happy/shell/utils/cqrs/domainevents/DomainEventBusFactory.java index 25c1a9c..134c53e 100644 --- a/happy-neighbourhood-spring-shell/src/main/java/com/barsifedron/candid/cqrs/happy/shell/utils/cqrs/domainevents/DomainEventBusFactory.java +++ b/happy-neighbourhood-spring-shell/src/main/java/com/barsifedron/candid/cqrs/happy/shell/utils/cqrs/domainevents/DomainEventBusFactory.java @@ -54,7 +54,7 @@ public DomainEventBus afterMainTransactionDomainEventBus() { /** * This bus will include and execute ALL handlers found in the configured classpath */ - public DomainEventBus withAllHandlersEventBus() { + public DomainEventBus basicEventBus() { return new MapDomainEventBus(domainEventHandlersRegistry.handlersList().asMap()); }