How should I handle multiple aggregates root interaction

1.7k Views Asked by At

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.

1

There are 1 best solutions below

3
On
  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.