0

I have a library system that allows borrowing books for chosen time. I need to protect it from ordering the books if any of them is already fully booked in any point in this time.

So let's say in our stock we have these books and their counts:

Fellowship of the ring (x10)
The Shining (x1)

The user A decides to order both of these books in a quantity of 1 between 2022-01-01 and 2022-01-20.

When user B wants to order both of these books in quantity of 1 between 2022-01-15 and 2022-01-30 an error should be thrown, because The Shining is not available anymore.

Additionally - there is a configurable margin before and after each order to prepare (eg. 1 day would mean that user B can make the order starting as early as 2022-01-17).

Also at any point in time, library may buy new books and add them to stock, meaning that user B would be able to make an order.

Now the logic to check if books are available needs to find all orders for specified time, including the restocking margin and compare it to the overall available stock.

To simplify it I have split the time into 30min slots, so we can iterate through all slots in a given timeframe and calculate the stock for this slot.

Here is a sketch of my aggregate:

BookOrder

Members:
DateTime From
DateTime To
Book[] BooksOrdered

Methods:
CanOrder(from, to, books, stocksForTimeSpanWithMargin)
Order(from, to, books, stocksForTimeSpanWithMargin)

The problem is that all this logic is quite complex and the only way to enforce it via an aggregate root I can think of is retrieving ALL of the orders and feeding the aggregate with them via constructor. But it certainly wouldn't scale to 10000+ books, 10000+ orders per day for 2 years in future.

To reduce the data pushed into aggregate I could make the query for only specified items in only the specified time slots. It could still be a lot, but significantly less.

However this introduces another problem - part of the logic would be located in the DB query. How would aggregate root know if it received all orders from the timespan? It would have to work only with what it was given.

Rasmond
  • 442
  • 4
  • 15

1 Answers1

0

I would try to use divide and conquer technique here. I mean I would try to create simple classes which will have just one sinlgie responsibility. So let's look how classes would look like in my view.

This is a book class:

public class Book
{
    public int Id { get; set; }

    /// <summary>
    /// We can have many copies of the book with different Id 
    /// However, all copies will have the same code
    /// </summary>
    public int Code { get; set; }

    public string Title { get; set; }

    public string Author { get; set; }
}

And BookOrder class will look like this:

public class BookOrder
{
    public int Id { get; set; }

    public DateTime From { get; set; }

    public DateTime To { get; set; }

    public Book OrderedBook { get; set; }

    public bool IsOrdered { get; set; }

    public bool CanBeOrdered(DateTime newBookingFrom, DateTime newBookingTo, 
        Book bookToBeOrdered) { 
        if (IsOrdered)
            return false;

        if (IsOverlappingPeriod(newBookingFrom, newBookingTo))
            return false;

        // library may buy new books
        if (OrderedBook.Id != bookToBeOrdered.Id)
            return true;

        return true;
    }

    private bool IsOverlappingPeriod(DateTime newBookingFrom, 
        DateTime newBookingTo)
    { 
        return newBookingFrom < To && From < newBookingTo;
    }
}

And this is how User will order a book from library:

public class User
{
    private Library _library;

    public IEnumerable<BookOrder> DesirableBooks { get; private set; }

    public IEnumerable<BookOrder> AvailableBooksToBeOrderedByLibrary { get; 
        private set; }    

    public User(Library library)
    {
        _library = library;
    }

    public void Order() 
    {
        AvailableBooksToBeOrderedByLibrary = _library.Order(DesirableBooks);
    }
}

And this is library class. This class can :

public class Library
{
    public IEnumerable<BookOrder> Order(IEnumerable<BookOrder> desirableOrders){ 
        IList<BookOrder> availableOrders = new List<BookOrder>();
        // Here logic to check whether the book can be ordered from the library
        foreach (BookOrder bookOrder in desirableOrders)
        {
            IEnumerable<BookOrder> availableBookOrdersByCode = 
                GetAllBooksByCode(bookOrder);
            if (availableBookOrdersByCode.Contains(bookOrder))
                availableOrders.Add(bookOrder);
        }

        MakeOrder(availableOrders);

        return availableOrders;
    }

    private void MakeOrder(IEnumerable<BookOrder> orderedBooks) {
        foreach (var orderedBook in orderedBooks) {
            // you should mark book here that it is reserved in library
        }
    }

    public IEnumerable<BookOrder> GetAllBooksByCode(BookOrder bookOrder) 
    {
        // Here we should get all avalilable books from the library
        // filtered by `Code` field and availability to be booked (use 
        // method `CanBeOrdered()` for this purpose) 
        return new List<BookOrder>();
    }
}

There is a great post about how to check whether there is overlapping periods between book orders.

StepUp
  • 36,391
  • 15
  • 88
  • 148