0

I want to apply DDD to a pet project and everything fits perfectly except for one part: aggregates with collections and the need of pagination.

I've been researching about how to deal with an aggregate that has a collection of entities that are too many to load in memory and, therefore, pagination is needed. I've found two main strategies to deal with this:

  1. Rethink the domain because maybe the aggregate is not well designed.
  2. Extend the main aggregate class to fulfill the specific query needs. For example, if I have the class Foo and I need a representation of Foo with Bar paginated collection, create the FooWithBar class extending Foo.

In theory, I understand what both mean, but when I try to apply these ideas to my problem I'm not sure how to handle it. So, I'll try to explain a minimized version of the problem and I hope you can give me some light.

Case study

Imagine we have product storages where we can store products (duhh). These products have an expiration date and can be consumed. Once consumed, the expiration date does not matter. From the product storage we want to know the list of the expired products, the list of the consumed products and those that are not expired. A product storage can have thousands of products, but these are mainly consumed or expired products, few will remain unexpired.

If I do not attend to performance problems and I only take into account the needs of the domain I first create a Product class as:

class Product:
    id: str
    expiration_date: date
    consumed_date: date

    def is_expired():
        return self.expiration_date < today

    def is_consumed():
        return self.consumed_date is not None

    def consume():
        self.consumed_date = today

And ProductStorage class as something like:

class ProductStorage:
   products: Product

   def consume_products(list_ids):
        for id in list_ids:
             product = self.products.get(id)
             product.consume()

   def get_expired_products():
        return [product for product in self.products if product.is_expired()]

    def get_consumed_products():
        return [product for product in self.products if product.is_consumed()]
    
    def get_consumable_products():
        return [product for product in self.products if not product.is_expired() and not product.is_consumed]

Note 1: Do not focus on the methods or if they can be abstracted in a better way. I've put it this way in order to better illustrate the problem. Note 2: The example is implemented in a sort of pythonic pseudocode because I think is easy to understand, do not focus on revealing execution or code problems.

If instead of having the data in memory, we use a repository with a DataMapper and I load the product storage where I have thousands of products, it will be a performance problem so naturally I'd want to paginate the products loaded, but this is not a domain concern.

An example of "code" to solve this problem would be much appreciated.

EDIT

Following the advice of the comments I've deleted some parts of my original post and try to be more specific about the question.

As @RickD suggested, it's perfectly fine "to create a separate (read only) data access layer that fetches the data to create the views", and that "the aggregate root pattern is perfect for executing commands; changing the state of the system".

Nevertheless, I don't really grasp how it is translated to practice.What I understand is that I can maintain the Aggregate root pattern ProductStore only for updating the products like:

class ProductStorage:
   products: Product

   def consume_products(list_ids):
        for id in list_ids:
             product = self.products.get(id)
             product.consume()

And another class to simply access the data through a repository?:

class ProductStorageView:
   products: ProductRepository
   

   def get_expired_products(offset=0, limit=20):
        return products.get_expired_products(offset, limit)

    def get_consumed_products(offset=0, limit=20):
        return products.get_consumed_products(offset, limit)
    
    def get_consumable_products(offset=0, limit=20):
        return products.get_consumable_products(offset, limit)

Is this a valid way to maintain DDD principles?

2
  • 1
    First you should improve the question. Asking for resources and/or code examples is off topic. You should also focus on a single question. Wether the Aggregate root is ‘correct’ depends entirely on its ability to fulfill the behavior you want. Imo the aggregate root pattern is perfect for executing commands; changing the state of the system. For queries I tend to create a separate (read only) data access layer that fetches the data to create the views. Pagination is simple enough in such a query layer.
    – Rik D
    Nov 2 '21 at 21:50
  • Thank you @RikD , i'll edit the post to be more specific.
    – ainsausti
    Nov 3 '21 at 18:32
2

Few takes:

  1. Pagination is not a domain related concern. It's a technical concern and it should be treated as it is, namely non taking it into account during the design phase

  2. besides the point 1, you still have an aggregate that contains an unbounded set of child entities (ProductStorage -> Product) and this sound like a design smell. Why do you need this? What are the domain invariants that ProductStorage should enforce for which it needs to scan all the products in the store?

  3. Looking more in depth, ProductStorage doesn't really have any behaviour associated, it's an abstraction for a collection of products, namely a repository

So the solution could be to query the Product repository as Product is your aggregate. As techincal concern, worths to notice that you are designing this flow as bulk command

def consume_products(list_ids):
    products = product_repository.find_by_ids(list_ids)
    for product in products:
         product.consume()
    product_repository.save(products)
2
  • Thanks for the response. As far as I've understood I shouldn't have an aggregate that has no invariants that may limit the number of children it has. If I need an aggregate should be one with invariants and behavior and if not it should be more a use case of the application and trying to join all these use cases in a class will be artificial and a design error. Have I understood correctly?
    – ainsausti
    Nov 4 '21 at 15:07
  • 1
    IMO perfect :) the entire point of aggregate design is to "keep behaviour close to data" and this forms transactional boundaries that can either make succeed or fail the update of a piece of application state as whole, according with the rules set by invariants. As per my experience, everytime you try to introduce a "technical concept" in your domain that doesn't seem to belong to the "ubiquitous language" (try to speak out loud the workflow you are implementing) some piece in the analysis might be missing Nov 4 '21 at 15:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.