2

I have found interesting problem in DDD, which can be solved extremely simply without DDD, but with DDD looks huge.

Let's say I have: Category - AR (AggregateRoot) Product - AR

Category can have multiple products and also a featured product, like this:

public class Category : AggregateRoot {
   public List<Product> Products { get; set; }
   public Product FeaturedProduct { get; set; }

Product have Name, Description and IsActive flag:

public class Product : AggregateRoot {
   public string Name { get; set; }
   public string Description { get; set; }
   public bool IsActive { get; set; }
}

Now the business rules are:

  1. Can not Set Inactive product as featured.
  2. Can not Deactivate product when set as featured.

Proposed solutions (none of them is proper from DDD perspective):

Option 1) Put this business rules to Command Handlers, but it my opinion this would be code-smell. I believe such business rules should be part of Domain (not application layer, and command handlers are application layer).

Option 2) Create CategoriesProductsDomainService and put logic that touches these 2 AR there, e.g.:

2A:
public void DeactivateProduct(int productId) {
   // load product from products_repository
   // call categories_repository to check if any category has this product as featured
   // deactivate product
}

or

2B:
public void DeactivateProduct(Product product, List<Category> categories_with_product_as_featured) {
   if (categories_with_product_as_featured.Any(x => x.FeaturedProduct.Id == product.Id)) {
      // return or throw validation error
   }
   product.Deactivate();
}

but such approach removes business logic from Aggregate and place it in additional DomainService. Not sure if this is proper approach.

Option 3) When Deactivating product, dispatch a ProductDeactivatedDomainEvent and handle it in same transaction, and event handler can throw an exception if the business rules is not met.

This approach however have 3 drawbacks:

  • I don't think DomainEvent should be used to validate transaction that just happened
  • throwing exception for something that is just validation error is a design-smell
  • it is not unit testable without mocks (DomainEventsDispatcher is part of infrastructure)

Final thoughts

Looks like Option 2) is the most DDD friendly so far. Nevertheless I see a danger here, that a lot of logic will be removed from Aggregates with new business rules.

8
  • A preffered approach would be to pass the domain service into the deactivation method on the product aggregate, and call the domain method from there. That way the aggregate defines that the domain method must be invoked, and thanks to domain services being registered in IoC containers and having ability to inject other services and repositories, the collection check may be performed in that. This way unit tests can be written for both the domain service as well as the product aggregate, while keeping both of them cohesive and strict about protecting their invariants.
    – Andy
    Feb 14, 2021 at 8:00
  • 1
    Aggregate Roots should reference other Aggregate Roots by Id.
    – Rik D
    Feb 14, 2021 at 10:12
  • 1
    I don't have any specific book on this but my experience, but simply Google "DDD pass domain service to method", which should give you some materials to read through. 😉 Also plus what @Rik D wrote about the references. Check Vaugh Vernon's Effective aggregate design.
    – Andy
    Feb 14, 2021 at 10:19
  • 1
    If your aggregate root publicly exposes internal structure, and doesn't have a set of well-defined methods that capture the relevant business rules and provide means through which other code can interact with the aggregate and its internals, then it's not an aggregate root - and that makes all these other considerations superfluous. (It's not an aggregate root (AR) in the sense that it doesn't match the AR concept, and doesn't serve the function that an AR is supposed to serve - and if it doesn't do that, then all these considerations/constraints related to aggregate design are meaningles). Feb 14, 2021 at 12:02
  • 1
    More often than not, purism is an approach that has an unjustifiable amount of effort to get right, relative to the concrete requirements and concerns of the business. This is why in the majority of cases you will have to make compromises. Which compromises you make is very situational, based on both the available budget and cost of things going wrong that could've been addressed earlier. Because of this, it's really hard to write a singular answer that can account for all of these possible scenarios. Are you looking for a purist solution or anything that works with a generally clean approach?
    – Flater
    Jul 28 at 11:31

4 Answers 4

3

The domain logic usually isn't the hard part.

The most straight forward answer is that your command handler (in the application layer) is responsible for retrieving copies of the information that you need, and passing that information to the domain model. It's the model that is responsible for deciding what to do with that information.

You achieve this by having some function that knows how to fetch the information you need (given an product Id, return the active status). You can then choose between having the command handler invoke that function (passing the result to the domain model) and having the command handler pass the function itself (aka a "domain service") to the model, which can then invoke the method to get the value.

