I have a Bounded Context where two Aggregates are central to the business in that particular model. The model is starting to express quite well, but the association between those needs to be bidirectional (I am exploring and making alternative designs, so probably I ended up finding a better approach).
Let's say A
and B
are the aggregate roots of each aggregate. A
and B
has a n:m
association. Invariants around A
need track at least the number of instances of B
, and there is high concurrency contention on A
(many users modifying instances of A
at the same time, very likely the same A
) and business says invariant here must be enforced (no eventual consistency is allowed). If an user is trying to "attach" an instance of B
to an instance of A
(this has a concept in the UL, of course), the rules requiere inspecting the current state of B
, including the instances of other A
s it relates to. This is crucial, because the operation can be denied depending on the analysis and so the aggregate's invariants are preserved. There is low concurrency contention on B
(it's very, very unlikely that different users will attempt to modify the same B
, and is probably a mistake by the user).
Since there is low contention on B
, there is little damage on save both aggregates in the same transaction. However the "attach" operation on A
requieres only the current state of B
, so it is possible to keep them out of sync in the current transaction, but the next one (that could be started immediately) needs to see the changes of the last one.
I know by reading the Blue Book and the Red Book that a Repository is allowed to return, in some cases, a Value Object instead of an entire aggregate, but what about let the repository hydrate part of the state of B
derived from the state of A
at the database level?
The current transaction could mutate and save the state of A
without modifying B
. Then the next transaction retrieves B
again, but this time the changes are visible since B
is regenerated inspecting the state of A
in the repository.
My question is not about the implementation itself, it's about the model: with this approach the model expresses that A
relates to many B
s and A
has the right operation to modify that collection (the "attach" operation). But even if B
relates to many A
s, the use cases requiere to navigate from B
to its A
s and therefore a method on A
allows this navigation, this collection of A
's inside B
is not managed explicitly by the "attach" operation since now A
doesn't have a method to mutate its collection; it's implemented in the infrastructure inside the Repository, through a database lookup and reconstitution. Yes, the code model still expresses the existence of the relationship from B
to A
, but is unclear where it comes from and how is calculated.
What should I favor? The more explicit code model that keeps the bidirectional association in code, managed through the "attach" operation, even if it involves dealing with maintaining references in sync and mutate two different aggregates in the same transaction; or let the repository reconstitute one of those aggregates, examining the state of the persisted state, but leaving part of the semantics of the association inside the repository implementation?
UPDATE: More context
The domain, simplified, is very similar to this: an educational center where there are classrooms, courses and students. Students are primarily managed in another BC (personal data, etc) but here they can also mutate since they are allowed to change the contract/agreement almost at any time (this is managed in another BC too), and that determines the kind of courses they can be enrolled in. The unique number of that contract is relevant and the Student
entity needs to keep track of them. The student start to attends the classes but can change to another course at any time, so all the records about attendance (scores, etc) belongs to the Student
entity.
An user meet with an student and the user register the student in courses based on the current contract, but there are many rules for that beyond capacity, most of which requiere to explore the current status of the Student
, as previous assignments or attendance. The business is quite restrictive in that and one of the system goals is to enforce those rules without leaving lot of choices users. Different strategies of enrollment can be picked based on the permissions of the user, though. Oh, and there are many users performing the same operation with different students at the same time, and business request that users needs to see the changes the "most real time" as possible.
There are more rules of course, some of them relating to the capacity of the course, but that is a glimpse of the domain. I modeled both, Course
(before referenced as A
) and Student
(before as B
) as Aggregate Roots. To fulfill the enrollment use case (the "attach" operation) the Course
is retrieved from its Repository and collaborates with a Domain Service. Course
needs the ids to its current students to, at least, calculate its capacity, since another user can be modifying the same course at the same time (a concurrency exception is thrown and rules must be validated again); there is no use case to hold a direct reference to a Student
from the Course
, only by id. An instance of Student
is needed to inspect its current contract and the Course
s is/have been enrolled, including those of the "same session of enrollment", in order to decide the result of the operation requested.