Modeling (apparent?) dependency between DDD Bounded Contexts

256 Views Asked by At

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 Tasks 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 the Task 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 single Meeting, at the end of which an Opinion must be expressed on the Task (something along the line of "is valid", "is invalid", "ok but this needs to be changed")
  • there must exist an API to fetch all Tasks eligible to be scheduled for the next Meeting (i.e. not draft but not already added to another Meeting)

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:

  1. 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 (is Task draft?) and another part is in the "meetings" BC (is this Task already scheduled in a Meeting?)
  2. how to not create a two-way link between Task and Meeting, since a Meeting must hold to a list of TaskIds, but if I were to add to Task's Status the case scheduled(MeetingId) it would feel like a duplication of information which must be kept in sync
  3. the Opinions are expressed in the context of a Meeting, but should be saved on a Task... 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 Tasks, 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.

1

There are 1 best solutions below

2
On BEST ANSWER

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.