Largely, the tradeoff is about where in your design you want to be handling plumbing errors. If "testable without mocks" is important, then you'll invoke the function in the application layer, and pass the result as a value to the domain model.


Where things get tricky: how do you ensure that this information doesn't change while your domain model is thinking about its local copy?

In effect, you need some way to enforce a logical lock on the data you are reading, to ensure that nobody changes it. This might mean that there is only a single process that is allowed to change either aggregate, or it might mean that any process that wants to change these aggregates must first acquire a lock on both.

In a distributed environment, coordinating the acquisition and release of multiple locks can be a real pain -- make sure that the benefit to the business justifies the work.


In the DDD community that embraces event sourcing, you are likely to instead see one of two alternative approaches

A) when it is not expensive to the business that the two pieces of information disagree, to simply ignore immediate enforcement of the invariant and instead look to "best effort" escalation of detected problems

B) redesign your aggregate boundaries such that each invariant is contained within a single aggregate. In this case, that might look like a new "product catalog" aggregate, with two data structures: one that maps productId to active status, and another that maps categoryid to featured productId. Unnecessary information (like the relationship between productId and product description) would live "somewhere else".


Related reading:

  • Race Conditions Don't Exist - Udi Dahan 2010
  • Memories, Guesses, and Apologies - Pat Helland 2007
  • Data on the Outside vs Data on the Inside - Pat Helland 2005 / 2020
2

Option 4) Design your domain model around the behaviors required of your system.

As is usually the case when the kind of tension you are seeing above arises, the problem is the model - not some missing piece of technical knowledge. What I mean is that you seem to have designed your domain model in a way to create the very problem you are trying to solve! So let's see if we can't come up with another model shall we?


class Category
{
    public List<ProductCategory> Products { get; set; }
    public Product FeaturedProduct { get; set; }

    public void DeactivateProduct(ProductId id)
    {
        // make sure this product is not featured
    }
    
    public void ChangeFeaturedProduct(ProductId id)
    {
        // make sure this product is not active
    }
}

// Represents a Product within a given Category
class ProductCategory
{
    public ProductId id { get; set; }
    public bool IsActive { get; set; }
}

class Product 
{
    public ProductId id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    // We have moved IsActive into the Category aggregate
}

And there we have it! What we were missing is the concept of "a Product within a Category". We then made this concept explicit by modeling it via our ProductCategory object (which is "owned" by Category).

The above is, quite simply, the only design that satisfies your invariant. Any other, ad-hoc, service-driven, event-laden design is going to boil down to the above anyway (but with way more complexity). It is a fact that your Category aggregate must be in charge of enforcing this invariant. For it is the only piece of knowledge that contains all of the information necessary to do so (the featured product and a list of available products).

(You can imagine how the above might have to change should you need to be able to coordinate deactivating a Product contained within many Category)

It is your duty as a practitioner of DDD to always be on the lookout for a more suitable design as requirements are added. A domain model is discovered!

3
  • In the above example Category is AR while Products are entities inside Category. The point here is that not all products must be inside category. There can be orphaned products, thus Category and Product are separate ARs, meaning that Category can not decide about another AR. I'm thankful for your answer but I consider above example as invalid. Feb 14, 2021 at 16:58
  • @MaciejPszczolinski You must be misunderstanding my answer. The solution above is specifically designed to avoid the problem of "Category cannot decide about another AR". We achieve this by moving the data that Category needs to control into Category. My answer boils down to: You have modeled your domain wrong. Here's how you fix it. If you need Product to exist and be active/inactive outside of a Category, then you need to either create a Catagory that represents "no category", or invert the model. Feb 14, 2021 at 19:06
  • @MaciejPszczolinski "Category and Product are separate ARs, meaning that Category can not decide about another AR" - there's no such rule in DDD, the design principle says something different: you don't access the internals of an aggregate directly, you instead manipulate the aggregate through its aggregate root. That's the whole point of having an aggregate root - a client-facing object responsible for maintaining the invariants of the aggregate. Feb 15, 2021 at 4:50
1

It seems Category is only interested in active products. Make that explicit:

public class Category : AggregateRoot
   public List<ProductId> ActiveProducts { get; } // When loading the AR from the Repository, make sure you only load Active products
   public ProductId FeaturedProduct { get; }

   public void ChangeFeaturedProduct(ProductId productId)
      // cannot change featured product if productId doesn't exist in ActiveProducts
      // set FeatureProduct to productId
      // Raise ProductFeaturedEvent(productId)

