Reputation: 320
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:
ProductStockRepository
to reserve the products and then on success use OrderRepository
to create an order. Meaning, we use multiple repositories in a single handler.ProductStockRepository
in the "Create Order" handler directly, instead, create a ProductStockService
and invoke methods on it to check and reserve the products. We still use multiple repositories in a single handler, but the usage of the stock repository is abstracted.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
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):
Reservation and Order in same BC, option 2 (Domain Events):
Reservation and Order in same BC, option 3 (Inject Products):
Reservation and Order in different BCs, option 1 (Domain Service):
Reservation and Order in different BCs, option 2 (Domain Events):
In all cases, use a concurrency token on the Product to prevent concurrent 'over-reservation'.
Upvotes: 3
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
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