0

I have read this post, in this post Udi Dahan talks about many to many relationships. In that example he explains that in the case of a many to many relationship like the one a job would have with job boards, and taking into account the bounded context of adding a job to a job board the aggregate root would be the job board and you just add the job to it. In the comment section he also explains that in a different bounded context the Job would be the aggregate root, which makes sense since the job can exist without the job board and there many operations that you could do to a job that does not affect the job board. I have a similar problem but I can not seem to figure out how this would work out. I have 2 issues:

  1. In case where we would need to delete a job, it looks like depending on wether the job has been posted or not, we will need to either delete job alone or delete the job but also remove it from the board, which would means to modify two aggregate roots in the same transaction, but also where should this code go? domain service?
  2. if job and job board can be two different aggregates, job entity needs to exist in both context so how do we deal with this, just create two job classes with duplicated data?

UPDATE 1:

So this is the scenario I'm dealing with... I have a routing app, I have Requests which represent a trip request, I have routes, which have stops and each stop have one or more requests. In order to create a route, I use an external service that does the routing, and stores the routing result in a routing table.

The problem is that I do not how to model this relationship, here is a use case to consider, request cancellation is a process that depending on the state of the request and the state of the route can lead to different actions:

  1. Request is not routed (not assigned to a route), just cancel the request and that is it.
  2. Request is routed, route is schedule,then cancel the request, remove the request from the route, and re-create the route (using an external library), since remove a request may lead to removing a stop, so I need to recreate the route internals, it is still an update.
  3. Request is routed, route is en route, then I mark the request as no-show, and update the route.

So at first I though that request, route and routing table are separate aggregates, but that means that I need to modify more than one aggregate in the same transaction, (either by using a service or by using domain events) I'm not sure if it makes sense to create a higher level aggregate root (with request, and route data, and eventually routing data) because I' won't always have all the data to load the aggregate root, in facto most of the times I'll have a portion of it, either 1 request or a route with multiple requests. I'm open to suggestions, because I can not seem to get a solution to this.

UPDATE 2: So adding some more context, I'll add some more detail to the entities:

  • Request, it represents a trip request, it has several states with a defined workflow
  • Route, it has a defined workflow with defined transitions, has a collection of stops, and each stop has a collection of payloads, each payload has a request id, (Route -> stops[] -> payloads[]-> requestId)
  • Routing, it represents the result fo calling a routing engine, which based on a series of requests that you want to route it will generate the route/routes

So this entities are stored in a mongodb collection, lets see the Use Cases:

UC - Request Cancellation I can cancel a request using the request id only, but depending on the state of the request I may need to modify the route also. 1 Request is NOT routed, so with the request id, I get the request and cancel it, this one is simple. 2 Request is Routed, and Route is Scheduled, in this case I need to get the request, then get the route and all the requests that are tied to that route (it includes the one that triggered the command), then remove the payloads (and the stop if it has only one payload) that are tied to the requests, since this option can change the stops I need to re-create the route using an external api (routing engine) and create an entry in the routing table. 3 Request is Routed, and Route is en-route, in this case I need to get the request, then get the route and all the requests that are tied to that route (it includes the one that triggered the command), and change the request as a no-show, but also mark the payload as a no-show

UC - Start a route Once a route is created and scheduled, I can start it, which means modifying the state of the route, state of the stop, state of the payloads, and state of the asociated requests.

As you can see in the use cases, route - request and routing table, are very closely related, so at first though of having separate aggregates root, Request is an AR, route is an AR, routing is an AR, but this means modifying more than one AR in the same transaction. Now lets see what an AR that will have all entities will look like

class Aggregate {
  constructor(routeData, requests[]) {
  }
}

So lets see the UC again UC - Request Cancellation

  1. In this scenario I only have 1 request data, so I have to leave routeData empty, which does not sound right
  2. In this one I have route and request data so I'm good
  3. In this one I have route and request data so I'm good

The main problem here is that some operations can be done on 1 request, and some other operations will be done on the route, and some other will be done in both. So I can not always get the aggregate by Id, or I can not always get it with the same id.

