DDD, Aggregate Root and entities in library application scenario

204 views Asked by At

I'm building a library application. Let's assume that we have a requirement to let registered people in the library to borrow a book for some default period of time (4 weeks).

I started to model my domain with an AggregateRoot called Loan with code below:

public class Loan : AggregateRoot<long>
{
    public static int DefaultLoanPeriodInDays = 30;

    private readonly long _bookId;
    private readonly long _userId;
    private readonly DateTime _endDate;
    private bool _active;
    private Book _book;
    private RegisteredLibraryUser _user;

    public Book Book => _book;
    public RegisteredLibraryUser User => _user;
    public DateTime EndDate => _endDate;
    public bool Active => _active;

    private Loan(long bookId, long userId, DateTime endDate)
    {
        _bookId = bookId;
        _userId = userId;
        _endDate = endDate;
        _active = true;
    }

    public static Loan Create(long bookId, long userId)
    {
        var endDate = DateTime.UtcNow.AddDays(DefaultLoanPeriodInDays);
        var loan = new Loan(bookId, userId, endDate);

        loan.Book.Borrow();

        loan.AddDomainEvent(new LoanCreatedEvent(bookId, userId, endDate));

        return loan;
    }

    public void EndLoan()
    {
        if (!Active)
            throw new LoanNotActiveException(Id);

        _active = false;
        _book.Return();

        AddDomainEvent(new LoanFinishedEvent(Id));
    }
}

And my Book entity looks like this:

public class Book : Entity<long>
{
    private BookInformation _bookInformation;
    private bool _inStock;

    public BookInformation BookInformation => _bookInformation;
    public bool InStock => _inStock;

    private Book(BookInformation bookInformation)
    {
        _bookInformation = bookInformation;
        _inStock = true;
    }

    public static Book Create(string title, string author, string subject, string isbn)
    {
        var bookInformation = new BookInformation(title, author, subject, isbn);
        var book = new Book(bookInformation);

        book.AddDomainEvent(new BookCreatedEvent(bookInformation));

        return book;
    }

    public void Borrow()
    {
        if (!InStock)
            throw new BookAlreadyBorrowedException();

        _inStock = false;

        AddDomainEvent(new BookBorrowedEvent(Id));
    }

    public void Return()
    {
        if (InStock)
            throw new BookNotBorrowedException(Id);

        _inStock = true;

        AddDomainEvent(new BookReturnedBackEvent(Id, DateTime.UtcNow));
    }
}

As you can see I'm using a static factory method for creating my Loan aggregate root where I'm passing an identity of the borrowing book and the user identity who is going to borrow it. Should I pass here the references to these objects (book and user) instead of ids? Which approach is better? As you can see my Book entity has also a property which indicates the availability of a book (InStock property). Should I update this property in the next use-case, for example in the handler of LoadCreatedEvent? Or should it be updated here within my AggregateRoot? If it should be updated here inside my aggregate I should pass the entire book reference instead of just an ID to be able to call it's method _book.Borrow().

I'm stuck at this point because I would like to do it pretty correct with the DDD approach. Or am I starting to do it from the wrong side and I'm missing something or thinking in a wrong way of it?

2

There are 2 answers

4
Ankit Vijay On

DomainEvents are in-memory events that are handled within the same domain.

You commit or rollback the entire "Transaction" together. Consider Domain Event as a DTO, which needs to hold all the information related to what just happened in the domain. So, as long as you have that information I do not think it matters if you pass Id, or the entire object.

I would go for passing the id in the domain event though as that information is sufficient to pass on the information to the DomainEventHandler.

Also, refer to this example of a similar scenario in Microsoft Docs, where they only pass UserId and CardTypeId along with all the other relevant information in the Domain event.

public class OrderStartedDomainEvent : INotification {
public string UserId { get; }
public int CardTypeId { get; }
public string CardNumber { get; }
public string CardSecurityNumber { get; }
public string CardHolderName { get; }
public DateTime CardExpiration { get; }
public Order Order { get; }

public OrderStartedDomainEvent(Order order,
                               int cardTypeId, string cardNumber,
                               string cardSecurityNumber, string cardHolderName,
                               DateTime cardExpiration)
{
    Order = order;
    CardTypeId = cardTypeId;
    CardNumber = cardNumber;
    CardSecurityNumber = cardSecurityNumber;
    CardHolderName = cardHolderName;
    CardExpiration = cardExpiration;
} }
0
Francesc Castells On

There are a couple of things that look suspicious in your sample code:

The first one, Loan does the following:

loan.Book.Borrow();

but it doesn't have a reference to Book and, at first sight, it doesn't seem it should either.

The second one, your Book entity seems to have many responsibilities: hold book information like Author, title, subject, hold stock information, and manage the Borrowing state. This is far too many responsibilities for an aggregate, let alone for an entity within an aggregate. Which also begs the question, does a Book really belong to a Loan? it seems strange.

I would recommend, rethinking your aggregates and try to give them a single purpose. What follows is purely as an example on the type of thinking that you could do, not a proposed design:

First, it makes sense to have a Book somewhere, which holds the book information. You can manage book information and Author information completely independent from the rest of the system. In fact, this part would look pretty much the same for a book store, a library, a publishing company, and an e-commerce site.

As you are modeling a Library, it probably makes sense to have something like LibraryItem (domain experts will know the right word for this). This item might have a type (book, DVD, magazine, etc) and the id of the actual item, but it doesn't care about the Title, Description, etc. with the Id is enough. Potentially, it also stores the location/sorting of the item with the library. Also, this Item might have some sort of Status, let's say Active/Retired. If it's Active, the item exists in the Library. If it's Retired, it doesn't exist anymore. If you have multiple items of the same book, you'll simply create more Items with the same BookId and if it's possible to identify the concrete physical book, with a bar code, for example, each Item will have that unique code, so you can find it by scanning the bar code.

Now a Loan, to me, it's basically an ItemId plus a CustomerId (not sure if they are called customers in this domain). Every time a Customer wants to borrow an Item, the user will find the Item (maybe scanning the bar code), and find the Customer. At this point you have to create the Loan with the CustomerId, ItemId, date and not a lot more I would say. This could be an aggregate on itself or simply be managed by the Item. Depending on what you chose the implementation will vary obviously. Note that you don't reuse Loans. You'll have somewhere a list of Loans that you can query and this list won't be inside the Item aggregate. This aggregate only needs to make sure that 2 loans of the same item are not allowed at the same time.

Well, that was a rather long preliminary explanation to answer your question: you need to manage the InStock in the same aggregate that allows you to Borrow a book. It has to be transactional, because you want to ensure that a book is not borrowed multiple times at once. But instead of passing one aggregate to the other, design your aggregates so that they have the right data and responsibilities together.