Design Patterns – Forcing Aggregate Root Child Access Through the Aggregate Root

design-patternsdomain-driven-design

Context

I'm developing an application using a Domain Driven Design approach. I want to use a design pattern wherever appropriate and apply all SOLID principles.

Scenario

I have an order and I want to allow clients to add order lines to it.

Consider the following pseudo code:

class Order
    OrderLines[] (readonly)
    AddOrderLine(orderLine)

class OrderLine
    Product
    Amount

class OrderLineFactory
    OrderLine Create(product, amount)

class OrderLineRepository
    Save(orderLine)

Now lets add an OrderLine to the Order:

Order.AddOrderLine(OrderLineFactory.Create(product: "Chocolate Cake", amount: 3))

By the way, the OrderLineFactory is there because in my case, there is some relatively complex logic involved with creating order lines. This is one of the main reasons for applying the factory pattern.

Problem

The problem is that I could do this instead…

orderLine = OrderLineFactory.Create(product: "Chocolate Cake", amount: 3)
OrderLineRepository.Save(orderLine)

…and get around the Order.AddOrderLine method, effectively short-cutting any checks that are performed in that method (e.g. protecting against duplicates or limiting the order amount).

Working towards a solution

An argument could be that there shouldn't be a Save method on OrderLineRepository, because an OrderLine isn't an aggregate root. This would (just like I want) render the client incapable of storing the bare OrderLine except from adding it to the Order and saving the Order consequently.

However, having a Save method on repositories that concern non-root aggregates, allows for saving changes made to them directly, such as:

orderLine = orderLineRepository.Get(31923)
orderLine.Amount = 5
OrderLineRepository.Save(orderLine)

Not being able to save the OrderLine would require me to do something like…

order = OrderRepository.FindByOrderLineId(31923)
order.OrderLines[31923].Amount = 5
OrderRepository.Save(order)

…which isn't all that efficient or practical. I could add a requirement to provide the (actually redundant) Order ID as well but that would put some unnecessary burden on the client.

So lets say I want to stick with my OrderLineRepository.Save method and still find a way to stop my client from being able to get around my Order.AddOrderLine method.

All I can think of now, is dropping the OrderLineFactory and creating a domain service that combines the creation and the assignment.

class OrderService
    AddOrderLine(order, product, amount)

So we can:

order = orderRepository.Get(123)
OrderService.AddOrderLine(order: order, product: "Chocolate Cake", amount: 3))
orderRepository.Save(order)

I don't really like the service combining multiple concerns here though, those of creation and assignment. That would call for… a factory… again.

Okay, let's consider a private factory then, one that's not available to the client but to the domain service alone. In my case I would get stuck with unit testing and Inversion Of Control. I'd be creating an uninjectable dependency, since my client wouldn't be able to register my non-public factory.

Summary

  • I want my client to be able to create order lines through the Order class and in no other way
  • I don't want to mangle multiple concerns in a single class
  • I want to keep OrderLine creation outside of the Order class
  • I'd like to keep using a factory for that

So I'm a little stuck here, trying to find the best way to deal with this. Any suggestions?

Best Answer

I feel like you were really close, but just missed it

order = orderRepository.Get(123)
OrderService.AddOrderLine(order: order, product: "Chocolate Cake", amount: 3))
orderRepository.Save(order)

What you were looking for is more like this:

order = orderRepository.Get(123)
order.AddOrderLine(orderService: orderService, product: "Chocolate Cake", amount: 3))
orderRepository.Save(order)

Domain services support queries within the domain model - they don't write changes to the model; the aggregates change and protect the domain state. So as a rule, you want to pass the domain service to the aggregate, then let the aggregate pass current state to the service, as needed. For instance

// Order.AddOrderLine()
orderLine = orderService.createOrderLine(this.id, product, amount);
this.lines.add(orderLine);

Note the (implied) separation of responsibilities

  • The domain service creates an instance of the orderLine, using only the context provided to it. But there's no persistence here, it's just transient data in memory at this point.
  • The aggregate then evaluates the orderLine to determine whether or not it satisfies the invariant. If it does, the orderLine will become part of the graph of entities reachable via the aggregate root.

However, having a Save method on repositories that concern non-root aggregates, allows for saving changes made to them directly

That's not a good thing?

orderLine = orderLineRepository.Get(31923)
orderLine.Amount = 5
OrderLineRepository.Save(orderLine)

But if this orderLine is part of an order aggregate, then presumably the Order is expected to check that the state of the order is consistent. It can't do that if you insist on being able to mutate the subordinate entities directly.

Part of the point to aggregates is that all changes to the domain model must go through paths that force the model to remain consistent. Are all of the OrderLine methods going to walk back up the graph to the Order to ensure that the invariant is still satisfied?

Of course, this might actually be a hint that the aggregate boundaries are in the wrong place. If you should be able to modify OrderLine without the entire Order, then perhaps OrderLines are aggregate roots, and not merely subordinate entities. Your domain experts might tell you that discrepancies between the Order and OrderLines aren't actually particularly expensive; ensuring that they are rare (rather than eliminating them entirely) may suffice. Horses for courses.

Related Topic