Reputation: 31
Let's say we sell cars, customizable cars.
A customer chooses a CarModel
and then starts configuring the CarModel
. In our shop, she can only choose the color of the Steeringwheel
.
Some CarModels can have more types of SteeringWheels than others.
Therefore, we have a Catalog
which contains CarModels
and SteeringWheels
.
A customer can create a CarConfiguration
. She chooses the model and then from the available steering wheels for that model she chooses the color steering wheel she likes.
class Catalog
{
public IReadonlyCollection<int> CarModels { get; }
public IReadonlyCollection<int> SteeringWheels { get; }
public void RemoveSteeringWheel(int steeringWheelId)
{
...
}
}
class SteeringWheel : AggregateRoot
{
public int Id { get; }
public string Color { get; }
public decimal Price { get; set; }
}
class CarModel : AggregateRoot
{
public int Id { get; }
public decimal Price { get; set; }
public IReadonlyCollection<int> SteeringWheels { get; }
public void AddSteeringWheel(int steeringWheelId)
{
...
}
public CarOrder CreateCarOrder(int steeringWheelId)
{
return new CarOrder(...);
}
}
class CarOrder : AggregateRoot
{
public int Id { get; set; }
public CarConfiguration CarConfiguration { get; set; }
}
class CarConfiguration : ValueObject
{
public int CarModelId { get; set; }
public int SteeringWheelId { get; set; }
}
For this to work there is an invariant that the available steering wheels for a car model must always be present in the catalog. To enforce this invariant, we must guard (at least) two methods:
AddSteeringWheel
on CarModel
; we can only add a SteeringWheel
if it is available in the Catalog
RemoveSteeringWheel
on Catalog
; we can only remove a SteeringWheel
if it is not configured on any CarModel
.How to enforce this invariant? CarModel does not know about the SteeringWheel collection on Catalog and Catalog doesn't know anything about CarModel's steering wheels either.
We could introduce a domain service and inject repositories into that. That service would be able to access the data from both aggregates and be able to enforce the invariants.
Other options are to create navigation properties and configure the ORM (Entity Framework Core in my case) to explicitly load those relations.
And probably many more, which I can't think of right now…
What are the most elegant/pure-ddd/best practice options to achieve this?
Upvotes: 3
Views: 604
Reputation: 57377
How to enforce invariants on collections across aggregates
Fundamentally, what you have here is an analysis conflict. When you distribute information, you give up the ability to enforce a combined invariant.
For example:
For this to work there is an invariant that the available steering wheels for a car model must always be present in the catalog
So what is supposed to happen when one person is updating a CarConfiguration concurrently with another person modifying the catalog? What is supposed to happen with all of the existing configurations after the catalog is changed?
In many cases, the answer is "those activities are both to be permitted, and we will clean up the discrepancy later"; ie we'll attempt to detect the problem later, and raise an exception report if we find anything.
(If that answer isn't satisfactory, then you need to go back into your original decision to split the information into multiple aggregates, and review that design).
Pat Helland has a lot of useful material here:
In effect, your local calculation include stale (and possibly outdated) information from somewhere else, and you encode into your logic your real concerns about that.
Upvotes: 1
Reputation: 1210
Invariants between aggregates can't be transitionally consistent, only eventually. So when you add a steering wheel to your carModel, you raise an event saying steeringWheelUsedbyCarModelEvent, you catch that event in Domain event handler and update the steering wheel. Steering Wheel aggregate holds an id(or collection if can be used by multiple car configurations) of the carmodel it is assigned to.
Upvotes: 0
Reputation: 429
Well first of all, it might be that CarModel
knows something of SteeringWheels
, as I assume that if you add a SteeringWheel
with a Price
, the Price
of CarModel
changes?!
So there probably should be a Value Object or Entity as part of the CarModel
aggregate that represents that.
Furthermore, I think you need a command handler, that knows of both, and decides if the provided SteeringWheel
is valid, before trying to add that to the CarModel
, that on its own has to decide if adding the SteeringWheel
is allowed, trusting the command handler that the reference of the SteeringWheel
is valid.
Upvotes: 0