8

I'm working on a legacy warehouse system. There is one Aggregate root, Product which has its correspondent ProductRepository.

Right now I have a new requirement that says the following:

Some Products are Purchasable and need to keep track of the datetime they have become purchasable.

So, in order to implement this requirement, I decided to take the following approach that, since I can see a "is-a" relationship, I decided to create a new class called PurchasableProduct which inherits Product and adds this new attribute.

class PurchasableProduct(Product):
    def __init__(product_properties, purchasable_datetime):
        super().__init__(product_properties)
        self.purchasable_datetime = purchasable_datetime

What's bugging me right now are the repositories. ProductRepository, of course, should still return instances of Products (even though they might be PurchasableProducts), but I need a way to retrieve and save those PurchasableProducts. Adding a PurchasableProductsRepository seems a solution but it's kind of weird that I can have two repositories, ProductRepository and PurchasableRepository, that I can use to save instances of PurchasableProducts.

In a DDD paradigm, what would be the best way to implement this situation where an aggregate root is an specialization of another one?

4 Answers4

8

it's kind of weird that I can have two repositories

Get used to it.

It's long been considered good practice to model your use cases explicitly. From the perspective of the application, it has a reference to a repository that plays the role of providing a reference to a particular flavor of aggregate root.

So you might have:

interface Product {
    // ...
}

interface ProductRepository { Product get(Id id); }

interface PurchasableProduct { // ... }

interface PurchaseableProductRepository { PurchasableProduct get(Id id); }

With use cases that need to access PurchaseableProducts wired to the PurchaseableProductsRepository.

The application doesn't need to know whether or not the underlying implementations are the same.

The underlying implementation can definitely get tricky; for example, you don't want to lose the purchasable date time if a product gets updated using the repository that doesn't know about that information.

The way I've come to think of it: the persisted representation of the aggregate is a message, from a past version of the domain model to the present (and future). So the basic principles of message compatibility still apply - we need must-ignore and must-forward semantics.

In short, if we use a ProductRepository to store a PurchaseableProduct, we need to be sure that the implementation doesn't overwrite the properties that it knows nothing about.

VoiceOfUnreason
  • 34,589
  • 2
  • 44
  • 83
4

Your gut feeling is right, both Product and PurchasableProduct should be retrieved from the ProductRepository.

This shouldn't be an issue, as one inherits from the other, say we add an extra table to the database with the additional PurchasableProduct fields, left join it into the Product select statement, and then where its not null instead of instantiating and populating a new Product, we instantiate a new PurchasableProduct. You can still return an array/List of Product. when calling the overridden Purchase() method for example the PurchasableProduct will check its date and the Product wont.

Essentially your class sub typing is just replacing a conditional statement in the Purchase method and some nullable fields on Product. Its neater code, but t shouldn't change the overall flow of the application.

If you have a specific need to get only the purchasable products, then you might add a GetProductsWhichArePurchasable() method(s) to the repository to allow for optimisation. Whether you still return a list of Products, or PurchasableProducts is interesting. I think a purist would stick with Product, but since your are optimising anyway.

Thomas Owens
  • 85,641
  • 18
  • 207
  • 307
Ewan
  • 83,178
2

Late to the party, but I also went through it so here I'll share my finding.

In this case, it's pretty clear that the Product is the root of your aggregate, and aggregate root are usually equipped with a repository (and a factory).

But also, a PurchasableProduct is a Product so that the PurchasableProduct is a fully fledged aggregate root.

If you're introducing this subclassing of Product is surely because some workflows apply to all products, but some other workflow exist only for PurchasableProducts, so you only load PurchasableProducts from your repository, for example:

public void purchaseProduct(PurchaseProductCommand cmd) {
    Product p = productRepository.findById(cmd.productId())
        .orElseThrow(()-> new AggregateNotFoundException());
if (product instanceof PurchasableProduct) {
    ((PurchasableProduct) product).purchase(cmd);
}
else {
    throw new AggregateNotFoundException();
}

productRepository.save(product)

}

It's clear that we can do better than that. Let's see the two approaches that we can apply.

Approaches

Approach #1: Repository method specialization

In this case we can save a lot of lines by using the method:

PurchasableProduct product =
    (PurchasableProduct)productRepository.findByIdAndType(cmd.productId(), PURCHASABLE);

but this is not the best way since you have to introduce this "type" thing and you won't get rid of casting. Let's delegate this discrimination to the repository

PurchasableProduct product = productRepository.findPurchasableById(cmd.productId());

This is the simplest solution, especially when you can count on ORM frameworks that can map class inheritance onto data storage capabilities for free. Nonetheless, there are two pitfalls:

  1. The clients of ProductRepository want to only work on products. They might not want to burden of understanding what a PurchasableProduct is (that's why you are subclassing) and why they have methods in your repository that speak about Purchasable products. It's the single-responsibility principle (SRP) at the end of the day.

  2. If you have found that a PurchasableProduct has dedicated workflows, you also can infer that PurchasableProducts have a different lifecycle. They take care of purchasing, while Products are more well suited for catalogue etc. You may find out that you have a new bounded context, and eventually you want to split purchasing use cases implementation in a new module, then in a new microservice. As task, you may have to split out PurchasableProductRepository, hoping that nobody had the bad idea of mixing things in other workflow.

Approach #2: Specializing Repository

You just have:

PurchasableProduct product = purchasableProductRepository.findById(cmd.productId());

There is one more file to add to your repository, maybe a bit of maintenance more (but again, here ORM save our time). But this approach also scales, when it comes to find more states/roles for a product, because you will just add a new repository along with its subclass, using the basic CRUD repository methods (that you will define with or without ORM).

So I definitely would go for the second approach:

public void purchaseProduct(PurchaseProductCommand cmd) {
    PurchasableProduct p = repository.findById(cmd.productId())
        .orElseThrow(()-> new AggregateNotFoundException());
p.purchase(cmd);

repository.save(p);

}

0

It's a bit late, but I wanted to suggest another approach. The requirement says that we need to specify a product as Purchasable which we need to track its changes. From this, I don't think PurchasableProduct has any additional fields except for the date of changing states. So why bother defining another entity by inheritance?

You can model this by introducing a ProductHistory that keeps track of desired changes and defining a ProductIsPurchasable field indicating whether it's purchasable or not. ProductHistory is a collection property in the Product entity. This way, you can add a history record anytime you change the product states. The query to retrieve the PurchasableProduct would be simple (predicate on ProductIsPurchasable).

R.Abbasi
  • 121