Domain-Driven Design – How to Handle Growing Entity Collection

domain-driven-design

Say we have an imaginary system managing purchases made by customers, and there are the following business rules:

  1. You can only buy a product if you have enough money on your pre-paid account
  2. You can deposit any value to your pre-paid account
  3. If you buy a particular product for the first time, you get 50% discount

First thing that comes to my mind is to model it like so (C#-like pseudo-code):

class Purchase : Entity
{
  private Guid PurchaseId;
  private Guid GlobalProductId;
  private string ProductName;
  private decimal Price;
}

class Customer : AggregateRoot
{
  private Guid CustomerId;
  private List<Purchase> Purchases;
  private decimal Balance;

  public void Deposit(value){
    Balance += value;
  }

  public Result Purchase(globalProductId, productName, price){
    if(NeverBoughtProduct(globalProductId)){
      price = price * 0.5;
    }

    if(price > Balance){
      return Result.Fail('Insufficient funds');
    }

    Balance -= price;
    Purchases.Add(new Purchase(globalProductId, productName, price))
    return Result.Ok();
  }

  private bool NeverBoughtProduct(globalProductId){
    return Purchases.Any(purchase => purchase.GlobalProductId == globalProductId)
  }
}

The problem is that the number of purchases can grow rapidly. In order to make a purchase, you have to fetch all previous purchases for the customer form database.

In no-DDD scenario the solution would be trivial – a simple query fetching only a single boolean value whether the product has already been purchased.

I cannot make the Purchase an aggregate root, because then I wouldn't be able to encapsulate the business rules in domain objects.

Lazy loading isn't going to work either, since NeverBoughtProduct will fetch potentially every Purchase anyway.

This is a very common scenario, but I couldn't find a good solution on the web. How do you tackle such problems? Do you use some ORM tricks? Or would you change the model (if so, how would you do it)?

Best Answer

This is precisely the kind of situation that will often trip up inexperienced modelers!

Let us preface the solution with a quick refresher on the general "flow" an application will follow when handling commands:

1. Retrieve all state (domain model) necessary to coordinate use-case

2. Coordinate domain model to fulfill use-case ("tell don't ask")

3. Persist domain model

In your case, either we are pulling way too much state in step 1, or we are retrieving more state in step 2. Neither of these are ideal. So where is the solution?

A little more "knowledge crunching" can reveal that we are missing a concept that can be used to encapsulate the critical piece of state (NeverBoughtProduct). What we need here is a PurchaseRequest (VO). So now instead of:

// 1
customer = customers.Find( cmd.CustomerId );

// 2
customer.Purchase( cmd.ProductId, cmd.Price);

// 3
customers.Save( customer );

we have:

// 1
customer = customers.Find( cmd.CustomerId );

purchaseRequest = purchaseRequests.Find( cmd.CustomerId, cmd.ProductId );

// 2
customer.Purchase( purchaseRequest, cmd.Price ); 

// 3
customers.Save( customer );

In the end, you actually knew the solution to your problem! (take a look at your non-DDD passage - that's your PurchaseRequest).

Related Topic