Reputation: 3122
I'm new to DDD and my question might seem trivial to many of you.
Consider the case of Student and Course.
The student can enroll the course only if the student's age is above the minimum age required to enroll that course.
From my point of view, Student and Course can be considered as aggregate where Student is the root entity, Course the child entity, and age the invariant to respect.
Student should have a method Student.SubscribeTo(Course course) and the method should enforce the invariant Student.Age >= Course.MinAge
and generate an exception otherwise.
Is this correct in the DDD approach? Or should I pass to SubscribeTo only CourseId? Student.SubscribeTo(int CourseId)
From my point of view, if there is no way to break the invariant, access to Course externally to the aggregate should be allowed. If I change the Course.MinAge in some other places of my code I don't break my business requirements, as I want the age be respected only when subscribing the course and I don't mind if later the Course.MinAge changes.
Different case if business requirements state: when Course.MinAge changes students already enrolled in the course should be removed from the course if Student.Age < Course.MinAge
.
Upvotes: 3
Views: 183
Reputation: 2767
I'm also learning DDD and few days ago I ask a question to a similar problem you can find here.
What I've learned is that the only real answer is: it depends. There is no right or wrong approaches per se, everything must serve a purpose to the business problem and its solution. Although, there are guidelines and rules of thumb that can be very useful, for instance:
I think the problem in your scenario is that there is a lot missing. Who owns that association and why? Is there any other use case that span both, student and course? Why do you put student.SubscribeTo(course)
instead of course.enroll(student)
? Remember that the goal of DDD is tackle a complex domain logic so it shines when approaching the write side of the model, the reading side can be done in many different ways without put a lot of associations into the model.
For what you've said, just validating the age is probably not and invariant that requires making a large aggregate:
If I change the
Course.MinAge
in some other places of my code I don't break my business requirements, as I want the age be respected only when subscribing the course and I don't mind if later theCourse.MinAge
changes.
Then there is no reason to enforce Student
and Course
to be consistent at all times (in this particular context/scenario), and there is no necessity of make them part of the same aggregate. If the only rule you need to enforce is Student.Age >= Course.MinAge
you can stick with a simple:
Student.SubscribeTo(Course course)
where Student
and Course
are not part of the same aggregate. There is nothing against loading two different aggregates and use them in the same transaction, as long as you modify only one. (Well, there is nothing against modify two aggregates in the same transaction either, is just a rule of thumb, but probably you don't need to break it in this case).
Here Student.SubscribeTo
will enforce the rule regarding the age. I have to say, it sounds "weird" to let the Student
validate his own age, but maybe it is just right in your model (remember, don't model reality, model solutions). As the result, Student
will have a new state holding the identity of the course and Course
will remain unchanged.
Different case if business requirements state: when Course.MinAge changes students already enrolled in the course should be removed from the course if
Student.Age < Course.MinAge
.
Here you have to answer first (with the help of a domain expert) some more questions: Why should they be removed? Should they be removed immediately? What if they are attending the class in that moment? What does it mean for a student to be removed?
Chances are that there is no real need in the domain to remove the students at the same time the MinAge is changed (as when the operation is only considered successful when all happens, and if not, nothing happens). The students might need to enter a new state that can be solved eventually. If this is the case, then you also don't need to make them part of the same aggregate.
Answering the question in the title, unsurprisingly: it depends. The whole reason to have an aggregate is to protect invariants of a somehow related set of entities. An aggregate is not a HAS-A
relationship (not necessarily). If you have to protect an invariant that spans several entities, you can make them an aggregate and choose an entity as the aggregate root; that root is the only access point to modify the aggregate, so every invariant is always enforced. Allowing a direct reference to an entity inside the aggregate breaks that protection: now you can modify that entity without the root knowing. As entities inside the aggregate are not accessible from the outside, those entities have only local identity and they have no meaning as standalone objects without a reference to the root. It is possible to ask the root for entities inside the aggregate, though.
You can, sometimes, pass a reference to an inner entity to another aggregate, as long as it's a transient reference and no one modify that reference outside the aggregate root. This, however, muddles the model and the boundaries starts to become blurry. A better approach is passing a copy of that entity, or event better, pass a immutable view of that entity (likely a value object) so there is no way to break invariants. If you think there is no invariant to break by passing a reference to an inner entity, then maybe there is no reason to have an aggregate to begin with.
Upvotes: 1
Reputation: 4754
I think that the aggregate you have is not correct. A Course entity can exists on its own, it is not a child entity of a Student entity. A course has its own lifecyle: e.g. if a student leaves the school, the course keep on existing. The course id doesn't depend on the student id. The student can hold the course id, but they are different aggregates.
Anyway, to your question of passing just the course id to the "student.subscribeTo" method if they were an aggregate, the answer is no, you cannot pass the id of a child entity to an operation of the aggregate, as the child entities doesn't have a global identity known outside the aggregate. They have local id into the aggregate.
UPDATE:
Since Course and Student are two aggregates, the rule "student's age must be above the minimum age required to enroll the course" isn't an invariant. Why? Because an invariant is a business rule about the state of just an aggregate, that must always be transactionally consistent. An aggregate defines a transactional consistency boundary.
So, the rule is just a validation rule that must be checked when the student subscribes to a course ("student.subscribeTo" method). Since aggregates shouldn't use repositories, you can pass a domain service to the method, and the student aggregate would double-dispatch to the domain service in order to get the course from the course id.
Take a look at the aggregates chapter of the Red Book IDDD by Vaughn Vernon (pages 361-363) or the article by the same author:
http://www.informit.com/articles/article.aspx?p=2020371&seqNum=4
Hope it helps.
Upvotes: 3