pixelbadger
pixelbadger

Reputation: 1596

Can I cascade changes from one aggregate root to another?

I'm having trouble modelling a relationship in DDD. I have four entities:

When I remove a Claim from a Service, it should also be removed from all Roles that have that Claim assigned to them. Equally, when I change the name of a Service, that name should be reflected in all the Teams.

I currently have a Service aggregate root (to allow Services to be added and altered independently of any one Team), and a Team aggregate root (to allow management of Team-specific Roles). This creates two problems:

There's something clearly horribly wrong with my model, and was wondering if anyone can illuminate where I've gone awry.

Upvotes: 0

Views: 267

Answers (2)

Stormwind
Stormwind

Reputation: 824

Imho this should be possible and functional. Your problem raises maybe from that you have a circular reference, a loop, kinda.

I painted a picture, cannot type it in here, but consider a circle with 4 major directions:

  N  
W   E  
  S

or, for your different components:

     Team  
Service   Role
     Claim  

Circular-wise. Team touches Service and Role, Service touches Team and Claim, etc. Each component always touches 2 other ones.

Imo you should use "slots" to address each individual element in a component (i call these 4 layers "components", so you have 4 components).

You'd implement a "slot-thinking", because you of course have multiple occurances of each slot as well - need a way to housekeep them, add new ones, remove redundant ones, and above all update the "pointers" (let's call them so) from one components any part to another components any part. Oh well, like this:

Service components housekeeping (the [ ] is a "slot", a "list in a list")
(upper row indexes into Team slots, lower row indexes into Claim slots:

    [ ][1 3 4][ ][1 2 5][ ][ ]
    [ ][99 112 18 23 66 21][ ][1][ ][ ]

Apparently your Service slots 1, 3, 5 and 6 are atm empty. If a new Service appears, you'd probably take the lowest free slot (1) into use. If all slots are occupied, you'd append a new to end of list and grab that.

Apparently your Service slot 2 points at Team slots 1,3 and 4.
Apparently your Service slot 2 points at Claim slots 99, 112, 18, 23, 66 and 21.
Apparently both Service slot 2 and slot 4 point at Team slot 1. Hence Team slot 1 would point at Service slots [2 4].

See the system? When you update a component, you

  1. Update in the component itself towards 2 other components's slot indexes.
  2. Visit the component clockwise and update it's counterclockwise slot(s).
  3. Visit the component counterclockwise and update it's clockwise slot(s).

Setps 2 and 3: As multiple slots may point at multiple other (being Service, as we talk now) component's slots, you have to walk through the entire slot lists, ofc., so that no pointers are forgotten.

Then you can at any time resolve anything you need, and the circle remains intact. You can jump in to the circle at any point, and walk through it, ie. follow a path of dependencies.

Upvotes: 0

VoiceOfUnreason
VoiceOfUnreason

Reputation: 57377

There's something clearly horribly wrong with my model, and was wondering if anyone can illuminate where I've gone awry.

It looks to me as though you have prioritized nouns over rules, and gotten yourself lost in the swamp.

Example:

Equally, when I change the name of a Service, that name should be reflected in all the Teams.

Riddle: what invariant do you need to satisfy in your Team aggregate that needs to know the name of a Service aggregate? Is there some business rule like "if the Service name is camel case, then the Team is limited to 10 roles"?

For that matter, what invariant do you need to satisfy in your Service aggregate that needs to know the name? Do services in your domain model evolve differently depending on how their names are spelled?

What I think you need to do is look at your requirements gathering with two different viewpoints. You can look at reports, supported queries, and so on to get a flavor for what data you need to incorporate in your solution; what state you need to have available.

But for aggregates, the interesting questions are about how that state changes, and which bits of state constrain the changes that can occur.

Put another way, the job of the aggregate isn't to encapsulate a bunch of related state, but instead to reject commands that would change that state in a way that violates the invariant.

Can I cascade changes from one aggregate root to another

For database transaction meanings of "cascade", no.

The basic notion of the aggregate it that it is a collection of state, and rules, that doesn't need to look outside itself. Any change to a Team aggregate can be verified by looking only at the state of the Team; any change outside of the Team aggregate can ignore any state of the Team.

So if your thinking is "When this change to the Service happens, then that change to the Team should happen... if the team can't change, then the change to the Service must be rolled back", this is equivalent to the discovery that your boundaries are in the wrong place.

BUT

It is common for changes to an aggregate to have consequences on other aggregates. Maybe a scenario like "we've decommissioned the service, therefore the team is released from the responsibility for supporting it, and therefore has capacity to accept new responsibilities."

Contrived example; sorry about that.

If Service and Team are separate aggregates, then we would be looking at two different commands: Service.decommission() and Team.freeCapacity().

To invoke Team.freeCapacity(), a common solution is to publish a "domain event", ServiceDecommissioned. An event handler (often called a "process manager"), subscribing to that event, identifies the appropriate team, and schedules the freeCapacity command.

Note: Team is still responsible for its own invariant; it might reject the freeCapacity command if doing so would introduce an internal inconsistency.

Process managers are especially common in solutions that use and .

Upvotes: 2

Related Questions