0

In my doctor's appointment booking system, I identified the following entities:

  • Doctor
  • Patient
  • Appointment

I also identified an aggregate, which is Doctor (aggregate root) and Appointment. It's an aggregate, as it has to hold an invariant of making sure that appointments assigned to them do not overlap. In pseudocode the model looks as follows:

  • class Patient(id, name, lastname)
  • class Doctor(id, name, lastname, upcomingAppointments: List)
  • class Appointment(id, patient_id, start_date_time, duration)

Now, the requirement to my system is:

  • schedule an appointment
  • retrieve all appointments for the given patient

Question 1: scheduling an appointment

I see 2 ways to model scheduling an appointment:

a) Have a Doctor#schedule method, that would return a copy (I strive for immutability) of the Doctor with a new valid (non-operlapping) appointment. Then I'd have a DoctorRepository.update method, that would store this aggregate. Pseudocode:

transaction boundary start;
doctor = DoctorRepository.get(doctorId);
doctorWithNewAppointment = doctor.schedule;
doctorRepository.update(doctorWithNewAppointment);
transaction boundary end;

but in this way I'd have to update the whole aggregate and also the other appointments, that were not updated. This would be bad performance-wise.

b) Have a Doctor#schedule method, that would return just a new valid (non-operlapping) appointment (I strive for immutability in my system). Then I'd have AppointmentRepository#insert to insert this new appointment. Psuedocode:

transaction boundary start;
doctor = DoctorRepository.get(doctorId);
newAppointment = doctor.schedule;
appointmentRepository.insert(newAppointment);
transaction boundary end;

Which one should I choose?

Question 2: retrieving all appointments for the given patient

I have a problem, because I've read, that entities, that are referenced from an aggregate cannot be referenced from the outside of the aggregate by other entities. That means (if I understand it correctly), that I cannot retrieve appointments outside of the Doctor aggregate. My requirement says I need to retrieve all appointments for a given patient. Now I have 2 options:

a) Have a findAllAppointments(PatientId patientId) method inside DoctorRepository, but is it OK to retrieve entities, that belong to different instances of the same aggregate?

b) Have a separate AppointmentRepository with findAll(PatientId patientId) method, but given I have an aggregate, is it fine to have a separate repository for an entity, that is a part of an aggregate?

Which one should I choose?

3 Answers 3

1

It could be that there is a problem in identifying what is aggregate root in your domain. In your case, will doctor or patient alone will be able to tell you any business story, is it that in this context maybe both of them are only relevant if there is an appointment ? If that is the case then I consider Appointment as aggregate root and all the preconditions that makes sure that appointment can be created are business invariants.

You are right about

That entities, that are referenced from an aggregate cannot be referenced from the outside of the aggregate by other entities.

but this is in the context of domain layer, so that aggregate root can make sure all invariants are met for every transaction.

If you need some data to be available for better performance or make view layer beautiful, you can still talk to database and present it back to the customer without involving aggregate and all of this could be done either by creating another read model or just talking to persistence without even going to the domain layer.

In the context of CV/Resume, Aggregate root would be CV and value objects or entities could be languages like "java, .NET, python". Though in domain layer, languages in itself are not referable and not useful without a CV, it could still be a requirement to get the statistics of popular languages for which you might directly talk to persistence layer and create a read model to get statistics.

2
  • Thanks for the reply! Let's assume Appointment is an aggregate root and has a create factory method, that checks for invariants if the appointment is bookable.. Then it'd have to know about all the upcoming appointments for the given doctor to avoid overlapping. The only way I see to model it is to accept the list of all appointments for the given doctor in a factory method of the Appointment aggregate. What do you think about such design?
    – lamb_bd85
    Commented Jun 8, 2021 at 6:32
  • 1
    Yes, that sounds good to me. If I understand it correct, you are going to get all appointments of a doctor and then your invariant is to make sure, new appointment is not created for an overlapped/already-booked time slot of the doctor. Commented Jun 8, 2021 at 10:34
1

