Skip to content

Commit

Permalink
Merge pull request #34 from barsifedron/29-allow-some-domain-events-t…
Browse files Browse the repository at this point in the history
…o-be-processed-within-the-current-transaction-and-others-to-be-processed-after

Issue 29 : clean ups + added docs for clarification
  • Loading branch information
barsifedron authored Sep 10, 2024
2 parents 7e788db + dce76fb commit 6376996
Show file tree
Hide file tree
Showing 10 changed files with 179 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
public class DomainEventHandlersRegistry {

private final Map<Class<DomainEvent>, List<Supplier<DomainEventHandler>>> map = new HashMap<>();
private final DomainEventHandlerInfoList allEventHandlersInfoList;

@Autowired
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,7 @@ private void validate(ReturnItemCommand command) {
throw new UnknownItemException(command.itemId);
}

System.out.println("loans.all() = " + loans.all());
List<Loan> loans = this.loans.forItem(new ItemId(command.itemId), Loan.STATUS.values());
System.out.println("loans = " + loans);



List<Loan> activeLoan = this.loans.forItem(new ItemId(command.itemId), Loan.STATUS.IN_PROGRESS);
if (activeLoan.isEmpty() && command.ifNoActiveLoanIsFound == ReturnItemCommand.IF_NO_ACTIVE_LOAN_FOUND.FAIL) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ItemBorrowedDomainEvent> {

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<ItemBorrowedDomainEvent> listenTo() {
return ItemBorrowedDomainEvent.class;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ItemBorrowedDomainEvent>, ToProcessAfterMainTransaction {
public class ItemBorrowedDomainEventHandler_PostMainTransaction implements DomainEventHandler<ItemBorrowedDomainEvent>, 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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<NewMemberRegisteredDomainEvent> {

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<NewMemberRegisteredDomainEvent> listenTo() {
return NewMemberRegisteredDomainEvent.class;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public void onApplicationEvent(ContextRefreshedEvent event) {
.build()
);

commandBusFactory.simpleBus().dispatch(
commandBusFactory.withOutsideTransactionCapability().dispatch(
BorrowItemCommand
.builder()
.itemId("hammerId")
Expand All @@ -68,7 +68,7 @@ public void onApplicationEvent(ContextRefreshedEvent event) {
.build()
);

commandBusFactory.simpleBus().dispatch(
commandBusFactory.withOutsideTransactionCapability().dispatch(
BorrowItemCommand
.builder()
.itemId("hammerId")
Expand All @@ -86,7 +86,7 @@ public void onApplicationEvent(ContextRefreshedEvent event) {
.build()
);

commandBusFactory.simpleBus().dispatch(
commandBusFactory.withOutsideTransactionCapability().dispatch(
BorrowItemCommand
.builder()
.itemId("hammerId")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -68,7 +67,7 @@ public ResponseEntity<String> borrowItem(
@RequestBody BorrowItemCommand borrowItemCommand) {

CommandResponse<LoanId> commandResponse = commandBusFactory
.complexBus()
.withOutsideTransactionCapability()
.dispatch(borrowItemCommand
.toBuilder()
.loanId(new LoanId().asString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
* <p>
* 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.
* <p>
* 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
* <p>
* In that case, handlers implementing this interface will be executed in a separate transaction than the main one.
* <p>
* 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.
* <p>
* 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()
* <p>
* would not change the behavior and all handlers would still be processed in the main transaction.
* <p>
* 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();

Expand All @@ -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 :
Expand All @@ -123,15 +182,15 @@ 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);
}

/**
* 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();
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down

0 comments on commit 6376996

Please sign in to comment.