I'm quite new to DDD and I need to get something straight. It's asked many times but I never found a satisfying fits-all answer.
The question is: How to validate an aggregate requiring data from another aggregate root in a different context.
I'm assuming bounded contexts as if they could be microservices so they can't have tight coupling.
Googling everywhere I came across several solutions but it looks like everyone is proposing something different and there obviously tradeoffs for every solution.
So here is my hypothetical example which comes very close to our actual business (which is currently a CRUDy monolith).
A customer can reserve an ad space on our website for promotion purposes. Let's call it a 'Campaign'
The campaign is in a pending state on its initial creation and will be activated based on a schedule. The business rules are:
- The campaign must be started on the schedule's start date (and end on its end date)
- The campaign can only be activated if the customer has a signed agreement
- The campaign can only be activated with one or more creative ads.
So I figured out that I would probably have the following contexts and aggregates
- Customer => CustomerAggregate
- Legal => AgreementAggregate
- Management => CampaignAggregate
- Scheduling => CampaginScheduleAggregate
- Creatives => AdAggreate
So I already figured out the campaign can be activated/deactivated by the schedule using events. I've implemented this using the 'ADayHasPassed' event example which makes the schedule dispatch events when the schedule starts and ends based on https://verraes.net/2019/05/patterns-for-decoupling-distsys-passage-of-time-event/ (very interesting solution).
Also using primitive types in the events keeps them decoupled from other contexts. The only coupling is the reference to the event's class name currently.
But now the other two rules. Those require data from other bounded contexts for validation. I've found two solutions which come close but what would be the appropiate DDD one?
I assume both rules would be checked by specifications btw.
- Query another bounded context using its public queries (the same as used over HTTP by external parties). We have a query bus already implemented for our public API so it can be used.
- Keep a local copy of the number of created creatives and a flag if the agreement is signed in the campaign.
Solution one still feels a bit like tight coupling. If used in a specification, would I create an interface for the domain layer and the actual query call in the infrastructure? (so it can be swapped for another implementation in a later phase?) or just a single in the domain layer? - And if I would use this in a specification and it's also used for querying like in the example from https://enterprisecraftsmanship.com/posts/specification-pattern-always-valid-domain-model/, I would assume the query data can become massive if there are many records in the system.
Solution two (as proposed by Codeopinion, to keep a local copy of the relevant data) sounds great but may make the aggregate become bloated with the number of creatives and agreement details if there become more requirements. Also if the agreement is signed before creating a new campaign it would never know about it, so it still has to query it in its creation phase.
Another solution would be to store de agreement and number of creatives in separate aggregates in the campaign management bounded context. But how would the campaign aggregate use the other aggregate roots for its validation. All my research ended up in articles about distinct bounded contexts, not the same. Or should the aggregate root be a customer inside campaign management, holding: a collection of campaigns, number of creatives and customer agreement state (just something what crossed my mind while writing this)?
I read a lot of articles but the solutions are often not in agreement with preventing tight coupling disagree with each other or based on framework specific (c#, java) and not generic for most languages.
Recommended reading: Helland 2005.
Don't need to do that. Seriously.
If changes to entity A need to have immediate effect on the handling of information by entity B, then those two entities must be part of the same aggregate (in other words, to change entity B, you must be holding both the lock on entity B and the lock on entity A).
On the other hand, if a recent but not necessarily up to date copy of the information is satisfactory (a microsecond difference in timing shouldn’t make a difference to core business behaviors), then we treat the information from outside the aggregate as reference data.
In effect, the unlocked reference data becomes just another input that we pass to the model as part of this use case, and then we think about whether we want to pass the reference data in every time we need it, or if we want to cache a copy of that data within the aggregate itself, and how do we want to handle cache-invalidation.
What I would normally expect: "this" service has a logical cache of the reference data, which is obtained via some asynchronous process and has its own invalidation logic. Information from this cache is passed to the aggregate in the use cases where it is needed, and the aggregate itself makes its own decisions about whether the data is "fresh enough" to satisfy its needs.
(How the information gets from "that" service to the cache is mostly plumbing - any stable channel should be fine, and one would certainly hope that the external API is sufficiently stable so as to be a reasonable choice.)