5

Is it ok to have many aggregate root like this in a bounded context

Product
   ProductID : GUID
   Name : string
   Price : float

ProductPromotion
   ProductID : GUID
   Discounted : float

ProductShortName
   ProductID : GUID
   ShortName : string

I don't know it will break the rule of bounded context while these are difference kind of product but within a context.

link to my actual problem

kitta
  • 1,723
  • 3
  • 23
  • 33
  • I've edited the question. The last question is the things I really need. But if I design like this it may fix the problem by using only domain design. – kitta Jun 22 '18 at 01:57
  • answered based on the previous version. will check the new one. – Pedro Goes Jun 22 '18 at 02:04
  • @PedroGoes I asked on se stackexchange. You can answer it there :) https://softwareengineering.stackexchange.com/questions/372857/cqrs-how-to-query-aggregate-root-using-others-fields-rather-than-guid-id – kitta Jun 22 '18 at 02:12

2 Answers2

8

Each Aggregate Root should have its own unique ID in a DDD scenario and that seems to be missing (if I'm understanding your scenario correctly, it seems you want all of the 3 classes to be roots). So, if your design was something along these lines:

Product <ROOT>
   ProductID : GUID
   Name : string
   Price : float

ProductPromotion <ROOT>
   PromotionID: GUID
   ProductID : GUID
   Discounted : float

ProductShortName <ROOT>
   NameID: GUID
   ProductID : GUID
   ShortName : string

on its own, this design doesn't hurt the aggregate or bounded context principles. One aggregate should be allowed to hold a reference to another aggregate root's id (as in countless shopping-cart examples where the LineItem holds a reference to the ProductID).

What you shouldn't be doing (and it appears you aren't) is holding full references to whole aggregates inside another.

However, you should always reflect on what are your goals when designing aggregates. Yes they should be small, but they also must convey an objective. Usually they are designed to enforce certain invariants and these examples don't seem to be achieving that goal.

Maybe your example was overly simplified for the question though and I can't really evaluate it but it doesn't seem to me "ProductPromotion" and "ProductShortName" are good candidates for full aggregates the way they are presented.

Pedro Goes
  • 726
  • 3
  • 10
  • Generally if I design I will not let these aggregate root in the same bounded context. Is it bad? – kitta Jun 22 '18 at 03:16
  • But also by doing the way you mentioned I cannot reference to promotionID using promotionID and It's required because all those logic happened in the process manager which not good to query from the read models :*( – kitta Jun 22 '18 at 03:17
  • why are they not in the same bounded context? it seems like they should be. – Pedro Goes Jun 22 '18 at 03:36
  • you can access other aggregates (in a process manager for an example) to run a process. What you shouldn't is hold a static reference to a whole aggregate inside another aggregate. Those are different things. – Pedro Goes Jun 22 '18 at 03:37
  • I mean the process manager cannot get the ProductPromotion using ProductID from EventStore because EventStore can only get the aggregate root of ProductPromotion by PromotionID. – kitta Jun 22 '18 at 03:46
  • Or in this case I should get the promotion id from read models before create the process manager? But by this way what if there are more sophisticate business logic to select a promotion? Application layer will be so thick with business logic isn't it. – kitta Jun 22 '18 at 03:48
  • 1
    I think your best course of action is to redesign your aggregates at this point. It's clear they are not currently helping you achieve your domain rules. You have a couple of options which are hard to explain in a comment, but I'll fire away some of them: (1) you can make your product be a factory for the promotion aggregate and ensure promotionID matches productID on creation. (2) it seems like you're better off having a "pricing" aggregate which concentrates all the price+discount logic for a product (including active discounts, temporal discounts, etc). – Pedro Goes Jun 22 '18 at 03:52
  • 1
    (3) you could find the promo through the read model, always being aware that the read model might be stale and is only eventually consistent. – Pedro Goes Jun 22 '18 at 03:52
  • Hi, I've discussed with teams. I not sure why promotion and product aggregate should be in the same bounded context. Why don't purchase a product process asking promotion bounded context about promotion. Then promotion manage themselves about the promotion. – kitta Jun 22 '18 at 06:37
  • @b.ben you might want to post a new question for the modelling problem - it's significantly different from this question about IDs – guillaume31 Jun 22 '18 at 11:28
  • Regarding the IDs, it seems to me you two guys are saying the same thing (all aggregates should have the same ID value for the same Product). The misunderstanding is that in the question all IDs are called ProductID, where @PedroGoes says they should be called ProductID, ProductPromotionID and ProductShortNameID, but still with the value that matches ProductID for the three of them. – Francesc Castells Aug 23 '20 at 05:40
1

I've used that "pattern" in two different applications and it worked fine.
I think that if your aggregates have inside them a state (all its properties) that lets you check all the invariants of the commands it can work.
Problems arise if to make validation inside a command method of an aggregate you need a property stored inside another one.
In another response I read about having a unique ID per aggregate, well in this case the id of the aggregate can be seen as (AggregateType, GUID).
In the end you are just storing a single aggregate in multiple partitions, being an aggregate a boundary of consistency/transactions, until your commands modify a single part that is stored with an atomic operation your aggregate don't break any rules.
In order to understand if it fit your problem you have also to check how these aggregates are read, because if you need to read the entire aggregate state, depending on the persistence technology, you can end up to have multiple queries to the store to retrieve all of them degrading the performance, and this can or cannot be acceptable depending of your use cases (in my cases one was developed using event sourcing, so the read model was done to be a single read operation, in the second one with some cache help also doing multiple queries the performance was ok).

rascio
  • 8,968
  • 19
  • 68
  • 108