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:
- Rethink the domain because maybe the aggregate is not well designed.
- 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?