Cross-aggregate business rules (and eventual consistency?)

460 views
Skip to first unread message

Michael Ainsworth

unread,
Nov 19, 2016, 10:10:42 PM11/19/16
to DDD/CQRS
Of all the articles I've read regarding eventual consistency, they typically describe one of two things.

1. The case of multiple writers (e.g., to the same "key" in a distributed key/value store) whose changes need to be reconciled when concurrent writes are detected. This has nothing to do with Command Query Responsibility Segregation or Event Sourcing per se, but rather concurrency writability in general.
2. The time delay between when data is written and when a consistent read can be performed (i.e., containing the same data). This is typically in a Command Query Reponsibility Segregation and Event Sourcing context.

There is a third use of the term "eventual consistency", and it's the one I would like to learn more about. That is, eventual consistency in regards to cross-aggregate business rules in a Command Query Reponsibility Segregation and Event Sourcing context. That is, detecting them and correcting them.

As an example domain of blood donation, assume that there are the following aggregates. A Donor, representing a person who donates blood, and a BloodDonation, regarding a particular vial of blood that a donor has given. Both aggregates have a UUID as a unique identifier.

Now, consider the following business rules:

Rule 1. Blood can only be donated by donors who are at least 18 years of age.
Rule 2. Blood can only be donated by donors who have signed a health declaration.
Rule 3. Blood can only belong to a single donor.

Of the above three rules, the first two are legal requirements, while the second is a law of nature (that is, any amount of blood must have been produced by a single animal). Note, however, that Donors can be less than 18 (perhaps they could also donate money, etc), so this business rule spans the two aggregates.

In modeling the above business rules, I see three options.

Option 1. Combine the Donor and BloodDonation into a single aggregate.
Option 2. Keep zero or more UUIDs of all the BloodDonations within the Donor aggregate.
Option 3. Keep the UUID of the Donor within the BloodDonation aggregate.

Option 1 decreases scalability. That is, a larger aggregate is more likely to be accessed concurrently. For example, one user could be recording a new blood donation while another is updating the donors postal address. I understand that because these affect different fields (i.e., the internal array of blood donations vs the address property) that the application can re-try commands (taking the latest events into consideration) to reduce the concurrency issues here. Option 1 also means that other aggregates cannot reference the BloodDonation aggregate directly (for example, if a BloodFusion aggregate was introduced), because aggregates should only be able to hold references to the root of another aggregate. On the other hand, option 1 allows all three business rules to be enforced as invariants (that is, they can be enforced transactionally).

Option 2 also allows rules 1 and 2 to be enforced transactionally. That is, if a user is trying to record a blood donation, then the Donor aggregate could refuse to add the UUID of the blood donation to its internal array of BloodDonation UUIDs if either the Donor is less than 18, or if they have not signed the health declaration. On the other hand, it is possible for rule 3 to be violated, because it becomes possible for two Donors to reference the sample BloodDonation. This could be a significant problem. For example, if on the read side you are trying to determine what blood type a particular donation is, then it could give multiple answers (B positive and B negative), one for each Donor. In this case, the violation of rule 3 would need to be detected and resolve somehow (this is the type of "eventual consistency" I am pondering at present - rules that span multiple aggregates). Also, option 2 complicates the business process and makes the events less meaningful. For example, the recording of a new blood donation requires a process manager (created with the StartBloodDonationProcess command and recorded with the BloodDonationProcessStarted event). This process manager needs to create a new blood donation (the DonateBlood command and BloodDonated event), then instruct the donor to reference the blood donation (the RecordBloodDonationDonor command and BloodDonationDonorRecorded event). If the first command fails, the blood donation is "deleted" (for lack of a better word), the second command is not issued, and the BloodDonationProcess enters the "failed" state. If the first command succeeds, but the second fails, again, the blood donation is deleted and the BloodDonationProcess enters the "failed" state. When two Donors reference the same BloodDonation, one of them will need the reference removed. The RemoveBloodDonationFromDonor command and BloodDonationRemovedFromDonor event would be used in this case. These events are less meaningful than the CorrectBloodDonationDonor command and BloodDonationDonorCorrected event used by option three.

Option 3 allows rule 3 to be transactionally consistent, by the simple fact that a BloodDonation contains a single UUID pointing to the Donor from which it was obtained. If a user discovers that they recorded the wrong donor for a particular donation (oh my!), they can issue a CorrectBloodDonationDonor command (producing the BloodDonationDonorCorrected event), which is more meaningful that the command/event names used in option 2 ("RemoveBloodDonationFromDonor" and "BloodDonationRemovedFromDonor"). On the other hand, option 3 means that the first two rules can be violated. That is, user A could be issuing a CorrectDonorBirthDate command at the same time as user B is issuing a RecordBloodSample command. The end result of which is that a blood donation was taken from a donor who is less than 18.

