0

Suppose we have an Order in a warehouse context. And we have a Client that's getting that Order.

Although in the DB Client has an ID and a bunch of parameters, but I model it as a value object inside my Order:

class Order extends AggregateRoot {
    private client: Client;
    private shipping: Shipping;
    private items: Array<OrderItem>;
}

class Client extends ValueObject {
    public readonly name: string;
    public readonly contactPhone: string;
    public readonly contactEmail: string;
}

The reason I model it as a value object is simple - I don't think warehouse cares about who the client is. The data is primarily needed for booking couriers, and in this context the Client can't really change his names or contact details - that would require adjustments to courier booking (at which point, might as well just treat it as a different Client altogether).

Now I'm thinking that it would actually be nice to know client ID (for some analytics or traceability). Easy:

class Client extends ValueObject {
    public readonly name: string;
    public readonly contactPhone: string;
    public readonly contactEmail: string;
    public readonly clientId: DomainID; // or ClientID, not essential
}

However, here is the confusing point: now this looks like an entity (after all, I'm essentially denormalizing an entity from a different context as a value object). The "identity" doesn't really make sense here, as I wouldn't care about the clientId when checking for Client equality.

To put it in other words: for all practical purposes warehouse does not care about client identity. Also, warehouse can not change any client details. But, there exists the implicit understanding that we are shipping to the same client - client with same identity.

Am I fine modeling Client as a value object? Is this a common trap and I'm just taking the "Value Objects over Entities" rule to an absolute level?

Marian
  • 3,789
  • 2
  • 26
  • 36
  • It is not an entity, your thinking is correct. It's just some recorded details at the point the order was created. – Worthy7 May 09 '21 at 16:26

2 Answers2

4

The usual answer is that you reference the Client by id, rather than caching its properties

class Order extends AggregateRoot {
    private clientId: DomainID; // or ClientID, not essential
    private shipping: Shipping;
    private items: Array<OrderItem>;
}

You would only pull in cached copies of the other client properties if you needed them to maintain the integrity of the Order itself.

The clientId in Order gives you the hook you need to get at a copy of the client data when you need it. The hook would often be fulfilled by having a domain service that understands how to find a copy of the data that you need from the client id.

VoiceOfUnreason
  • 52,766
  • 5
  • 49
  • 91
  • 1
    Thank you. I started writing a huge edit with additional information, and realised a lot of things while doing so. Like, for example, that `Client` and `ClientID` could actually be unrelated (imagine ordering a gift for someone else). I'll put the `clientId` into the `Order` aggregate, and have the `receiverClient` as a value object. (And discuss naming with business folks) – Marian Feb 27 '18 at 14:56
3

Another approach, is that you can also keep copy of Client fields in Order AR.

You may ask why?

Because order (business requirement may vary however) is something that happened in single point of time. If an order happened to CustomerId: 1 at given point of time, at this point of time this customer has name: John Doe and contactPhone: 555-abc-xyz. And this is what really matters (again: business requirement may vary however, speak with your domain experts).

If customer change his/her name or contactPhone you may (or may not - depending on use case) update it for customer's PENDING orders, but don't change it for COMPLETED orders (since it wouldn't make sense updating phone number for orders that happened few weeks or years ago).

Maciej Pszczolinski
  • 1,623
  • 1
  • 19
  • 38