6

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!

6
  • 1
    The term "transaction", as it relates to DDD, is about boundaries. That is, each aggregate is the sole, unambiguous, and authoritative representation of its state. DDD is a design methodology and therefore has no opinion about how one manages persistence. It is perfectly acceptable (and often useful), as part of a single use-case, to mutate multiple aggregates within a single database transaction. Commented Mar 3, 2019 at 15:14
  • @king-side-slide that's good perspective to keep in mind. I tend to get too caught up in the persistence concerns when working in DDD. So, regarding the boundaries of my aggregates, though, you don't see an issue with how it's defined and given the requirements of initiating an new order request with potentially new customer information? Commented Mar 4, 2019 at 17:48
  • It seems to me that the problem for which your question seeks an answer is predicated on the idea that you not change two aggregates as part of a single use-case. I don't think this is a problem in terms of design. Commented Mar 4, 2019 at 19:47
  • May I ask why CustomerAddress is an Entity but not a Value Object? For me, custom addresses are distinguished by their attributes (number, street, cities, ...), not their identities.
    – Hieu Le
    Commented Mar 5, 2019 at 6:56
  • @HieuLe good question. I actually have a VO for Address, but a "CustomerAddress" is an entity that has an Address VO as well as a reference to the parent customer, plus a few other fields specific to the business. So, it's not just a list of address fields - that's what the Address VO is for. Commented Mar 5, 2019 at 16:41

1 Answer 1

9

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.

5
  • 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? Commented 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
    Commented 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? Commented Mar 5, 2019 at 0:00
  • 1
    That 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
    Commented Mar 5, 2019 at 9:06
  • did that answer your question? or do you still see some problems?
    – Roman Weis
    Commented Mar 5, 2019 at 22:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.