Search code examples
domain-driven-designddd-repositories

Choosing aggregate roots without breaking invariants at the same time


I'm trying to apply DDD to a model. How can I cluster the entities into aggregates, without breaking the invariants I have?

I have 4 entities (simplified):

public class Plan {
    public bool Completed;
    public DateTime StartDate; 
    public DateTime EndDate;
    public IList<Objective> Objectives;
}

public class Objective {
    public bool Completed;
    public IList<Person> Persons;
    public int TargetMeetingCount;
}

public class Person {
    public string Name; 
    public IList<Meeting> Meetings;
}

public class Meeting {
    public DateTime StartDate;
    public DateTime EndDate;
}
  • A plan has 0..* objectives
  • An Objective has 0..* Persons
  • A Person has 0..* Meetings

Invariants:

  • A plan is completed if all objectives in the plan are completed.
  • An objective is completed if the TargetMeetingCount is greater or equal to the Number of meetings for each person.
  • Each meeting must be within the same date range as the Start and End date of the plan.

This is the way I'm reasoning about the solution so far:

If the Plan is an Aggregate Root with all Objectives, the problem is that as there are too many Persons and Meetings. We don't want these many objects in the Plan aggregate. Calculating and retrieving data with the PlanRepository could be very slow. Also if you want to show a list of plans, you don't want to retrieve this data.

So then the options is that we could make the Objective an AR as well and detaching the objectives, that would simplify the code a great deal. An application service would assemble a "PlanViewModel" for the UI layter, by using a PlanRepository and an ObjectiveRepository and satisfy the invariants in the Plan.

However, if the Objectives are detached, we would break the invariant that a plan is completed if all objectives are completed, as the Domain Model itself can no longer verify this. So the "PlanViewModel" would be correct but not the Plan Model.

Worth mentioning here is that a Meeting object has many more properties in addition to the date range in reality, that we could filter by through the AR Plan, also that the Meeting object has no real purpose, as the actual completion status of an objective would be calcualated by a SQL query.

Not sure if I'm going the wrong way with all of this. But I have a feeling that eventual consistency could be used here, but I'm not really sure how that would apply that. But maybe I'm only a completely wrong track here, I'm new to DDD and I hope this makes sense.


Solution

  • The design you've chosen for your aggregates is something I see a lot of - too focused on has a relationships and the convenience of a nice object graph gained through composition.

    If the Plan is an Aggregate Root with all Objectives, the problem is that as there are too many Persons and Meetings.

    You are correct. This is also a disaster for concurrency. You could get a lot of failing transactions because someone modified the Plan while someone else modified a Person.

    Keeping your aggregates small leads to better concurrency, speed and scalability among other things.

    I would probably treat Plan and Objective as aggregate roots. You might be wondering how do you keep the invariant between them consistent? Well you have to make a choice between transactional consistency or eventual consistency. In this case you'd be using eventual consistency.

    When an Objective is marked as completed a domain event ObjectiveCompleted could be raised. At the application layer you have a service listening for the event. The event listener can:

    Use the PlanRepository to find out if it's complete

    public function onObjectiveCompleted(ObjectiveCompleted $event)
    {
        $planId = $event->getPlanId();
        $plan = $this->planRepository->find($planId);
        $isComplete = $this->planRepository->isComplete($planId)
    
        // Is the stored completed value consistent
        // with the newly computed value from the database?
        if (!$plan->isCompleted() && $isCompleted) {
            // They are not consistent
            $plan->markCompleted();
        }
    }
    

    The PlanRepository has access to the data store so it can do a query to determine if the Plan is complete.

    Load all Objective aggregate roots for the plan

    public function onObjectiveCompleted(ObjectiveCompleted $event)
    {
        $planId = $event->getPlanId();
    
        $plan = $this->planRepository->find($planId);
        $objectives = $this->objectiveRepository->findByPlan($planId);
    
        $plan->determineIfCompleted($objectives);
    }
    

    In the determineIfCompleted() method you would simply loop through all the objectives checking if they are completed. If they are then you'd update the completed field of the Plan to true. This is very easy code to unit test too which is great.

    You would try to name the method as close as possible to your ubiquitous language. When you and your team talk about a Plan being completed maybe you call it "updating the status". In that case call the method updateStatus().

    Conclusion

    The first approach pushes the logic for determining if a Plan is complete into the data store. This may or may not be what you want.

    The second approach keeps the logic right in the domain which I like but it could be less efficient when a Plan has 1000s of Objectives.

    Also, do not concern yourself with view/presentation related concepts when working on the domain model. The domain model is for solving the business problem. Domain model and view model are separate. In certain cases you might use repositories from the domain to get data for the view (for convenience) but quite often you will just do directly to the data store. This is more efficient and gives you greater flexibility as you can do very complex queries that return data which is specifically for a given view (web page/HTTP API/desktop app etc).

    I'm not going to design all your aggregates for you as I'd be here all night and don't know enough about your domain to do so. Proper aggregate design can be one of the toughest parts of DDD. Take your time and do it right. It will pay off in the long run.