public class Product : AggregateRoot
   public string Name { get; }
   public string Description { get; }
   public bool Active { get; }
   public bool Featured { get; }

   public void Deactivate()
      // cannot deactivate if Featured is true

The ProductFeaturedEventHandler reacts to the ProductFeaturedEvent by loading the Product from the repository and setting the Featured boolean to true. This is a separate transaction. AggregateRoots are transaction boundaries. Everything that has to be consistent within the boundary should be in the same aggregate.

5
  • It will work of course, but don't you think that the Featured field in product is leaking a domain knowledge from Category? Featured functionality is only Category concern. From Product's perspective, it does not need to know if it is considered Featured of any category. Does it? Feb 14, 2021 at 17:02
  • Apparently it does, because there’s a business rule that says featured products cannot be deactivated. Making that explicit is not leaking knowledge imo.
    – Rik D
    Feb 14, 2021 at 19:52
  • I just realized that technically you don’t need an event, you don’t even have to store the featured property on product. When retrieving a product, the repository could query the featured property value. I think using events is a little more elegant, but could also introduce some complexity.
    – Rik D
    Feb 14, 2021 at 19:56
  • I can't help but think that this solution is prone to race conditions. The invariants stated: Can not Set Inactive product as featured. and Can not Deactivate product when set as featured.. In the solution above, the invariants are spread across two aggregates, so it is quite possible to deactivate a product while setting it as featured. As was noted in the answer, consistency is only guaranteed within an aggregate. Therefore, you need a single aggregate to maintain the active/featured invariants. Feb 16, 2021 at 6:50
  • @martingreber that’s a good observation and it’s why I wrote the part about Aggregates being transaction boundaries. The premise in the question was that there are two Aggregates, but depending on the importance of this possible race condition that might not be the best choice.
    – Rik D
    Feb 16, 2021 at 6:58
1

I do think that king-side-slide's answer is great but can be improved though (as suggested by comments). Encapsulating invariants in domain models is the key. They contain all the information necessary to do so. I iterated on the answer to ensure a couple more invariants like:

  • adding a product to a category it's not already in
  • removing a product from a category it's already in
  • deactivating product that is not featured in any category
  • featuring an active product in a category it's already in

Instead of creating a gigantic model that tries to satisfy all the invariants, we can create models that satisfy them separately, ensuring all data is provided. The key to understand is that we aren't able nor we need to create 1:1 mapping between real world and the domain models. We aren't modeling data, rather behaviour.

I suggest watching the talk "All our aggregates are wrong by Mauro Servienti", where the author exercises a concept of shopping cart to show that it's not a domain model, rather a behaviour with data split across different bounded contexts.

These domain objects can live separated by bounded contexts, e.g. Add context (featuring), and product context (creating/categorising).

Another difference from the referenced answer is models naming. Category is still a category with name and description, but models like CategorizedProduct are named to better suit the use case.


// AR that controlls invariants:
// Adding/removing product from a category
// Activating/deactivating a product
class CategorizedProduct : AggregateRoot
{
    public ProductId Product { get; }
    public List<CategorizedProduct.Product> Products { get; }
    public bool IsActive { get; }

    public void AddToCategory(CategoryId id)
    {
        // make sure product is active and isn't in the category already
    }
    
    public void RemoveFromCategory(CategoryId id)
    {
        // make sure product is in the category
    }

    public void DeactivateProduct(ProductId id)
    {
        // make sure this product is not featured in any of the categories
    }

    // Represents a Product within a given Category
    class Product
    {
        public CategoryId Category { get; }
        public bool IsFeatured { get; }
    } 
}

// AR that controlls invariants related to featuring a product within a single category
class FeaturedProduct : AggregateRoot
{
    public CategoryId Category { get; }
    public List<FeaturedProduct.Product> Products { get; }
    public ProductId? FeaturedProduct { get; }

    public void ChangeFeaturedProduct(ProductId id)
    {
        // make sure this product is active
    }
    
    // Represents a Product within a given Category
    class Product
    {
        public ProductId id { get; set; }
        public bool IsActive { get; set; }
    }
}

class Category : AggregateRoot
{
    public CategoryId id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

class Product : AggregateRoot
{
    public ProductId id { get; }
    public string Name { get; }
    public string Description { get; }
    public List<CategoryId> Categories { get; }
}

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.