rage77
  • 3
  • 4
  • It seams like there is a strong relation between "request", "route" and "stop". What make you think that you need multiple aggregate roots? Would it make sense for "request", "route" and "stop" to be part of the same aggregate? – Maxime Gélinas Apr 06 '21 at 20:15
  • Actually it would, but I have read that you need to get an aggregate always by Id, and in this case I do not think I can, in some cases it will be by requestId, in some other cases it will be by routeId, so, this big aggregate will always have some part of the data, sometimes just a request, some other times multiple requests and route data, and some other times just route data, so what I mean is you can not execute all the operations just some of them depending on the data you are loading – rage77 Apr 06 '21 at 21:33
  • "you need to get an aggregate always by Id" you're right about that. It's hard to help while knowing so little about your usecases. Please provide some. – Maxime Gélinas Apr 07 '21 at 01:56
  • Thanks for the reply, I have added some more context information and tried to explain 2 use cases. – rage77 Apr 07 '21 at 12:46
  • It sounds to me like you have only one aggregate here that is "request" which can have an optional route if in routed state. Send commands to the request aggregate to update the route. – Maxime Gélinas Apr 07 '21 at 15:31
  • ok, I though about that as well... so how about the cases where I need to access the route directly?, like start route in this case I get a routeId, and I should load the route data, modify the route state, modify the first stop, and also the requests that are tied to that payload, in this case I will have several requests (or just 1, but I could have more than one) and a route. – rage77 Apr 07 '21 at 16:01
  • Well dont access the route directly. Just carry around the request id. It makes a lot more sens since the route commands should be validated by the request internal state. – Maxime Gélinas Apr 07 '21 at 16:08
  • well... I can not restrict API functionality because of DDD constrain, when a route starts or ends, or a stop has been completed that means modifying 1 or more requests, I do not know which requests before hand, since the route is what groups requests together – rage77 Apr 07 '21 at 17:15
  • Considering the relations are `[request (root)] 1--1 [route] 1--* [stop] 1--1 [request]` I was talking about sending the command to the root request not the sub-requests. – Maxime Gélinas Apr 07 '21 at 17:46
  • ok, so maybe I did not explained properly... so this how it should be, route 1-n requests, request 1-1 route, but this relationship is optional, it means you can have requests that are not part of any route, so a request may or may not have a route, a route will have at least 1 request (most of the times many), what I meant was, if I want to access a route by its route id I should be able to do it, and in that case there is not a root Request – rage77 Apr 07 '21 at 18:02
  • Ok I did not understood that correctly in the first place. Maybe you are right about request and route being two diff aggregates. If that so, route and/or request should splitted in two you cant have a circular dependency like that e.g. in a order bounded context a product isnt a product its a order item which dosent have the same props than the product from the catalog bonded context even if its the same physical thing. – Maxime Gélinas Apr 07 '21 at 18:32
  • So you are saying that it is OK to have a request aggregate, and a request entity (within route aggregate) with maybe a different set of properties (or the same)? so if this is the case, so I guess it is also OK to modify the request entity within the route aggregate? – rage77 Apr 08 '21 at 12:32
  • Yes its perfecly fine as long as they serve different purposes. You should find a different name to avoid confusion tho. Ubiquitous language is very important in DDD. – Maxime Gélinas Apr 08 '21 at 19:32

1 Answers1

2
  1. There is no such thing as "two aggregate roots in the same transaction". Transactions are scoped inside a single aggregate since in theory all aggregates should live in their own micro-service. The proper way to update two or more aggregates in a atomic way is with a saga. Sagas are a complex/advanced topic. I recommend avoiding them if you can by re-thinking your design.

  2. Spliting an entity between two bounded contexts is perfecly fine and most of time necessary, but these entities should be adapted to fit their context e.g. in the boards bounded context the "job" entity could be a "board card" which will not have the same properties than the "job" entity from the jobs bounded context.

Maxime Gélinas
  • 2,202
  • 2
  • 18
  • 35
  • Hey Maxime, thanks for your answer... I posted this sample in order to see if I can get a solution for my actual problem, I will update the question to see if you can help me. – rage77 Apr 06 '21 at 19:21
  • I think there is no reason to have single aggregate in single microservice. what happen if our architect choose monolithic architecture for project? – ihsan Jul 13 '22 at 06:13
  • @ihsan By definition a microservice should do only one thing (one aggregate). You are free to follow or not these rules depending on the context and limitations. – Maxime Gélinas Jul 14 '22 at 11:49