DDD - Enforcing rules which need to know about multiple aggregate roots
Asked Answered
S

4

9

I'm new to DDD, and currently looking at rebuilding an existing application by starting with a bit of a proof of concept while I'm still finding my way with DDD. My questions here only concern a small part of the domain model, so it may seem overly simplistic at the minute.

It's a scheduling application for nurses who visit patients in their homes. As such, it's clear that "Patient" is one AggregateRoot, and "Nurse" is another AggregateRoot. There is no direct tie for a patient to a nurse, aside from when a nurse is assigned to visit a patient using an "Appointment" entity.

Now, the appointment entity could easily belong to either patient or nurse AR's, or even both seeing as an appointment is a link between the two. As such, I'm also making the Appointment into an AR. So the first question is:

1) Does this modelling sound right? I was originally trying to add a collection of Appointment entities under either the patient/nurse AR's, but as it really belongs to both, it makes sense to be it's own AR. I was then thinking of adding a list of appointment ID's under the Nurse / Patient AR's to link to their appointments, but this would mean a transaction to save an appointment would need to affect multiple AR's at once, which from what I can tell would suggest a bad aggregate design.

Assuming this modelling makes sense so far, I now need to work out the best way of enforcing business rules which concern all 3 of the current AR's. For example, a nurse can't be in more than one place at once, so we can't create an appointment at the same time as another one assigned to the same nurse. It's also only possible to have a single pending appointment per patient. So the second question is:

2) How would you go about enforcing these kinds of rules, which concern multiple different AR's? Obviously the rules would be easy to enforce, and very self contained if the appointments were a nested collection under either the patient or nurse AR. This is now making me question whether my modelling is correct or not.

I've read a lot around BC's and Saga's / Process Managers, but to me this is all part of the same BC so not sure I need anything that complicated. Is it acceptable to simply have a CommandHandler which loads up multiple AR objects and uses their state to determine whether an appointment can be created or not?

