Search code examples
transactionsdomain-driven-designdomain-events

How to ensure composite commands including persisting to db and dispatching events are atomic?


Good morning, in our project we have a REST controller looking like this (it's only pseudocode and not the exact case; here I am giving a simple example showing the overall concept):

@Transactional
public ResponesEntity<Void> compisiteCreate() {
  customerService.createCustomer()
  productService.createProduct()
  orderService.createOrder()
}

Factory method of each of the services (customer, product and order) is a separate use case. It persists a record in db and if persisted successfully, a corresponding event is dispatched. Additionally, each of the methods is annotated with @Transactional as each of them is invoked by a dedicated endpoint. This way we ensure that if an exception is thrown, neither the record is saved in db nor the event is dispatched fo a particular scenario.

Now the requirement is to have a compositeCreate endpoint (presented above) that should be atomic i.e. if createOrder fails, both createCustomer and createProduct are rollbacked. The problem are the events. Event dispatched is synchronous and can't be rolled back (fire & forget). Hence if the third method fails but the first two are ok, events related to the first method are already dispatched but the record won't be stored in the db.

My first thought here was to wrap this logic in some kind of an "event dispatcher session". Inside that session events are not dispatched synchronously, but queued until the session ends. Then at the end of controller logic, when the code inside passes without exceptions, we close the session which will dispatch all queued events. Pseudo code could look like this:

@Transactional
public ResponesEntity<Void> compisiteCreate() {
  eventDispatcher.startSession()
  customerService.createCustomer()
  productService.createProduct()
  orderService.createOrder()
  eventDispatcher.flushSession()
}

The event dispatcher session would be associated with a thread (e.g. using ThreadLocal) to ensure single session per request. If the session is not started, events are dispatched synchronously, without any change.

Expected result: Either everything is persisted and all events are dispatched or nothing is persisted and no events are dispatched.

Question: Does that idea make sense? Maybe somebody can see already some flaws? Or maybe that idea is completely wrong? Does anybody has any other idea how to tackle this problem?


Solution

  • How to ensure composite commands including persisting to db and dispatching events are atomic?

    "Outbox Pattern": write the events to the database as part of the transaction; worry about publishing the events separately.

    See Reliable Messaging without Distributed Transactions, by Udi Dahan

    Either everything is persisted and all events are dispatched or nothing is persisted and no events are dispatched.

    Because we have physical constraints (ex: a finite speed of light), all or nothing isn't a guarantee that we can make. We can manage weaker claims, like "at most once" or "at least once".

    "At least once" is the weaker claim that is actually useful, but it does mean that subscribers have to be able to recognize repeat messages - in other words, you need idempotent message handling.

    See also de Graaw 2010: Nobody Needs Reliable Messaging