2

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

  • Are there limited subroom possibilities known ahead of time or can any two rooms be combined into a bigger one? – guillaume31 Oct 09 '17 at 15:04
  • Yes they are known ahead of time. I know which rooms can be combined. Actually I store this information in a db :) –  Oct 10 '17 at 00:49

2 Answers2

1

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.

VoiceOfUnreason
  • 52,766
  • 5
  • 49
  • 91
  • Thank you for this helpful answer. I will modify my model to add this concept of package. If I understand correctly you get the schedule of a package instead of getting it for a single resource. Am I correct? Also the availability engine lives in a "micro service" and does not know how to cancel/make booking. It is only used to actually get the availability of resources. –  Oct 10 '17 at 00:52
0

Almost any domain model that includes scheduling for resources has a concept of a Day as a separate aggregate. Without it, you will keep aggregating forever. Different contexts can keep different representation of the Day:

  • Booking can have room types and availability
  • Pricing can have room type and price
  • Reception can have room assignments, arrivals and departures
Alexey Zimarev
  • 17,944
  • 2
  • 55
  • 83