Reputation: 4118
I'm working on a Domain-Driven-Design implementation, where we have some of the operations within the same aggregate that need to happen in conjunction with the other operation. The two operations are unrelated to each other.
Here is a sample code.
Note that this code only for an illustrative purpose and not a real example.
public ProcessOrderCommandHandler : IHandler<ProcessOrderCommand>
{
public async Task Handle(ProcessOrderCommand orderCommand)
{
var order = _repository.LoadOrder(orderCommand.Id);
// Operation - 1
order.AddToCart(orderCommand.Item);
// Operation - 2
order.ProcessOrder();
}
}
public class Order : Aggregate
{
public void AddToCart(Item item)
{ ... }
public void ProcessOrder()
{ ... }
}
ProcessOrder
and AddToCart
operations are not related and I have many such CommandHandlers
which are independent of each other but still need to be called in conjunction.
I see there are three options to solve this issue:
Option 1: The above sample code.
I did not particularly like this option since we would need to call multiple domain operations in a single CommandHandler
.
Option 2: Update Domain operation to do both operations in a single method call as below
public ProcessOrderCommandHandler: IHandler<ProcessOrderCommand>
{
public async Task Handle(ProcessOrderCommand orderCommand)
{
var order = _repository.LoadOrder(orderCommand.Id);
order.AddToCartAndProcessOrder(orderCommand.Item);
}
}
public class Order : Aggregate
{
public void AddToCartAndProcessOrder(Item item)
{
AddToCart(item);
ProcessOrder();
}
private void AddToCart(Item item)
{ ... }
private void ProcessOrder()
{ ... }
}
Again, not 100% convenience if this the right way since I would need to do this in all relevant operations.
Option 3: Raise and Handle Domain Event
public ProcessOrderCommandHandler : IHandler<ProcessOrderCommand>
{
public async Task Handle(ProcessOrderCommand orderCommand)
{
var order = _repository.LoadOrder(orderCommand.Id);
// Operation - 1
order.AddToCart(orderCommand.Item);
}
}
public class Order : Aggregate
{
public void AddToCart(Item item)
{
...
AddDomainEvent(new OrderUpdated(id));
}
public void ProcessOrder()
{ ... }
}
public OrderUpdatedEventHandler : IDomainEventHandler<OrderUpdatedEvent>
{
public async Task Handle(OrderUpdatedEvent orderUpdatedEvent)
{
// Loads the same order object from Cache
var order = _repository.LoadOrder(orderUpdatedEvent.Id);
// Operation - 2
order.ProcessOrder();
}
}
I feel this approach is the cleanest among all as it helps to keep separation of concerns. However, here, I'm handling the two domain operations within the same aggregate through Domain-Event, which is not how Domain-Events usually are used. As per Domain-Event definition from Microsoft Docs:
Use domain events to explicitly implement side effects of changes within your domain.
In other words, and using DDD lingo, use domain events to explicitly implement side effects across multiple aggregates.
Question: Is Option 3 is an acceptable solution in Domain-Driven-Design?
If yes, can you share some reference material/links where Domain-Events are handled within the same aggregate?
If not, what are the other options I have, including but not limited to Option 1 and 2?
Upvotes: 1
Views: 1590
Reputation: 156
Without too much context of your domain it is hard to tell, but this is an answer you should get from your product expert.
It seems that you have an Order aggregate that exposes a public interface to do operations on a Shopping Cart.
(This is a description of what happens in reality in any store to help on illustrating my idea)
In this process we have:
Again, this was an assumption that I made based on your description, but I used it to show that concepts can be extracted fr what really happens in a business process and domain experts should help you on getting them right.
Upvotes: 1
Reputation: 3929
Does the ProcessOrder() method only reflect changes inside the aggregate that should be performed on the same aggregate to react on adding some item to the cart? If yes, I would move the call to that operation into the AddToCart() method. If it's some business invariant that has to happen in the same aggregate whenever an item is added why put that responsibility on the application layer? This would only allow your business logic to leak outside the domain layer. If you want to use an event-based approach to implement these actions cohesively on the same aggregate that's a design decision of your choice which might or might not be more suited. But still it is no longer the responsibility of the application layer that this processing has to happen after adding an item.
But if the ProcessOrder() method rather represents the next step in the user's workflow after he added something to the cart (like submitting the order) you should ask yourself, Is my interaction with the domain model designed too CRUD based?
So from my experience DDD is usually more suited or let's say easier applicable for task-based User Interfaces. That means that the client (e.g. Web Browser) interacts with the system by performing smaller well-defined tasks rather than sending a bigger bunch of data which is than translated to several tasks performed on the domain model (here the aggregates) in sequence.
So in your example, if ProcessOrder() means "Submit the order" I would personally have two interactions with the back end. One for adding something to the cart - Use Case A - and a second one for submitting the order - Use Case B.
To me, in this case this also feels more natural from the customer's point-of-view, as I would expect to perform these tasks in separate steps. Also, if you consider this approach you not have to handle events especially as you operating on the same aggregate.
Upvotes: 1
Reputation: 2857
ProcessOrder
andAddToCart
operations are not related and I have many suchCommandHandlers
which are independent of each other but still need to be called in conjunction.
Before directly answering your question, I would suggest reevaluating this statement. If the operations are not related to each other, why do they belong to the same aggregate? Also, are these many CommandHandlers
operating on the same aggregate?
In short, are you sure your aggregates are correct? they should have a single and clear responsibility. If there are unrelated operations or too many operations, they might be serving several responsibilities.
Now, regarding your three options. In my opinion CommandHandlers
are what is called Use Cases in Clean Architecture. A use case, in your scenario, basically loads the aggregate from the DB, calls the business operations, and stores the updated aggregate back to the DB (plus potentially publishing events, talk to third parties, etc). So, if you see it this way, the fact that your use case requires to call 2 business operations in the same aggregate is not a problem, because that's what your use case specifies.
You would combine the two operations into one if the two operations separately didn't make sense.
Option 3 is the only one I wouldn't go to if we assume that the aggregates are correct. In my opinion, it would be abusing a pattern for something it wasn't meant to be used.
But, if you figure that, in fact, these operations belong to different aggregates, then definitely using events would be the best option.
Consider the following requirements:
In this scenario, you clearly have two aggregates (Cart and Shipping). Adding to Cart and recalculating the Shipping cost are two independent operations and coordinating them with events is what makes more sense and aligns with the business specification ("When X happens then...").
Now consider another use case for the previous example:
Now, Update Shipping Address and Recalculate the shipping cost are two operations on the same aggregate, but in this case, it wouldn't make sense to call them separately from the use case, because changing the address without recalculating the cost would leave the aggregate in an inconsistent state, so the aggregate itself could automatically recalculate the cost as soon as the address is changed.
Upvotes: 3