Correct aggregate roots in appointment scheduling application

93 Views Asked by At

I'm working on an application where one of the modules is responsible for scheduling appointments. I have some problems with creating correct aggregate roots.

There are DailySchedules where the owner of the business can define working slots. Customers can schedule appointments and there are two invariants:

  • appointment has to fit to working slot,
  • appointment cannot overlap with another scheduled appointment

Scheduled appointments can also be cancelled or postponed in the future.

My main problem is with ensuring mentioned invariants and fit to other ddd rules at the same time, especially modifying only single AR in a single transaction.

#1 Attempt - single aggregate root with child entities

class DailySchedule {
    private UUID id;
    private LocalDate date;
    private UUID businessId;
    private SortedSet<Interval> workingSlots = new TreeSet<>();
    private SortedSet<Appointment> scheduledAppointments = new TreeSet<>();

    void scheduleAppointment(...) { 
        ...
    }
}

This looked fine, DailySchedule is aggregate root and Appointment is Entity. We have the transactional consistency here, because we can read or persist DailySchedules as a whole. But since Appointment also holds the data that have nothing to do with scheduling and scheduled appointments can be cancelled later, it looks to me that Appointment should be AR as well.

#2 Attempt - two aggregate roots

My second attempt was to create factory method in DailySchedule that create Appointments, and instead of storing Appointments inside DailySchedule, now I have only working hours and scheduled hours. Both DailySchedule and Appointment are now AR.

class DailySchedule {
    private UUID id;
    private LocalDate date;
    private UUID businessId;
    private SortedSet<Interval> workingSlots = new TreeSet<>();
    private SortedSet<Interval> scheduledAppointments = new TreeSet<>(); //no references to appointments

    Appointment scheduleAppointment (...) { 
        ... 
    }

in application layer it would look like this:

class DailyScheduleService {
    //...
    void scheduleAppointment(CreateAppointmentCommand createCommand) {
        DailySchedule schedule = dailyScheduleRepository.findBy(...);
        Appointment scheduledAppointment = schedule.scheduleAppointment(...);
        appointmentRepository.save(scheduledAppointment);
    }
}

Unfortunately, there is no transactional consistency, because when two appointments are created at the same time, they will overlap.

My #3 idea is to have DailySchedule and Appointment as the two AR and break the rule that there should be only one aggregate modified in a single transaction. It will look exactly like in #1 scenario, but I will be able to use optimistic locking on DailySchedule and persist new Appointment in a single transaction.

Which of these solutions is best in your opinion? Maybe you can propose other solutions or my aggregated are wrong from the beginning? I will also appreciate links to relevant resources.

1

There are 1 best solutions below

0
Rob Conklin On

#3 is likely the best solution. Two aggregate roots, appointment & slots. Slots have lists of appointments. Part of saving an appointment (before commit) is to register itself in the slot. If slot registration fails because someone else snuck in there, roll back the (appointment) transaction and throw an exception back to the user that the scheduling failed in the given slot.

This is how you avoid complexities like multiple, distributed transactions across aggregates. However, this only works once. When you add a second aggregate, you have to embrace either 2-phase commits, distributed transactions, or eventual consistency.

In reality, the likelihood of a double-booking depends completely on the concurrency levels of the application. Eventual consistency would be appropriate for 99% of the applications of a scheduling application (operating at human speed). For edge-cases (like a popular concert booking system), you would want something much more transactional in nature.