If so, and tying back in with Q1 above (assuming I don't store a list of appointment ID's under the nurse / patient AR's), the read model is the only way of easily finding the appointments belonging to the respective nurse/patient - so is it also acceptable to enforce business rules based on the state of the read model rather than an AR from the repository?

Hope this makes sense, and thanks in advance!

Saintebeuve answered 15/10, 2018 at 8:50 Comment(0)
W
6

Does this modelling sound right?

No (but that's not your fault -- the literature sucks). Your aggregates are going to be representations of information, not people who move around in the real world. Rotation Schedules, Duty Rosters, those are the kinds of things that may be aggregates.

For example:

a nurse can't be in more than one place at once, so we can't create an appointment at the same time as another one assigned to the same nurse

That's not a constraint on the nurse, that's a constraint on the schedule.

"At 9am, Nurse(id:12345) is to visit Patient(id:67890)" is a schedule entry. It's perfectly straight forward to manage all of the schedule entries together. Views of the schedule may also need to include additional information about the Nurse or the Patient, so the view may join additional information.

The schedule becomes its own "aggregate", using correlation ids to enable joins with other information.

Would the schedule be a "NurseSchedule" or a system-wide "Schedule"?

This is probably something specific to the use case of scheduling nurses. Depending on the domain, a given schedule might span a number of nurses and patients.

World answered 15/10, 2018 at 15:46 Comment(5)
Would the schedule be a "NurseSchedule" or a system-wide "Schedule"?Piacular
@Piacular The main problem is that the AR boundary would have to be very large in order to know if the nurse is already booked on a specific period AND if the patient itself already has an appointment, assuming that a patient can have an appointment with any nurse. If patients are bound to specific nurses it makes it easier. If not something you could do is to use a DailyNurseSchedule to make sure the nurse is not overbooked and use a DB unique constraint WHERE status = 'pending' to avoid having more than 1 pending appointment per patient.Christensen
Another approach could be to modify 2 ARs in a single transaction, where the Patient AR would keep track of the next pending appointment. You'd modify the DailyNurseSchedule and the Patient AR everytime a booking is made. Finally, if you can afford it you could choose to make one of these rules eventually consistent instead of modifying more than one AR per transaction.Christensen
@Christensen That was my motivation for the question. The system-wide Schedule would become a bottleneck, potentially. My initial thoughts on a solution involved the following: Nurse's Schedule, Patient's Schedule. They don't have to be different classes, btw. They can be two instances of "Schedule". A Schedule has two behaviors: Create and Cancel Appointment. An AppointmentSaga would first create an appointment with the NurseSchedule. If successful, it would then book against the PatientSchedule. If not successful, it would cancel the NurseSchedule appointment.Piacular
@Piacular Yes, you could do that, although the only reason you'd have PatientSchedule is to know if there's a pending appointment or not. I like the solution though, but I think you are still missing an Appointment AR. An Appointment could have a list of Participant, where the appointment's period has to be reserved into each participant's schedule with a saga.Christensen
P
1

Is it acceptable to simply have a CommandHandler which loads up multiple AR objects and uses their state to determine whether an appointment can be created or not?

No, not if you want to follow the DDD approach. The Aggregate should not be smaller than a transaction, the Aggregate is the transactional boundary.

From what I see you have these business invariants:

  1. a nurse can't be in more than one place at once, so we can't create an appointment at the same time as another one assigned to the same nurse

  2. it's only possible to have a single pending appointment per patient

These two rules can be strongly enforced only if the Nurse and the Patient belong to the same Aggregate. That is, you should have a big Aggregate if you want the two rules to be respected no matter what.

But having such a big Aggregate may feel wrong. There is something that you can do: a trade off: which of the two rules can be enforced in an eventually consistent manner? After you discuss with the business specialists and present him/hers the business implications, you pick one and then you create a Saga/Process manager that detects such an invalid state and corrects it and/or notifies someone to manually correct it.

If so, and tying back in with Q1 above (assuming I don't store a list of appointment ID's under the nurse / patient AR's), the read model is the only way of easily finding the appointments belonging to the respective nurse/patient - so is it also acceptable to enforce business rules based on the state of the read model rather than an AR from the repository?

A Saga/Process manager uses old data (eventually consistent updated data) to send the right commands to the Aggregates, just like the Readmodels. So, you could have the Saga maintaining a private state (a safer/cleaner solution) or let is query a canonical Readmodel to find the invalid cases (a quicker/dirtier solution).

Paphlagonia answered 15/10, 2018 at 9:21 Comment(4)
So if my understanding is correct, I could have an "AppointmentScheduler" process manager which maintains it's own list of appointments for all nurses/patients? If so, would this be in itself an AR so it can be persisted and retrieved using the same generic repository which reads from the event store and replays events to build the current state of the object?Saintebeuve
No, not an AR, it is the opposite of an AR: it receives events and it sends commands. It can however be persisted, like a CRUD entity. Regarding its rehydration from events: it can be done, however, it is a little tricky; you need to be carefully applying the events on it because Sagas produce commands when the process events, so it should process the events only once with side-effects and after that without side-effects (on rehydration).Paphlagonia
Thinking more about the model, I can't see a need anymore for assigning a list of appointment ID's under the patient/nurse AR's, as they won't be used for anything. Instead, having a central AppointmentScheduler AR which has a list of all appointments for all patients/nurses means the validation on availability is encapsulated within this object. The only thing that doesn't sit right is that there will only ever be one instance of it, so it's ID would be something statically configurable rather than storing it in the read model. Hopefully this makes sense?Saintebeuve
@StuRatcliffe No quite. The simplest thing would be to have a Saga that, when it receives an ApointmentWasMade event, it loads the count of appointments for that patient (from a Readmodel) and if greater than zero then send a command to Nurse aggregate, something like CancelApointmentBecausePatientIsAlreadyScheduledPaphlagonia
O
0

If you want immediate consistency the only clean way is to have a single big aggregate in the domain model. To avoid unrelated optimistic concurrency exeptions you can try to split the aggregate in the implementation of the domain repository.

Olivia answered 28/2, 2019 at 20:19 Comment(0)
O
-1

The aggregate root defines a consistency boundary which does not have to be a single transaction on the database. The question whether there should be a single or several transactions is a performance/infrastructure concern and should be addressed in the implementation of the repository and not in the domain model. It can not even be expected that the aggregate root of the domain model and the persistence model are the same. If your domain dictates to have consistency in all visits and you have to prevent any conflicts then model a singlton aggregate (let's call it "Schedule") that knows about all nurses and patients and enforces all consistency rules.

One possible implementation of the Schedule repository would be to create Nurse and Patient entities for persistence only. Retrieving the Schedule aggregate would mean to load all the Nurse and Patient entities, some caching strategy might improve performance here. Storing the modified Schedule aggregate would involve optimistic concurrency on the modified Nurse and Patient entities only hence reducing version conflicts. As you already noticed Saga/process manager seems to be overkill. I would even go further and say that a saga would diffuse the domain logic and the intial intent. Of course if the domain does not enforce consistency over all visits at all times, then you deal with eventual consistency. Then it would make sense to work with Nurse and Patient aggregates in the domain layer and implement a process manager.

Olivia answered 27/2, 2019 at 21:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.