Leks
Leks

Reputation: 31

DDD - How to enforce invariants on collections across aggregates

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:

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

Answers (3)

VoiceOfUnreason
VoiceOfUnreason

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

DmitriBodiu
DmitriBodiu

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

Corniel Nobel
Corniel Nobel

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

Related Questions