The example domain problem
There is a student attendance tracking system that keeps records of student attendances of ExerciseGroup
s.
Course
is a top-level component, AR, describes a generic info about the learning course (name, description)- Every
Course
hasExerciseGroup
s that describe groups where students can practice.ExerciseGroup
holds the name of the group and list of signed-up students and tutors.ExerciseGroup
has no sense outside theCourse
(from the domain), therefore it's under theCourse
AR. - Every
ExerciseGroup
contains a list ofSessions
that describe occurrings of theExerciseGroup
aUser
can attend. Is defined by (datetimeBegins, datetimeEnds and a set ofAttendance
objects). Attendance
is a VO record that holds info about student attendings. Contains a reference to theUser
(via Id) and some statistical data.
What approaches I've used so far:
1. Reference by identity outside the AR.class Group {
// ...
Set<UserId> studentIds;
Set<UserId> tutorIds;
}
2. Direct references inside the AR.
class Course {
// ....
List<Group> groups;
}
3. ExerciseGroup
/Session
mutable operations done only by Course
AR.
Following the idea that:
aggregate roots exist to protect the data within them... [you must not] tear the aggregate roots internal structures and do operations on the internals directly without incorporating the aggregate root, [since] the aggregate root can no longer validate the operation is valid.
@david-packer (https://softwareengineering.stackexchange.com/a/354667/169341)
I have the following mutable operations in AR:
class Course {
ExerciseGroup addExerciseGroup(String exGroupName...);
void removeExerciseGroup(ExerciseGroupId exGroupId);
void attendExerciseGroupSession(ExerciseGroupId exGroupId, SessionId sessionId, UserId userId...);
// etc
}
While still having read-only access to the underling entities:
class Course {
List<ExerciseGroup> exerciseGroups() {
// save because all mutable operators in ExerciseGroup
// are `protected` and available nobody but Course AR.
return unmodifiableList(this.exerciseGroups);
}
// etc
}
The problems and questions
- What is your overall personal feeling about the chosen domain model? Does it make sense for you? Isn't it too fat?
Should I delegate underlying entities of the AR to mutate its children or put the whole logic in the AR? For example, I can delegate
ExerciseGroup
to askSession
to createAttendance
:class Course { void attendExerciseGroupSession(ExerciseGroupId gId, SessionId sId, UserId uId) { this.eGroups.getById(gId).attendSession(sId, uId); } } class ExerciseGroup { void attendSession(SessionId sessionId, UserId userId) { this.sessions.getById(sessionId).attend(userId); } } class Session { void attend(UserId userId) { this.attendances.add(new Attendance(userId, LocalDateTime.now())); } }
Or I can create Attendance directly in
Course
AR.class Course { void attendExerciseGroupSession(ExerciseGroupId exGroupId, SessionId sessionId, UserId userId) { this.exerciseGroups.getById(exGroupId) .sessions().getById(sessionId) .attendances().add(new Attendance(userId, LocalDateTime.now())); } }
While the first approach does make more sense for me, it became extremely complicated to not forget to "propagate" some methods to the top AR. One change in the Session
(e.g. change void in attend(.)
to Attendance
) requires changes in all top-level classes.
- How to cope with deep nested objects in terms of AR method complexity? Let's say there will appear one more object btw
Session
andAttendance
-Meeting
, then method inCourse
may becomeaddExerciseGroupSessionMeetingAttendance(ExerciseGroup gId, SessionId sId, MeetingId mId, AttendanceParams.. o)
that is extremely long to me and unclear in terms of (2) - Should
addAREntity(EntityParam1,EntityParam1N)
returnEntityId
or it's valid to return the wholeEntity
(if it can be assured it is unmodifiable outside of the domain e.g. all mutable methods areprotected
)? The problem with returning justEntityId
is that usually you'll require the while object right after creating the object.
For example, an endpoint POST /courses/{courseId}/groups/{groupId}/sessions/
is very likely should return a complete Session
object (e.g. to get all autogenerated data [id?] not included in DTO given to the endpoint). Receiving a SessionId
from Course#addGroupSession(dto.param1(),..)
means I need course.group(groupId).session(sessionId)
fulfill this requirement.
- Will it be ok to return AR's users nested entities if we are sure they are immutable from outside (e.g. all mutable methods are protected).
In other words, is it okay to do course.getGroup(groupId).getSession()(sessionId)
from the example above (assuming .group(groupId)
and .session(sessionId)
return unmodifiable objects), or it's better to have course.getGroupSession(groupId, sessionId)
?
Thank you.
A day after (update)
I revisited my model and found it very fat, hard to maintain and designed with false invariants in mind that caused transactional failures. Nothing about creating a new session item should logically interfere with editing a course for example.
What I came up with after deeper transactional analysis is the following model (clickable):
The question I was using to separate boundary context is: can/should this and that in the same transaction? When you Session#attend, should it be done in the same transaction with Course [blocked]? Definitely it's not etc.
To fulfil the condition that session should not be outside the Group, I just put GroupId to the Session's controller and hope for eventual consistency :)
Questions left
Will it be ok to return nested entities from an AR if we are sure they are immutable from outside directly (e.g. all mutable methods are protected) [not by AR, the owner of the entity]. In other words, is ok to do this:
class Course { List<Group> groups; List<Group> getGroups() {return Collections.unmodifiableList(groups);} public setGroupName(GroupId gi, String name) { this.findGroup(gi).setName(name); } } class Group { String name; // Protected so Course#getGroups users won't be able to mutate Group protected setName(String name) { this.name = name; } }
Should I delegate underlying entities of the AR to mutate its children or put the whole logic in the AR? (see details above, #2 in The problems and questions)