235
CHAPTER 6 | Tackle Business Complexity in a Microservice with DDD and CQRS Patterns
Vaughn Vernon says the following in
Effective Aggregate Design. Part II: Making Aggregates Work
Together
:
Thus, if executing a command on one aggregate instance requires that additional business rules
execute on one or more aggregates, use eventual consistency […] There is a practical w
ay to support
eventual consistency in a DDD model. An aggregate method publishes a domain event that is in time
delivered to one or more asynchronous subscribers.
This rationale is based on embracing fine-grained transactions instead of transactions
spanning many
aggregates or entities. The idea is that in the second case, the number of database locks will be
substantial in large-scale applications with high scalability needs. Embracing the fact that highly
scalable applications need not have instant transactional consistency between multiple aggregates
helps with accepting the concept of eventual consistency. Atomic changes are often not needed by
the business, and it is in any case the responsibility of the domain experts to say whether particular
operations need atomic transactions or not. If an operation always
needs an atomic transaction
between multiple aggregates, you might ask whether your aggregate should be larger or wasn’t
correctly designed.
However, other developers and architects like Jimmy Bogard are okay with spanning a single
transaction across several aggregates
—
but only when those additional aggregates are related to side
effects for the same original command. For instance, in
A better domain events pattern
, Bogard says
this:
Typically, I want the side effects of a domain event to occur within
the same logical transaction, but
not necessarily in the same scope of r
aising the domain event […] Just before we commit our
transaction, we dispatch our events to their respective handlers.
If you dispatch the domain events right
before
committing the original transaction, it is because you
want the side effects of those events to be included in the same transaction. For example, if the EF
DbContext
SaveChanges method fails, the transaction will roll back all changes, including the result of
any side effect operations implemented by the related domain event handlers. This is because the
DbContext life scope is by default defined as “scoped.” Therefore, the DbContext object is shared
across multiple repository objects being instantiated within the same scope or object graph. This
coincides with the HttpRequest scope when developing Web API or MVC apps.
Actually, both approaches (single atomic transaction and eventual consistency) can be right. It really
depends on your domain or business requirements and what the domain experts tell you. It also
depends on how scalable you need the service to be (more granular transactions have less impact
with regard to database locks). And it depends on how much investment you’re willing to make in
your code, since eventual consistency requires more complex code in order
to detect possible
inconsistencies across aggregates and the need to implement compensatory actions. Consider that if
you commit changes to the original aggregate and afterwards, when the events are being dispatched,
if there’s an issue and the event handlers cannot commit their
side effects, you’ll have inconsistencies
between aggregates.
A way to allow compensatory actions would be to store the domain events in additional database
tables so they can be part of the original transaction. Afterwards, you could have a batch process that
detects inconsistencies and runs compensatory actions by comparing the list of events with the
236
CHAPTER 6 | Tackle Business Complexity in a Microservice with DDD and CQRS Patterns
current state of the aggregates. The compensatory actions are part of a complex topic that will require
deep analysis from your side, which includes discussing it with the business user and domain experts.
In
any case, you can choose the approach you need. But the initial deferred approach
—
raising the
events before committing, so you use a single transaction
—
is the simplest approach when using EF
Core and a
relational database. It’s easier to implement and valid in many business cases. It’s also the
approach used in the ordering microservice in eShopOnContainers.
But how do you actually dispatch those events to their respective event handlers? What’s the
_mediator
object you see in the previous example? It has to do with the techniques and artifacts you
use to map between events and their event handlers.
Dostları ilə paylaş: