12

I would like to write an application something like ecommerce.

And you know that in similar applications products could have different properties and features. To simulate such an opportunity I've created the following domain model entities:

Category - this is something like "electronics > сomputers" i.e types of products. Сategories contain a list of properties (List< Property >).

Property - independent entity that contains the name, units of measure, data type. For example "name", "weight", "screen size". The same property can have different products.

Product - just contains name and list of values relating to properties. Value is an object that contains just the value field and the field id of the property.

I originally decided to make the Category like single aggregate in this scheme because for example when I add new product I need to know all data related with current category including properties related to the current category (category.AddNewProduct(product)). But what should I do when I just need to add a new property that does not belong to any category. For example, I can not do this category.AddNewProperty(property) because it clearly says that we add the property to specific category.

Ok the next step I decided separate Property into a separate aggregate but then it will be a list with simple entities.

Of course I can create something like PropertyAggregate to keep inside list of properties and buisness rules but when I add a product, I need to have inside the category the entire list of properties belonging to this category to check the invariants. But I'm also aware that keeping the links inside the aggregate on other aggregates is a bad practice.

What are the options to design this business case?

3
  • Could you provide a more complete example of a category, property, and product? Electronics or computers would be a category, iPhone X would be an example of a product, and a property would be what exactly? 11" inch display?
    – Neil
    Nov 22, 2017 at 7:40
  • you are almost right. I added some clarifications
    – cephei
    Nov 22, 2017 at 8:22
  • It seems you're looking at aggregate design exclusively from a "data container" perspective. You might also want to think about uses cases of your application, taking into account transactional aspects, collaboration/concurrent access, events that occur, state transitions, etc. Nov 22, 2017 at 14:22

2 Answers 2

8

In the DDD perspective, Category, Product and Property are entities: they all correspond to objects that have their own identity.

Option 1: your original design

You made Category the root of a single aggregate. On one side, this makes sense, because the aggregate shall ensure consistency when its objects are modified, and Product must have the Properties of its Category:

enter image description here

But on the other side, the single aggregate means that all its objects are related to a root that owns them, and all external references must be made via this aggregate root. This implies that:

  • one specific Product belongs to one and only one Category. If the Category is deleted, so are its Products.
  • a specific Property belongs to one and only one Category. Otherwise said, if "TV screens" and "Computer monitors" would be two categories, "TV screens:size" and "Computer monitors:size" would be two different properties.

The second point doesn't correspond to your narrative: "But what should I do when I just need to add a new Property that does not belong to any category". And it's not clear if the same Properties can be used in different Categories.

Option 2: Property outside the aggregate

If a Property exists independently of the Categories, it must be outside the aggregate. And the same if you want to share Properties between Categories (which makes sense for height, width, sizes, etc...). This seems definitively be the case.

The consequence is on the link between Property and things that belong to the aggregate: while you can navigate from the inner of the aggregate to Property, you are no longer allowed to go directly from a Property to the corresponding values. This navigability restriction can be shown in an UML diagram:

enter image description here

Note that this design doesn't prevent you to have a List<Property> in Category, with a reference semantic (e.g. java): each reference in the list refers to a sharable Property object in a repository.

The only problem with this design is that you could change a Property or delete it: as it's outside of the aggregate, the aggregate can't take care of the consistency of its invariants. But this is not a problem. It is the consequence of the DDD principles and the complexity of real world. Here a quote from Eric Evans in his seminal book "Domain-Driven Design: Tackling Complexity in the Heart of Software":

Any rule that spans AGGREGATES will not be expected to be up-to-date at all times. Through event processing, batch processing, or other update mechanisms, other dependencies can be resolved within some specified time. But the invariants applied within an AGGREGATE will be enforced with the completion of each transaction.

So yes, if you change a Property, you'll have to make sure that a service checks the Categories referring to it are updated as needed.

Option 3: Category, Property and Product in different aggregates

I just wonder if the assumption that a Product belongs to a single Category is founded:

  • I frequently see online shops proposing one Product under several Categories. For example, you'd find "Laptop Brand X Model Y" under the category "Laptops" and the category "Computers", and a "multifunction printer Z" under category "printer", "scanner" and "fax".
  • Isn't it possible that someone creates a Product first, and only later assigns it to Categories and fill the values ?
  • If you want to split a category, would you really delete its Products and then recreate them under the new categories ?

