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;
}
Invariants:
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.
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:
PlanRepository
to find out if it's completepublic 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.
Objective
aggregate roots for the planpublic 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()
.
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 Objective
s.
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.