CKK
CKK

Reputation: 320

DDD and CQRS: use multiple repositories from a single command handler?

A canonical example of a simple e-shop.

Let's say a user adds some items to a basket and clicks "Checkout". A "Create Order" command gets issued. Now, before actually creating an order record with status "Payment Expected" and corresponding order lines in a db, we have to check that the items that the user selected are still available (maybe some items were available when the user added them to the basket but not anymore). And we also have to reserve them, so that they do not suddenly disappear while the user is still checking out.

So my question is how to perform this "check and reserve" routine? The way I see it I have multiple options:

This is not a question about how to best model an e-shop checkout flow. The above is just an example. I would imagine there could be many similar scenarios in many different applications.

Upvotes: 4

Views: 2841

Answers (3)

Neil W
Neil W

Reputation: 9247

As mentioned in other answers you don't want your CommandHandler to maintain multiple aggregates. This should be delegated to a DomainService, achieved through DomainEvents, or pass the Products into the Order aggregate to maintain. The solution also depends on whether the reservation process and order process are in the same bounded context or not.

Reservation and Order in same BC, option 1 (Domain Service):

  • CreateOrderCommand dispatched
    • Infrastructure Starts Transaction
      • CreateOrderCommandHandler invokes CreateOrderDomainService
          1. CreateOrderDomainService retrives products from repository and attempts to reserve, throw on failure
          1. CreateOrderDomainService tries to create the Order and adds to repository, throw on failure
    • If no errors, Infrastrucure commits.
    • If errors, infrastructure aborts.

Reservation and Order in same BC, option 2 (Domain Events):

  • CreateOrderCommand dispatched
    • Infrastructure Starts Transaction
      • CreateOrderCommandHandler creates the Order and adds to repository
      • Order creates domain event "OrderCreated"
      • OrderCreatedEventHandler retrieves product from repository and attempts to reserve
    • If no errors, Infrastructure commits.
    • If errors, infrastructure aborts.

Reservation and Order in same BC, option 3 (Inject Products):

  • CreateOrderCommand dispatched
    • Infrastructure Starts Transaction
      • CreateOrderCommandHandler retrieves products used in order
      • CreateOrderCommandHandler creates the Order passing in all products
      • Order tries to reserve products using product domain entity, throw on failure
    • If no errors, Infrastructure commits.
    • If errors, infrastructure aborts.

Reservation and Order in different BCs, option 1 (Domain Service):

  • CreateOrderCommand dispatched
    • Infrastructure Starts Transaction
      • CreateOrderCommandHandler invokes CreateOrderDomainService
          1. CreateOrderDomainService dispatched ReserveProduct command to Reservations BC and waits.
          • Reservations BC completes the reservation, throws on failure
          1. CreateOrderDomainService tries to create the Order and adds to repository
          • If failure, then compensate by dispatching UnreserveProduct command to Reservations BC, then throw.
    • If no errors, Infrastrucure commits.
    • If errors, infrastructure aborts.

Reservation and Order in different BCs, option 2 (Domain Events):

  • CreateOrderCommand dispatched
    • Infrastructure Starts Transaction
      • CreateOrderCommandHandler creates the Order and adds to repository
      • Order creates domain event "OrderCreated"
      • OrderCreatedEventHandler dispatches ReserveProduct command to Reservations BC and waits.
        • If failure, then throw.
    • If no errors, Infrastructure commits.
    • If errors, infrastructure aborts.

In all cases, use a concurrency token on the Product to prevent concurrent 'over-reservation'.

Upvotes: 3

rascio
rascio

Reputation: 9279

A command should update a single aggregate, otherwise you are breaking the "aggregate" contract, as far as this is true you can do how many reads you want in your handler.

Events are the most consistent solution for these kind of situation, but you pay the price of complexity to write your software in that way.

Should you use a repository or a service, this depends if the data you are reading are part of the same bounded context of the handler (repository) or in a different one (service).

Introducing a ReserveProduct command you are defining the domain behavior, and is a different issue on how to do things technically, you may want to do it or not, but this depends on the domain.

Upvotes: 1

Francesc Castells
Francesc Castells

Reputation: 2857

The solution to the problem you present is not a matter of coding "style" or following good DDD practices. If using multiple repositories in a single handler solved your problem, I believe you should consider it a good option.

But the main issue in this type of scenario is that in many systems, Orders and Stock are in different Services/Bounded Contexts, therefore in different databases. Stock could even be in an external system not controlled by you. This means that you can't reserve the stock and place the order transactionally, so you risk reserving the stock and not placing the order or the other way around.

The reason why using events is recommended to handle these scenarios is because with events it is possible to develop this type of workflow reliably although this introduces new complexities. With a bit of technology, it is possible to reliably reserve the stock and publish an event and on the other side, reliably capture the payment and publish another event, then place the order and publish another event, etc. This workflow can involve things like outbox pattern, retries, sagas, compensating actions (to rollback the previous steps in case one step fails), etc.

Upvotes: 5

Related Questions