Ghini Antonio
Ghini Antonio

Reputation: 3122

Can entities inside the aggregate be accessible or visible externally to the aggregate?

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

Answers (2)

Daniel
Daniel

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:

  • Don't model reality. The domain model is an abstraction designed to solve a particular problem.
  • Don't model based on data relationship. Every association must be there to enforce a rule or invariant and not because those objects are related in real life. Start modeling based on behavior.
  • Prefer small aggregates.
  • Prefer modify a single aggregate at the same time (use case / transaction), and use eventual consistency to update other aggregates.
  • Make associations between aggregates by identity only.

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 the Course.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

choquero70
choquero70

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

Related Questions