There are other things to consider here as well. For example, in option 2, the natural directionality of the relation is from the donor to the blood donation. If the write side needed to look up the donor for a particular blood donation, it would need to use a domain service (or some other mechanism). Likewise, in option 3, the natural directionality of the relation is from the blood donation to the donor. If the write side needed to look up all the blood donations provided by a particular donor, then again a domain service would be required.

So, given the above scenario:

1. Which of the three business "rules" do you see as invariants, and which do you see as rules which can be violated (and rectified by either a user or a process manager)?

2. What do you think of the above analysis of the problem? Are there other things I should consider when choosing between the three options? Are there any other options?

Appreciate your input, folks.



xiety

unread,
Nov 21, 2016, 2:37:03 AM11/21/16
to DDD/CQRS
I will validate all this rules against the read model. And there will be an Event from UI called BloodDonated without any aggregates involved. Because I don't know what to say to person in front of me, that their donation was incorrect.
"Pour it back, please".

Michael Ainsworth

unread,
Nov 21, 2016, 3:58:30 AM11/21/16
to DDD/CQRS
"I will validate all this rules against the read model."

I assume you're going for option 3, and that the client will validate these rules prior to submitting the command (in order to reduce the likelihood of a problem).

"And there will be an Event from UI called BloodDonated without any aggregates involved."

Why does the UI generate an event? And why is there an event not tied to an aggregate/stream?

"Because I don't know what to say to person in front of me, that their donation was incorrect. "Pour it back, please"."

I understand that in a sense the system is a "downstream" event processor in that the event (the donating of blood) has already occurred, and the system is just recording this fact. But why can't it be modelled as a command? The system could possibly say "The donor is less than 18 years old. Please discard any blood donated."

Thanks for the input.

xiety

unread,
Nov 21, 2016, 4:12:21 AM11/21/16
to DDD/CQRS
1. I would rather prevent the donor from starting the blood donation, not from rejecting already finished procedure.
2. In your situation the command would have name more like "RegisterFinishedBloodDonation".
3. What manager and donator must do with the rejection? Just go home with his blood? The fact is already done in real world, it must be recorded in the system. And then process managers can find the violations and do something with them later.
4. All validation for command can be done against the read model before the domain model involved. Those tiny situations when user changes his age just at the moment of donation registration can be managed by retries or just ignored.

Kyle Cordes

unread,
Nov 21, 2016, 8:11:03 AM11/21/16
to ddd...@googlegroups.com
On November 21, 2016 at 2:58:32 AM, Michael Ainsworth
(michae...@gmail.com(mailto:michae...@gmail.com)) wrote:

> I understand that in a sense the system is a "downstream" event processor in that the event (the donating of blood) has already occurred, and the system is just recording this fact. But why can't it be modelled as a command? The system could possibly say "The donor is less than 18 years old. Please discard any blood donated.”



Actually your message in conjunction with a couple of others here went
exactly to the heart of eventual consistency. It is not a computer
problem, it is a human problem. In the example scenario you are
writing about, there is no possible way to universally enforce this
rule (donor must be 18+ to donate) pre-donation. The best we can
possibly enforces “to the best of our knowledge at the time of
donation, donor was 18+ at the time of donation”. But some other
additional information might be learned later. Then some business
process (some decision made by humans on how to handle a scenario
which everyone hoped would not happen) must deal with these things.
There might or might not be, in a real human organization, a process
for “oops, we realize retroactively that we accepted a donation from
someone not eligible to do so. What shall we do with the inventory?
What shall we do if it was already used for some downstream process?”.

I have never worked in this problem domain, but I would guess the
answer is something like: do our best to retroactively follow the
rules when we learn about facts retroactively, and have a way of
reporting even further downstream of how well it worked so that future
processes might be changed.

Here’s the interesting bit, dealing with that real-world complexity is
much more feasible in a system which has a way to represent what
actually happened (and then apply rules to those things that
happened), than it is in a system which has no way to represent a fact
that was not supposed to be able to happen.


--
Kyle Cordes
kyle....@oasisdigital.com

Michael Ainsworth

unread,
Nov 21, 2016, 8:06:20 PM11/21/16
to DDD/CQRS
Thanks for the input Kyle. It's enlightening to read.

1. What ways are there of detecting these violations? A periodic batch report (based on read-only projections) is one approach, but this has the disadvantage of not being persisted, but xiety alluded to a process manager. How do you envision the process manager would work?

