I started to learn about domain driven design and some questions popped into my mind. Let's imagine that i'm building an e-learning application.
I have the following hierarchies:
Section
can have 0 or more Lesson
Lesson
can have 0 or more Resource
The ids of all the entities are globally unique.
Each entity act as their own aggregates, but contain references to to other aggregates. For example: a Lesson
references a Section
by its id.
If we imagine a class for Lesson, it'd be something like this:
class Lesson {
id : string,
sectionId : string,
order: number,
title: string,
description: string,
resources: Resource[]
}
And if we imagine a class for Section is something like this:
class Section {
id: string,
lessons: Lesson[]
}
Now, i can modify a Lesson
name or description without accessing the Section
(since the ids are globally) because there's not invariant rule to break. But, if i'm modifying the ordering of the Lesson
, it does make sense doing it through the Section
, because i need to keep track of the lessons in a particular section to maintain the invariant.
Having said this, here are my doubts:
Section
class. In this scenario, if i'm executing an UpdateLessonTitleUseCase i think it could look like this:Option A)
let mySection = SectionRepository.getById(sectionId);
Section.updateLessonTitle(lessonId, 'new title');
SectionRepository.save(mySection);
But if i need to expose a method for each property, is a pain (because the tree can grow very deep or a particular class have a lot of properties). Another option would be like this:
Option B)
let mySection = SectionRepository.getById(sectionId);
let myLesson = mySection.getLessonById(lessonId);
myLesson.setTitle('new title');
mySection.updateLesson(myLesson);
SectionRepository.save(mySection);
If i'm executing a ChangeLessonsOrderUseCase, it would be something like this:
let mySection = SectionRepository.getById(sectionId);
mySection.updateLessonsOrder(orderData); // orderData is an array of {lessonId: string, order: number}
SectionRepository.save(mySection);
If i'm executing a UpdateResourceLinkUseCase, it would be something like this:
let mySection = SectionRepository.getById(sectionId);
let myLesson = mySection.getLessonById(lessonId);
let myResource = myLesson.getResourceById(resourceId);
myResource.setLink('new link');
myLesson.updateResource(myResource);
mySection.updateLesson(myLesson);
SectionRepository.save(mySection);
In these scenario, Lesson
could not be an aggregate of it's own, right? Because Section
can only return a read-only version of other aggregates and, besides that, SectionRepository
should not access a LessonRepository
, right? So, we'd have only on aggregate, and that's Section
, and the SectionRepository
would take care of saving everything. And, if we go up the tree (or down the tree) it'd have a lot more things to store.
Any help is appreciated in thinking about this. Thanks!
This sounds like an issue I also ran into a couple of times. From my experience I have learned that this kind of deeply nested access from an aggregate root to an entity that seems to be located further down a hierarchical tree sometimes indicates that you did not discover and model the aggregates appropriately.
From this line of code you shared:
let myResource = ResourceRepository.getResourceById(resourceId);
myResource.setLink('new link');
it looks like your are only changing the content of a Resource entity. I don't know the details of your domain model and business requirements but I assume that in your case you have at least one additional aggregate root that can live on it's own rather than just being a child of a Section aggregate. I think this would make your life a lot easier.
So from my point-of-view a Resource what be something that can be referenced in different lessons. If this assumption applies you should consider making your Reference an Aggregate of its own. Then you will only keep a list of Reference ids in your Lesson. I would usually use an explicit Value Object to represent a strongly typed reference to a root entity, such as a "ReferenceId" value object class.
So your Lesson class would look something like this instead:
class Lesson {
id : string,
sectionId : string,
order: number,
title: string,
description: string,
resources: ResourceId[]
}
So in this case changing the link of the Resource would be managed via a ResourceRepository directly rather than going through the whole tree starting at your Section aggregate.
With this approach comes another benefit: in case you reference the same resource in different Lessons you do not need to update it everywhere since the resource is only referenced by an identifier. Of course you need to chnage your approach when provisioning the data as you only store a reference. But from my experience the actual data is in many cases only required when viewing the data at some front end rather than for performing domain logic. So when you know the references when rendering your view models becomes easy as you do not really need to go through the domain repositories when it only comes to viewing data but you can access all the data the way you need it from your database. When viewing a Lesson the Resource id allows you to fetch whatever Resource data you need for rendering.
Be aware that is approach may or may not make sense depending on your business requirements. But if the Lesson -> Reference relationship is somehow comparable to an Order -> User relationship I think it can be a viable option for you.
Another option would be to think of your Lessons as aggregate roots by themselves. This would also give you the option to compile Courses by resuing the same lessons in different courses and thus sections of different courses.
In this case the Lessons would also be referenced by Lesson Ids (and if necessary just some additional information such as a title depending on your needs) within Sections. The question usually is, what kind of information does an aggregate really need to correctly apply it's business rules and invariants.
Update
I want to elaborate further on the additional questions you asked in your comment.
1.) Who's in charge of fetching the actual resources from the lessons, for example?
If you just want to fetch data for viewing a section or lesson on the UI I want use a read model. In this case you can bypass the aggregate repositories alltogether as you to not change any data performing read access to your database should happen in a way that is most appropriate for viewing the data. This is a totally valid approach and also prevents you from unwanted performance issues as loading aggregates via domain repositories is usually designed for performing consistent business transactions while keeping business logic invariants in tact. Also it usually focuses on changing a single aggregate in most cases.
If you need to perform a business operation on the Lesson aggregate and you need Resource data (other than only the Resource id) I would use the Resource repository to load the Resource based on its Id via the Resource repository and than pass in the required data to the corresponding Lesson domain operation.
E.g. if you process an authors request to change, let's say the title of the resource within this lesson (considering you have different titles of the same Resource within a Lesson), I would use a value object, something like LessonResource. This value object would than hold the reference id of the Resource aggregate and the title which is used only inside this Lesson.
This workflow at the application layer (or use case which you refer to) could look like this:
let resource = resourceRepository.findById(changeResourceTitleCommand.resourceId);
let lesson = lessonRepository.findById(changeResourceTitleCommand.lessonId);
lesson.changeResourceTitle(
changeResourceTitleCommand.resourceId,
changeResourceTitleCommand.title,
resource
);
lessonRepository.save(lesson);
Note: the ChangeResourceTitleCommand would be some simple DTO representing the data passed in via, for instance, a REST request, which only holds the data required for this operation.
In this case let's consider your Lesson domain operation (changeResourceTitle()) also needs information from the Resource. For instance, information if for this Resource a globally consistent title should be used which has to be used in all Lessons. It may not be the best real life example but I hope you get the idea :-)
2.) And, let's assume that i have a use case to change the order of the lessons within a section. The section holds the lessonsId, and the Lessons need to be modified and stored. Should the Section domain acccess the LessonRepository?
In this case I would use a SectionLesson value object which holds the id to the lesson as well as the order (position).
In this case you do not even have to fetch the Lesson aggregate data to perform the domain operation on the Section aggregate.
The use case could look like this:
let section = sectionRepository.findSectionById(changeSectionOrderCommand.sectionid);
section.changeLessonOrder(
changeSectionOrderCommand.lessionId,
changeSectionOrderCommand.position
);
sectionRepository.save(section);
Your changeLessonOrder() method could than take care of reording of the lessons within the section.
Note: Depending on your implementation and requirements it might also be even more appropriate to send the whole new ordered list of lessons as defined by the user (author) in the UI to the backend. Either way you would also only need a sorted list of lesson ids to perform the Section domain operation without the need to access any LessonRepository.