I have a couple of points to highlight, that might give you a different perspective, even if none of them are a simple yes/no answers.

  1. In your class "model" you highlight the data elements. This is not really relevant for an object design. Data is secondary, public behavior is what tells me what an object is. In case you're highlighting that you have data structures with some behavior mixed in, that is also not a good object model. And by "not good" I mean less maintainable.

  2. "Retrieve all appointments" is not a use-case in itself. Displaying all appointments to a user is. This is a crucial difference that might answer one of your questions. Since you don't "retrieve" all appointments, there's potentially no need to get them in object form at all. You might be better off with constructing a UI object directly.

  3. Question 1: The schedule method does not implement your use-case. It does some in-memory things, and then you have to persist it to the database in addition. What's worse, because you have no insight into the data structures that got modified, now you potentially don't know how to optimally persist it. Whether DDD specifically says that you have to do it this way, is up to your interpretation of it. In any case it's definitely wierd that the actual use-case that you're building your application for requires that many lines of code, and specific knowledge of what to persist and how, to work.

  4. Question 2: As said, "retrieving" is not a use-case. "Displaying" it is, or "sending" it over the network is, etc. If your use-case is displaying the appointments, you're just making your work harder by introducing a completely pointless abstraction, that will likely make your code less maintainable in the long run.

If I had your requirements, I would have these things:

class Patient {
   UIComponent displayAppointments()
}

class Doctor {
   void schedule(...)
}

Only after I got these would I start think about what data actually has to live in these things (probably nothing other than their database id), and how to instantiate them, etc. I would have them directly talk to the database and do whatever they need to do in the most optimal way for the given use-case.

Is that DDD? I've seen interpretations where entities, value objects and aggregates are almost pure data structures, and I've seen talks where these are not even mentioned, instead the focus was on the domain and the requirements and using them in the code as closely as possible. I think the above fits the latter interpretation.

Again, I don't know your exact use-cases, you might have other requirements. My point is simply you're doing more work than necessary, and it's likely it even makes the code less maintainable, not more maintainable.

1

but in this way I'd have to update the whole aggregate and also the other appointments, that were not updated. This would be bad performance-wise.

Yes, when doing strict DDD, the repository always saves (and loads) entire aggregates. And yes, this would result in rather bad performance with your model.

Have a Doctor#schedule method, that would return just a new valid (non-operlapping) appointment (I strive for immutability in my system). Then I'd have AppointmentRepository#insert to insert this new appointment.

That's not transactionally correct, because the following may happen:

Thread 1                    Thread 2

read doctor jones
and find free time         
                            read doctor jones
                            and find free time
create 10 AM appointment    
                            create 10 AM appointment

Because the transactions are different objects (the new appointments), the database does not see a conflict between these writes, and permits them.

That's why DDD says to always save the entire aggregate, because that would have meant:

read doctor jones
and find free time
                       read doctor jones
                       and find free time
write doctor jones
                       write doctor jones

Here, both transactions write the same object (the doctor), and thus the database will detect a conflict.

So, how does one schedule non-overlapping appointments with DDD without creating huge aggregates?

If we're doing DDD, we should not treat this as a purely technical concern utterly divorced from the reality of the business, but instead uncover how the domain experts are currently tackling that concurrency issue.

I'd therefore ask the domain experts how, exactly, appoints are scheduled. Suppose they say this:

We tell the patient which time slots are available, and upon him choosing one, we write the appointment into that space on the doctors calendar.

If we listen real closely, there are some nouns in this description which could be entities. For instance, the "time slot" seems to exist even before an appointment is booked.

If we translate this into software, we'd therefore create non-overlapping TimeSlot entities up front, and when a patient wishes to schedule an appointment, we can query the database for unassigned TimeSlots, and assign the selected TimeSlot to the Appointment.

In making TimeSlot entities in their own right, we can find unassigned TimeSlot a lot more easily, because we can query for TimeSlots that have not been assigned, rather than having to infer availability for the absence of an overlapping Appointment.

More germane to your question, it also turns the business rule that appointments must not overlap into something that can be ensured once (at creation time) rather than an invariant that must be continually maintained. This enables us to treat each TimeSlot as an aggregate of its own, rather than having a huge aggregate that encompasses them all, resulting in transactional correctness and good performance.

Not the answer you're looking for? Browse other questions tagged or ask your own question.