I have have a business process that receives an order request which also includes full customer information.
In the cases where the external customer ID from that order request is not found in our DB, we need to process that piece of the "create order" command, before we can actually save the order for that customer.
In the cases where we do find a matching customer ID but the address record in the DB differs from the one in the order request, we need to insert it as a new address record for that customer id (many CustomerAddress to 1 Customer) - note that this address also gets saved as part of the order's shipping address value object, because... well... those change, but the order is point in time.
I originally designed an orders aggregate with my Order
entity as the aggregate root (note that I have the Id property defined in a base Entity class, which the AggregateRoot abstract class also extends, but omitted here for brevity):
public class Order : AggregateRoot
{
//ctor ....
public long CustomerId { get; }
public Address ShippingAddress { get; } //value type
private readonly List<OrderItems> _items = new List<OrderItems>();
public IReadOnlyCollection<OrderItems> Items => _items.AsReadOnly();
// more props and behavior methods here....
}
In a separate aggregate, named "customers', I have my Customer
aggregate root and a CustomerAddress entity:
public class Customer : AggregateRoot
{
public string ExternalId { get; }
private readonly List<CustomerAddress> _addresses = new List<CustomerAddress>();
public IReadOnlyCollection<CustomerAddress> Addresses => _addresses.AsReadOnly();
// more props and behavior methods...
public void AddNewAddress(string externalKey, Address addressInfo)
{
_addresses.Add(new CustomerAddress(externalKey, addressInfo, this));
}
}
public class CustomerAddress : Entity
{
//ctor ...
public string ExternalAddressKey { get; }
public Customer Customer { get; }
public Address AddressInfo { get; }
// more props and behavior methods...
}
There will be times when we'll manage (add new, and update) customer data all by itself via UI and not in the context of an order. However, about 50-60% of the time the customer info will actually arrive as part of the order request API and sent to the internal command handler and we need to check if it already exists as mentioned above.
I suspect that I may have my aggregate boundaries designed incorrectly, but I am not sure, as I keep going back and forth in my mind how to achieve the business requirement as stated above using proper CQRS + DDD patterns.
I am not sure if I should treat my command handler as a sort of process manager/app service, and if I do that I'd be creating multiple transactions (working w/ 2 different aggregates) in the same command, which seems to go against the tenets of DDD.
Would appreciate some guidance on this. Thanks!
1 Answer 1
A comment from Eric Evans DDD Book:
On the other hand, a feature that can transfer funds from one account to another is a domain SERVICE because it embeds significant business rules (crediting and debiting the appropriate accounts, for example) and because a "funds transfer" is a meaningful banking term. In this case, the SERVICE does not do much on its own; it would ask the two Account objects to do most of the work. But to put the "transfer" operation on the Account object would be awkward, because the operation involves two accounts and some global rules.
Evans, Eric.
Domain-Driven Design: Tackling Complexity in the Heart of Software (S.107).
Pearson Education. Kindle-Version
Sometimes it is just not possible to achieve immediate consistency with a single aggregate root and it is not desirable to implement a long-running process. Then it is perfectly fine to use a domain service to coordinate changes on several aggregate roots.
-
Thanks for bringing me back to the "scriptures" (Evans'). I am not sure that what I am needing would end up as a domain service, since it would also be in charge of saving changes on my unit of work. It sounds more like that would belong in my command handler. Thoughts?Thiago Silva– Thiago Silva2019年03月04日 17:45:38 +00:00Commented Mar 4, 2019 at 17:45
-
Hmm, I think its a trap:) Here my thoughts: The address object is the intersection of the consistency boundaries of both aggregates. You need somehow to sync the customer address with the order, that is a domain invariant and should be addressed in a domain service. The second domain invariant is that a customer must exist before an order can be created, but since we would pass already both aggregates as parameters to the domain service that rule would be already covered. Does that make sense to you?Roman Weis– Roman Weis2019年03月04日 19:27:56 +00:00Commented Mar 4, 2019 at 19:27
-
I see what you're saying, but I still need to check for/create the customer aggregate in my command handler, before I can create the order aggregate and then turn to a domain service that enforces my domain invariants between those two aggregates. Or did I misunderstand your point?Thiago Silva– Thiago Silva2019年03月05日 00:00:51 +00:00Commented Mar 5, 2019 at 0:00
-
1That is almost exactly what I mean. Only I would not let the "create customer" logic leak into the application layer. I supose you have a CustomerRepository where you get the Customer by Id. If it does not find it, it should return a new Cutomer object with the specified Id. Then the domain service asserts always that the cusomter name etc. and customer address are consistent with the request. After that the application service would just update the customer.Roman Weis– Roman Weis2019年03月05日 09:06:28 +00:00Commented Mar 5, 2019 at 9:06
-
did that answer your question? or do you still see some problems?Roman Weis– Roman Weis2019年03月05日 22:06:58 +00:00Commented Mar 5, 2019 at 22:06
Explore related questions
See similar questions with these tags.
CustomerAddress
is an Entity but not a Value Object? For me, custom addresses are distinguished by their attributes (number, street, cities, ...), not their identities.