It won't simplify the aggregates, and you would have even more rules that span aggregates. But your system would be much more future proof.

4
  • Thank You very much this is a very useful explanation. But I would like to clarify a few points.I would like to start with the second option and who knows, maybe I'll come to the third. If I take the Property beyond the boundaries of the Category aggregate does this mean that Property becomes an aggregate in itself and needs a repository ? If it's true then how to pass required List<Property> into Category instance ? Through the constructor ? It will be right ? And how do I find out the list of Property IDs of a Category that has not yet been created ?
    – cephei
    Nov 23, 2017 at 4:34
  • @zetetic in short: yes, you'll need an independent Property repository. Either you pass a list of existing Properties to the Category's factory, or you create empty Categories and populate the list with an addProperty method. Question in return: imagine you want to have "mandatory" and "optional" Properties, and the mandatory characteristic depends on the category. How would you handle this ?
    – Christophe
    Nov 23, 2017 at 8:50
  • answering your question the first thing that comes to mind is that I can create a special entity Feature and it will only belong to the Product. and this entity will not participate in the search. what do you say ?
    – cephei
    Nov 23, 2017 at 10:29
  • @zetetic why not ! I'd have left the Values as they are currently in the product, and would ave associated the feature to the category. A category has n features (part of its aggregate), a Property defines m Features (but the link goes via the Category->feature). You then have decomposed the many-to-many relationship into more manageable elements, clarifying the aggregate boundary. Finally about repository injection: this is not required if you reference other aggregates by identity (read this article informit.com/articles/article.aspx?p=2020371&seqNum=4 )
    – Christophe
    Nov 23, 2017 at 11:33
5

As I see it, you can solve this in one of two ways:

Category is a special type of product

This means for any given product in your database, it contains a foreign key pointing to the very same table product. A product is a product only if there exist no products whose foreign key is equal to the id of said product. In other words, if it has no products under it, it is a product.

This would simplify things a bit. Products to properties would have a one-to-many relationship and therefore also your categories have a one-to-many relationship as they are also products. Adding a property to a category would be as easy as adding a property to a product in your program. Loading all properties would mean combining the properties of the product with the properties of its associated category product and on up until you reach a category product with no parent.

Your ecommerce application would need to make this distinction, but if you're likely to load products of a category anyway, it is not a performance loss to know if you're dealing with a category or a product. It also lends itself nicely to searching in tree fashion to the product level as each product (category) would open up to a list of sub-products without much additional work.

The downside of this is of course extra info present in product that doesn't make sense for a category would create awkward unused fields in product. While this solution would be more flexible in your application, it is also somewhat less intuitive.

Many-to-many relationship

Products are no longer in a composite relationship with property. You create a ProductProperty table with foreign keys of both product table and property table which link the two. Likewise, you have a category table with a many-to-many relationship with property table and a CategoryProperty table with foreign keys of both category table and property table.

Product itself would have a many-to-one relationship with category, allowing you to essentially create a list of unique properties pertaining to both product and category through a well-formalized select statement.

From the point of view of the database, this is definitely cleaner and more flexible. Your application could probably do for the most part without dealing directly with CategoryProperty or ProductProperty if the query is done properly. However neither should you treat category or product as owner of property. It should be its own entity within your program. This also means management of said properties would be a matter of creating the property itself, then associating it with a category or a product in two separate steps. Certainly more work than the first solution, but by no means more difficult.

In addition to this, you would also have to perform an additional check upon deletion of category or product if any of its properties are being used by others (unlike the first solution where you could safely eliminate all associated properties of a given product/category).

Conclusion

In a professional context, I would go the extra mile and distance category from product and product from property using the many-to-many approach. There would be no possibility of overlap of data, and in a sense, it is easier to reason each of these three as its own entity. However, by no means is the first solution a bad one as it also lets you write a simpler application. Just know that if you thought you might need to eventually shift from one solution to the other, it would probably just be in your best interests to pick the second.

Good luck!

1
  • Thanks for the detailed and interesting answer! at the database level I have already modeled as you explained in the second case, this pattern is called entity–attribute–value but I'm stuck on code level namely the definition of aggregates. In most cases, all these entities are used together. it is possible to combine into one aggregate but there are cases that like filling of directories which by sense are beaten out from the aggregate.
    – cephei
    Nov 22, 2017 at 16:31

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.