2. What about a domain with a significant number of cross-aggregate inconsistencies where staff can make "judgement calls". E.g., Sally might ignore the reported age-error for Jame's blood donation because "He turns 18 next week". If using batch reporting, the next time the report is produced it will contain the same error ("James was not 18 when he donated vial ABC of blood"), even though Sally said that this violation is permitted.

Again, thank you for your response.

xiety

unread,
Nov 22, 2016, 12:45:30 AM11/22/16
to DDD/CQRS
Violation of 18 year constraint will be in only one case, when some boy was already 18, and then, in the exact same millisecond as registration of donation occured, he suddenly becomes 17. You can ignore this. Or your domain expert should tell you, what did they do with wrong donations in real life. Imagine, that there are no computers invented, how did they fight this problem? Some additional checks upstream?

Kyle Cordes

unread,
Nov 22, 2016, 8:27:35 AM11/22/16
to ddd...@googlegroups.com
On November 21, 2016 at 7:06:22 PM, Michael Ainsworth
(michae...@gmail.com(mailto:michae...@gmail.com)) wrote:
> Thanks for the input Kyle. It's enlightening to read.
>
> 1. What ways are there of detecting these violations? A periodic batch report (based on read-only projections) is one approach, but this has the disadvantage of not being persisted, but xiety alluded to a process manager. How do you envision the process manager would work?
>
> 2. What about a domain with a significant number of cross-aggregate inconsistencies where staff can make "judgement calls". E.g., Sally might ignore the reported age-error for Jame's blood donation because "He turns 18 next week". If using batch reporting, the next time the report is produced it will contain the same error ("James was not 18 when he donated vial ABC of blood"), even though Sally said that this violation is permitted.


It is straightforward to implement a projection which detects these
rule violations. As with all projections it consumes the sequence of
events in order, updating its persistent state according to the
results of each one. Therefore it is persisted (addressing one of your
questions).

Such a projection could be written to works a batch report, but there
is no reason it must be. It could also be written such that its output
is available as a real time “dashboard”, displaying information about
this and any other rule violation to anyone who needs to see it.

(I think there is an interesting fuzziness between a projection and a
process manager. In some sense when people say process manager they
mean a projection which has some additional business logic attached,
such that inferences about the overall state of the system can trigger
additional commands/events. But for what I’m describing here, just the
ordinary meaning of projection is plenty good enough.)

To answer your second question: once you have this projection
displaying a dashboard about violations of these rules, you are now
well-positioned to add another kind of logic to the system. Perhaps
certain people can issue commands (which dispatch events blah blah
blah) recording decisions having been made about such rule violations.
Those decisions (in the form of events) can then continue and flow
through the projections in the same way. Thus making it possible (and
I would say, quite straightforward) to have a real-time “push”
dashboard showing past and present rule violations, automatically
moving such violations from the “need to do something about this” list
to the “here are some past decisions that were made” list as those
decisions come through.

As with many of my posts here, this reflects our particular
understanding here (at Oasis Digital where I work) of CQRS/DDD/ES, and
we have summarized some of that in a series of blog posts:

http://blog.oasisdigital.com/category/cqrs/


--
Kyle Cordes
kyle....@oasisdigital.com

Michael Ainsworth

unread,
Nov 22, 2016, 7:13:09 PM11/22/16
to DDD/CQRS
Thanks Kyle.

So a command might be IgnoreBloodDonationAgeCheck, which is directed to the BloodDonation, which then emits a BloodDonationAgeCheckIgnored event, which the projection will pick up and toggle a flag (moving it from the "fix this" list to the "ignore this" list). The BloodDonation aggregate itself doesn't change state, but the events are recorded permanently so the projection can be completely rebuilt from these events.

Sound about right?

(BTW, I've already read several of your articles. I particular like the discussion on linear event store, which is a great balance between core DDD/CQRS/ES concepts and pragmatism).

Kyle Cordes

unread,
Nov 22, 2016, 7:42:55 PM11/22/16
to ddd...@googlegroups.com
On November 22, 2016 at 6:13:11 PM, Michael Ainsworth
(michae...@gmail.com(mailto:michae...@gmail.com)) wrote:

> Thanks Kyle.
>
> So a command might be IgnoreBloodDonationAgeCheck, which is directed to the BloodDonation, which then emits a BloodDonationAgeCheckIgnored event, which the projection will pick up and toggle a flag (moving it from the "fix this" list to the "ignore this" list). The BloodDonation aggregate itself doesn't change state, but the events are recorded permanently so the projection can be completely rebuilt from these events.
>
> Sound about right?



Yes, that’s right.



--
Kyle Cordes
kyle....@oasisdigital.com
Reply all
Reply to author
Forward
0 new messages