3

I am practicing Domain-Driven Design so why not build a demo product catalog project? Apparently Product is the Core Domain here, but since I like to make the project more interesting, I would love to support nested Category hierarchy. In other words, a Category could have many child Category.

Moreover, I would want to separate Category from Product domain and make it its own Supporting Domain.

Question: Marking Category as AggregateRoot doesn't sound right to me. A Category could have many child Category, which are also AggregateRoots?!! How can I go about modeling this? Nested product category is pretty common in E-Commerce real life situation.


namespace DL.Demo.Domain.Shared

public abstract class ValueObjectBase<T> : IEquatable<T> 
    where T : ValueObjectBase<T>
{
    public abstract bool Equals(T other);
    public abstract override bool Equals(object obj);
    public abstract override int GetHashCode();
}

public abstract class EntityBase : IEquatable<EntityBase>
{
    public Guid Id { get; private set; }

    protected EntityBase()
    {
        this.Id = Guid.NewGuid();
    }

    // Some Object overrides
}

And I actually have AggregateRoot inherents from Entity because I guess only an Entity could be an AggregateRoot?

public abstract class AggregateRoot : EntityBase
{
}

namespace DL.Demo.Domain.Catalog

public class Category : AggregateRoot
{
    public string Name { get; private set; }
    public Guid? ParentCategoryId { get; private set; }
    public CategoryStatus CategoryStatus { get; private set; }
}

Having a nested list of AggregateRoot just doesn't sound right to me. If you don't mark the Category as the AggregateRoot, how would you go about modeling this?

I am new to DDD and all other related cool stuff like Domain Events, Event Sourcing, etc. I will be appreciated if somebody who had experience can tell me if I am going to the right way.

David Liang
  • 20,385
  • 6
  • 44
  • 70
  • 1
    Well, what behaviors does a category have? What invariants do you need to enforce in these behaviors? There are so many ways to model this problem, it all depends on behaviors and true invariants. ProductCategory doesn't sound like a very interesting aggregate. I suspect most behaviors will be CRUD so perhaps the domain model pattern is not the right choice for that. – plalx Mar 02 '17 at 03:53
  • +1, I wouldn't use DDD tactical patterns (aggregate, etc.) for `Categories`, unless they interfere in an intricate way in `Product` domain rules. – guillaume31 Mar 02 '17 at 08:39
  • I am making something up since this is just a demo project: when administers of the site decide to deactivate a Category, it needs to deactivate all products directly linking to that Category, as well as all sub Categories under that Category, as well as all products linking to those categories? It might as well need to send email to all other admins. Also we might need to support moving a Category under other Categories. Is this a good example of complicated behaviors? – David Liang Mar 02 '17 at 17:44

3 Answers3

2

I am new to DDD and all other related cool stuff like Domain Events, Event Sourcing, etc. I will be appreciated if somebody who had experience can tell me if I am going to the right way.

You are on the right way.

Category should be an Aggregate root, with a reference to parent category by it's ID and this is very good.

Nested categories are a good candidate for event-sourcing, even if there are no special invariants to protect because of the different modes that this hierarchy could be projected in the Read models. You are not limited in any way on that representation, although the Aggregate is straight-forward. In every used Read model you could implement them differently as:

  1. Model Tree Structures with Parent References
  2. Model Tree Structures with Child References
  3. Model Tree Structures with an Array of Ancestors
  4. Model Tree Structures with Materialized Paths
  5. Model Tree Structures with Nested Sets

See more here about implementing tree structures (this link points to MongoDB but that is not relevant).

The Category Aggregate just emits simple events as ACategoryWasCreated, ACategoryHasMovedToOtherParent and so on and the Read models just adapt to reflect those events.

I've implemented a tree structure like this and the queries on the read-side (the query side) are very very fast. You could select the products in a category and all child categories with no joins. Or you could build a category path, again, with no joins.

Constantin Galbenu
  • 16,951
  • 3
  • 38
  • 54
  • I don't worry about Category being the Aggregate root. The only thing I feel straight is a Category can have child Aggregate roots?! I didn't mention Domain Events on my post but supposedly an Aggregate Root will contain a list of Domain Events as well. So the parent category will have list of events emitted by itself, but its Children also have their own list of events?! That doesn't sound right. – David Liang Mar 02 '17 at 18:00
  • 1
    Should I, instead of having the Category as Aggregate root, have something like CategoryTree as Aggregate root, and it just has a list of Categories to start the recursive category hierarchy? That way, the CategoryTree could contain all the Domain Events for the whole Aggregate. – David Liang Mar 02 '17 at 18:03
  • 1
    @DavidLiang you hold only the parentId on the `Aggregate`, not the entire parent or tree. You don't need to. The tree is build on a `Read model`. Every instance emits events only for itself. – Constantin Galbenu Mar 02 '17 at 18:20
1

The key to defining an aggregate is to define first a transactional boundary. Outside of an aggregate boundary consistency is eventual - achieved by reacting to the domain events emitted by an aggregate.

Aggregate can hold another aggregate ID (Value Object) as a reference, however, is not responsible to be transactionally consistent with another aggregate.

So, the main question - Is your tree transactionally consistent? If yes, linked list won't scale well. You have to model it differently.

Modeling is context specific and is not cookie-cutter exercise. Maybe your category is just a value object that could be modeled as a path. Hard to say without broader context.

Sergiy Chernets
  • 427
  • 2
  • 5
0

If you want to have a category tree, then the tree itself should probably be your aggregate root (I see you were coming to this conclusion on your own in the comments already). And this would have functions to add or remove children and so on.

And yes, with very large trees you could probably gain a lot of performance in having a read-only projection of your tree in for instance json format (stored in MongoDb, cache, file or whatever). Especially considering how such a category tree is typically updated only a tiny fraction of how often it is read, you could also easily get away with always just maintaining that json and foregoing a normalised database table tree altogether.

Arwin
  • 973
  • 1
  • 10
  • 21
  • Not correct. If a category tree is an aggregate, and only aggregate's root (not internal entities) can be referenced, then product can't reference (belong to) a specific category. Therefore, category itself must be an aggregate root (at least in some bounded context). The category tree structure might be a different aggregate root. The post is a bit misleading, as it's focusing only on one view/context of domain model. Please, correct this. – kravemir Oct 13 '19 at 13:35
  • That is an assumption though. There is no real reason why you couldn't reference using TreeGuid and CategoryGuid. Typically when you relate two aggregate roots, you need a relationship entity anyway. So that could be CategoryTreeEntityMembership. But as long as the only property of that relationship outside the EntityGuid, it is not even strictly necessary and can wait until that happens. Of course it is different if you were to do something where if you attach a Product to one Category, and that link should be maintained across Trees. Then Categories and Trees become independent. – Arwin Nov 04 '19 at 11:19