Search code examples
.netdomain-driven-designavailability

How to determine combined room availability DDD


I revamped our availability engine few month ago in order to move our logic from a DB to a micro service. At that time, the business logic was fairly simple:

  • A resource (meeting room, desk, empty office, equipment) is available on a given time frame ONLY if it is not already booked (ie: no other booking using the same resource)

  • When a resource is not available, the closest available time frames must be calculated

To cover these requirements, I built the small piece of code below:

public class Schedule : IAggregateRoot
{
    public int CityId { get; }
    public int BuildingId { get; }
    public int CentreId { get; }
    public int ResourceId { get; }

    public ICollection<Booking> Bookings { get; }

    public Schedule(int cityId, int buildingId, int centreId, int resourceId, IEnumerable<Booking> bookings)
    {
        CityId = cityId;
        BuildingId = buildingId;
        CentreId = centreId;
        ResourceId = resourceId;
        Bookings = new List<Booking>(bookings);
    }

    public bool IsTimeSlotFree(DateTimeOffset startDate, DateTimeOffset endDate)
        => Bookings.Any(/* Predicate */);

    public IEnumerable<Availability> GetFreeTimeSlots(
        DateTimeOffset startDate,
        DateTimeOffset endDate, 
        TimeSpan duration)
    {
        var nbSlots = Math.Floor((endDate - startDate) / duration);
        for(int i=0; i<nbSlots; i++) {
            /* yield return availability */
        }
    }
}

public class Availability : ValueObject
{
    public DateTimeOffset StartDate { get; set; }
    public DateTimeOffset EndDate { get; set; }
    public int ResourceId { get; set; }
    public bool IsAvailable { get; set; } 
}

public class Resource : Entity
{
    public string Code { get; set; }

    // Required for EF Core
    protected Resource() { }
}

public class Booking : Entity
{
    public DateTimeOffset StartDate { get; set; }
    public DateTimeOffset EndDate { get; set; }
    public string Status { get; set; }
    public int ResourceId { get; set; }

    // Required for EF Core
    protected Booking() { }
}

Few weeks ago I was asked to handle combined rooms (two smaller rooms can be merged into a bigger combined room). In this scenario a combined room is available ONLY its sub-rooms and itself are available. In other words, I need to check several schedules to determine the availability and unfortunately my current level of abstraction doesn't allow that (one schedule, one room).

The only way I found is to retrieve a resource and its children (=subrooms) and then create a schedule containing a dictionary of ResourceId and bookings.

public class Resource : Entity
{
    public string Code { get; set; }

    public Resource Parent { get; set; }
    public ICollection<Resource> Children { get; set; }

    // Required for EF Core
    protected Resource() { }
}

public class Schedule : IAggregateRoot
{
    public int CityId { get; }
    public int BuildingId { get; }
    public int CentreId { get; }
    public int ResourceId { get; }

    public IDictionnary<int, ICollection<Bookings>> Bookings

    (...)
}

I don't find this solution really elegant. To me a better solution would be to retrieve the schedules and combine them in order to determine the actual availability. I tried several solutions but I ended-up writing spaghetti code.

Do you have any ideas on how I could re-design my aggregates to properly handle this new concept?

Thank you, Seb


Solution

  • At a guess, the core problem is that you are missing domain concepts in your model.

    My guess is that you are missing a representation of the product catalog that describes the available inventory. In that catalog you would have an entry for room #101 and an entry for room #102. If those rooms can be packaged together, then you would also have an entry for the package[#101 and #102].

    So the availability of a package is easy - take the intersection of the schedules of the elements in the package. Since you know the contents of the package, it's easy to find the schedules you need to reconcile.

    Note that you can update the catalog - adding or removing packages - without affecting the bookings in any way.

    You do, of course, have to figure out how you are going to deal with the actual booking of multiple rooms. There are a couple possibilities; the simplest, and the one I would guess is most familiar to your users, is to allow the schedule aggregates to accept overlapping bookings, setting a double booked flag to track the case where compensating actions need to be taken.

    Another alternative is to book the packages using the saga pattern; where the process doing the orchestration of the booking knows to cancel the booking of the rooms if the entire package cannot be booked.

    You could simplify things by moving all of the schedules into a single aggregate; raising out the consistency boundary to a larger scale (the property, perhaps, rather than individual rooms); that trades away the autonomy and scale of larger rooms.