Life inside an Aggregate Root, part 1

This is the first half of a two-part article. Read the second half here: Life inside an Aggregate Root, part 2.

One of the most important concepts in Domain Driven Design is the Aggregate Root — a consistency boundary around a group of related objects that move together. To keep things as simple as possible, we apply the following rules to them:

  1. Entities can only hold references to aggregate roots, not entities or value objects within
  2. Access to any entity or value object is only allowed via the root
  3. The entire aggregate is locked, versioned and persisted together

It’s not too hard to implement these restrictions when you’re using a good object-relational mapper. But there are a couple of other rules that are worth mentioning because they’re easy to overlook.

Real-life example: training programme

Here’s a snippet from an app I am building at work (altered slightly to protect the innocent). Domain concepts are in bold:

A Training Programme is comprised of Skills, arranged in Skill Groups. Skill Groups can contain Sub Groups with as many levels deep as you like. Skills can be used for multiple Training Programmes, but you can’t have the same Skill twice under the same Training Programme. When a Skill is removed from a Training Programme, Individuals should no longer have to practice it.

Here’s what it looks like, with our two aggregate roots, Training Programme and Skill:

Training Programme, Skill Groups and Skills

Pretty simple right? Let’s see how we can implement the two behaviours from the snippet using aggregate roots.

Rule #4: All objects have a reference back to the aggregate root

Let’s look at the first behaviour from the spec:

…you can’t have the same Skill twice under the same Training Programme.

Our first skill group implementation looked this like:

public class TrainingProgramme
{
    public IEnumerable<SkillGroup> SkillGroups { get; }

    ...
}

public class SkillGroup
{
    public SkillGroup(string name) { ... }
    
    public void Add(Skill skill)
    {
        // Error if the Skill is already added to this Skill Group.
        if (Contains(skill))
            throw new DomainException("Skill already added");

        skills.Add(skill);
    }

    public bool Contains(Skill skill)
    {
        return skills.Contains(skill);
    }

    ...

    private IList<Skill> skills;
}

What’s the problem here? Have a look at the SkillGroup’s Add() method. If you try to have the same Skill twice under a Skill Group, it will throw an exception. But the spec says you can’t have the same Skill twice anywhere in the same Training Programme.

The solution is to have a reference back from the Skill Group to it’s parent Training Programme, so you can check the whole aggregate instead of just the current entity.

public class TrainingProgramme
{
    public IEnumerable<SkillGroup> SkillGroups { get; }

    // Recursively search through all Skill Groups for this Skill.
    public bool Contains(Skill skill) { ... }

    ...
}

public class SkillGroup
{
    public SkillGroup(string name, TrainingProgramme programme)
    {
        ...
    }

    public void Add(Skill skill)
    {
        // Error if the Skill is already added under this Training Programme.
        if (programme.Contains(skill))
            throw new DomainException("Skill already added");

        skills.Add(skill);
    }

    ...

    private TrainingProgramme programme;
    private IList<Skill> skills;
}

Introducing circular coupling like this feels wrong at first, but is totally acceptable in DDD because the AR restrictions make it work. Entities can be coupled tightly to aggregate roots because nothing else is allowed to use them!

Continue reading: Life inside an Aggregate Root, part 2

October 13, 2009

8 Comments

Kevin Berridge on October 13, 2009 at 11:31 am.

Not sure if you’re planning on covering this in a future post, but I’m curious. If you were using NHibernate (or any ORM really), how would you set the back pointer from SkillGroup to TrainingProgramme? I’m assuming the ORM wouldn’t know how to do this for you, so you would have to intercede at some point.

Richard on October 13, 2009 at 11:58 am.

Kevin: good question – I inject the aggregate root into the entity when it is constructed i.e., when you create a SkillGroup, you specify which TrainingProgramme it belongs to (see example). If you’re using NHibernate, the TrainingProgramme->SkillGroup association is inverse.

Part 2 will be about making the TrainingProgramme responsible for constructing child SkillGroups :)

Jaime Metcher on January 28, 2010 at 12:54 pm.

I don’t really have a problem with the backreference from SkillGroup to TrainingProgramme, but having a method TrainingProgramme.addSkill(skillGroup,skill) that does the uniqueness check before calling skillGroup.add(skill) seems to me to be putting the responsibility where it belongs. The requirement for whole of programme skill uniqueness is part of the specification of TrainingProgramme, and yet it’s being implemented by SkillGroup. To put it another way, looking at the code of TrainingProgramme would not tell you that requirement existed.

Fred Wang on August 10, 2010 at 9:46 am.

Hi Richard,

In my opinion,there are two kinds work of aggregate root:
1.as a container(or lifemanagement) ,it is the root,we have a relation from the root to the leaves .It is a unidirectional relationship.
2.for validation task(or some thing similar),just as your example.So the leaves need a reference to the root.It is a bidirectional relationship.

But , I think the bidirectional is very hard to maintain, if we don’t need the validation function,do we need a bidirectional?
Another question is ,if I have a deep graph,how to set the reference to the root and mantain the bidirectional relationship by nhibernate?

Cheers,
Fred Wang

Thomas Han on August 24, 2011 at 10:44 pm.

You’ve stated that
Entities can only hold references to aggregate roots, not entities or value objects within.

Why can’t the entities hold a reference to VO???

Entities can’t hold reference to other entities due to aggregate boundary stating is can’t hold reference other entities but through the aggregate root but there is no reason entities can’t hold a reference to VO.

Ali Çevik on April 19, 2012 at 8:41 pm.

How about this: allow adding a Skill only through a call to the TrainingProgramme.

trainingProgramme.AddSkill(skillGroupName, skill)

This way you could make the validation before adding the Skill to the SkillGroup yet without a circular dependency.

Leave a Reply