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?

3
  • 3
    Based on how the requirement is formulated, it seems to me that products can be changed between being purchasable and non-purchasable. This strongly suggests that being purchasable is not a special kind of product (for which a sub-class is appropriate), but rather a property of a product. Oct 17, 2017 at 12:38
  • @BartvanIngenSchenau That was exactly one of my thoughts, implement a state pattern on Product. However it feels that, if I follow that approach, I would require to directly modify the Product class, violating the Open/Closed principle and potentially affecting all clients of the Product class. What are your opinions on that regard? Thanks a lot for sharing your thoughts. Oct 17, 2017 at 12:44
  • 1
    In my view, the Open/Closed principle does not apply to the Product class when you get a new requirement that specifies a change the products. Would you also create a FooProduct subclass if the next requirement is "all products must be extended with a Foo attribute"? Oct 17, 2017 at 17:46

3 Answers 3

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.

5
  • Your answer makes sense on reusing the Product Repository for both specialisations. Though that means that the repository should be aware and coupled to this specialization to save/retrieve products and generating instances of one type or the other, right? Wouldn't it be a kind of Open-Closed principle violation regarding to the modifications the repository will require? Thanks! Oct 17, 2017 at 12:51
  • 1
    Not really, you could make a new ProductAndPurchasableProductRepo, which encapsulated the ProductRepo and added on an extra db select to add the PurchasableProducts, extending the original class if you wanted and the same if you invent some new type of product later. It would be good OOP but bad in practice, because you want to optimise for your data storage layer and also because you would prob never use the old ProductRepo.
    – Ewan
    Oct 17, 2017 at 12:58
  • I guess, if you had separate dbs for each type of product you might do that, make two repos and then wrap them together. Im assuming that you are using a standard relational db and it will be altered to accomodate the new product type
    – Ewan
    Oct 17, 2017 at 13:01
  • in either case though the app would use a product repo which returned all types of product. It doesnt care about the implementation
    – Ewan
    Oct 17, 2017 at 13:03
  • Please stop reverting edits that multiple people in the community - including myself, after reviewing flags - believe improve the readability of the post. I've locked the post for 1 week. If you have concerns, please raise them on Meta instead of an edit war.
    – Thomas Owens
    Dec 16, 2021 at 18:38
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.

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);
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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