The simplified scenario is the following: there is a BC (Bounded Context) called "tasks" which contains the Task
Aggregate, and a BC called "meetings" which contains the Meeting
Aggregate.
// in BC "tasks"
class Task extends AggregateRoot {
private TaskId taskId
private string name
private string description
...
static func register(TaskId taskId, ...): Task { ... }
func rename(string newName) { ... }
...
}
// in BC "meetings"
class Meeting extends AggregateRoot {
private MeetingId meetingId
private DateTime meetingDate
...
static func plan(MeetingId meetingId, ...): Meeting { ... }
func postpone(DateTime newMeetingDate): void { ... }
func scheduleTask(TaskId taskId): void { ... }
...
}
You can schedule Task
s for a Meeting
, which will be discussed when the meeting happens, but there are a few rules:
- the person which created the
Task
must explicitly mark it as "ready for meeting", because the creation process can be long and theTask
can be "incomplete" for a while (e.g. document must be added but were not sent, the description is not clear or incomplete...) - a
Task
can only be scheduled for a singleMeeting
, at the end of which anOpinion
must be expressed on theTask
(something along the line of "is valid", "is invalid", "ok but this needs to be changed") - there must exist an API to fetch all
Task
s eligible to be scheduled for the nextMeeting
(i.e. not draft but not already added to anotherMeeting
)
I am not sure how and where to model the state relative to the status
of the Task
("draft", "ready for meeting", ...) and about the Opinion
.
What I've tried so far was to add a status
property to Task
which starts at "draft" and can be changed to "ready for meeting" via a specific operation:
class Task extends AggregateRoot {
...
private Status status = Status.draft
...
func markAsReadyForMeeting(): void {
// let's ignore other checks, Domain Event publishing etc.
this.status = Status.readyForMeeting
}
...
}
But at this point I don't know:
- how to create the fetch API, and in which BC it should live, since part of the information about the
Task
availability is on the "tasks" BC (isTask
draft?) and another part is in the "meetings" BC (is thisTask
already scheduled in aMeeting
?) - how to not create a two-way link between
Task
andMeeting
, since aMeeting
must hold to a list ofTaskId
s, but if I were to add toTask
'sStatus
the casescheduled(MeetingId)
it would feel like a duplication of information which must be kept in sync - the
Opinion
s are expressed in the context of aMeeting
, but should be saved on aTask
... so what?
The other thing I have thought of was to have a "simplified" Task
model in the "meetings" BC and manage the status in there and not in the "tasks" BC. At this point there will be no Status
or Opinion
in the "tasks" BC, and the act of "making a Task ready for meeting" will be implemented on the "meetings" BC and not in the "tasks" one.
I have the feeling that this can be a better approach since it appears to me that the "meetings" BC could operate in autonomy, but it also feels that in this way there is a lot of duplication of data between the two BCs (both have a complete list of all Task
s, albeit the contained information is different).
Is my modeling wrong, there is something I'm missing? Or should more integration effectively exist between the two BCs?
As a final note: the two BCs are more complex than this simplified example and are composed of more parts, and I believe that they should remain separated, but I still remain open to explore a "refactoring" approach.
Bounded contexts should be designed around use cases and not object structures like persistence model do. You are partly right in the approach of putting the ready-for-meeting (RFM) state and the Opinon concepts in the Meeting context. The justification behind that is that these concepts do not exist outside of the meeting context, ie: there would not be a ready-for-meeting status, nor Opinions if there was no meeting in your system.
What you are missing, in my opinion, is that you should not confuse draft and RFM states. Draft status should be handled in the Task context as you already do, as it controls the state of the Task outside the meeting concept. The Meeting context would subscribe to Task "undrafted" events. This would allow the Meeting BC to maintain a list of non-draft tasks, and associate them with meetings. The Meeting context is then able to provide a list of undrafted tasks, not associated with a meeting, which is your definition of RFM tasks.
The Task context don't need to know whether the Task is associated to a meeting or not, and if the meeting is planned or has already happened. If you want to prevent the Task context from altering tasks once they are associated with a meeting, you could maintain a readonly state in the Task context. The Task context would subscribe to a "task associated with meeting" event in the Meeting context and would update the readonly state of the Task.