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!
CustomerAddress
is an Entity but not a Value Object? For me, custom addresses are distinguished by their attributes (number, street, cities, ...), not their identities.