4

Domain Events in DDD — System Architecture

 2 years ago
source link: https://dckms.github.io/system-architecture/emacsway/it/ddd/tactical-design/domain-model/domain-events/domain-events-in-ddd.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Eventual Consistency vs Strong (Transactional) Consistency

Eventual Consistency - это следствие, а не причина

A distinct, though related set of issues arises in distributed systems. The state of a distributed system cannot be kept completely consistent at all times. We keep the aggregates internally consistent at all times, while making other changes asynchronously. As changes propagate across nodes of a network, it can be difficult to resolve multiple updates arriving out of order or from distinct sources.

- "Domain-Driven Design Reference" 2 by Eric Evans, Chapter "Domain Events"

It is difficult to guarantee the consistency of changes to objects in a model with complex associations. Objects are supposed to maintain their own internal consistent state, but they can be blindsided by changes in other objects that are conceptually constituent parts. Cautious database locking schemes cause multiple users to interfere pointlessly with each other and can make a system unusable. Similar issues arise when distributing objects among multiple servers, or designing asynchronous transactions.

<...>

Use the same aggregate boundaries to govern transactions and distribution. Within an aggregate boundary, apply consistency rules synchronously. Across boundaries, handle updates asynchronously. Keep an aggregate together on one server. Allow different aggregates to be distributed among nodes.

- "Domain-Driven Design Reference" 2 by Eric Evans, Chapter "Aggregates"

Здесь мы видим, что краеугольной причиной Eventual Consistency является распределенное хранение данных. Это значит, что, в силу CAP-теоремы (перевод на Русский), становится невозможно достигнуть одновременно Consistency и Availability при Partition Tolerance. Это та самая причина, по которой концепция Агрегата лежит в основе практически любого распределенного NoSQL хранилища - агрегат просто хранится целиком на одном узле, поэтому, он всегда и доступен, и согласован одновременно.

Представьте на минутку, что узлы автомобиля хранятся на разных узлах, и они не успели прийти в согласованное состояние после обновления агрегата, в котором был заменен типоразмер шин. Тогда у нас возникла бы вероятность получить из хранилища автомобиль с различными типоразмерами шин, что нарушило бы инвариант агрегата.

Иными словами, Eventual Consistency является не причиной, а следствием. И сохраняется агрегат одной транзакцией потому, что иное просто технически невозможно в условиях распределенности. Точнее, Агрегат является границей транзакции. И Вернон прибегает к Eventual Consistency потому что это лучше для high availability, чем Two-Phase Commit.

Таким образом, используя распределенное NoSQL хранилище или Actor Model, как правило, просто нет технической возможности сохранить более одного агрегата в одной транзакции. Хотя, многие распределенные NoSQL хранилища и позволяют пакетировать несколько операций, транзакциями их считать нельзя.

Используя микросервисную архитектуру с RDBMS, существует техническая возможность сохранять более одного агрегата внутри одного и того же микросервиса одной транзакцией. Правда, это может ухудшить уровень параллелизма, поэтому важно стремиться достигать наименее возможных границ транзакции. А вот синхронизация агрегатов различных сервисов может быть только асинхронной, либо же с использованием Two-Phase Commit. То же самое справедливо и для Bounded Contexts DDD-монолита.

Стремление избежать Two-Phase Commit, в целях достижения highly scalable, подталкивает Vaughn Vernon к Eventual Consistency:

It can eliminate the need for two-phase commits (global transactions) and support of the rules of Aggregates (10). One rule of Aggregates states that only a single instance should be modified in a single transaction, and all other dependent changes must occur in separate transactions. So other Aggregate instances in the local Bounded Context may be synchronized using this approach. We also bring remote dependencies into a consistent state with latency. The decoupling helps provide a highly scalable and peak-performing set of cooperating services. It also allows us to achieve loose coupling between systems.

-"Implementing Domain-Driven Design" 3 by Vaughn Vernon, Chapter "Chapter 8. Domain Events :: The When and Why of Domain Events"

Но мы видим, что, кроме проблемы достижения одновременной Согласованности и Доступности при распределенном хранении агрегатов (и устойчивости к разделению), озвучивается еще одна причина - database locking. Означает ли проблема database locking то, что коммититься должен только один агрегат в одной транзакции при использовании RDBMS (Relational Database Management System)? Это означает только то, что транзакция должна быть fine-grained. "Fine-grained system transaction" != "one aggregate per transaction".

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 was not correctly designed.

- ".NET Microservices: Architecture for Containerized .NET Applications" 7 by Cesar de la Torre, Bill Wagner, Mike Rousos, Chapter "Domain events: design and implementation :: Single transaction across aggregates versus eventual consistency across aggregates"

О проблемах ухудшения параллелизма говорит и Vaughn Vernon, причем, причиной проблемы может стать даже один-единственный крупный агрегат. Как видно, дело не столько в количестве агрегатов, сколько в размере границ транзакции.

Smaller Aggregates not only perform and scale better, they are also biased toward transactional success, meaning that conflicts preventing a commit are rare.

- "Implementing Domain-Driven Design" 3 by Vaughn Vernon, Chapter "Chapter 10 Aggregates :: Rule: Design Small Aggregates"

Сам Eric Evans в своем известном выражении, которое многие приводят как первопричину Eventual Consistency, вовсе не требует одну транзакцию на агрегат, а говорит лишь о том, что после коммита инвариант каждого из агрегатов должен соблюдаться:

Invariants, which are consistency rules that must be maintained whenever data changes, will involve relationships between members of the AGGREGATE. Any rule that spans AGGREGATES will not be expected to be up-to-date at all times. Through event processing, batch processing, or other update mechanisms, other dependencies can be resolved within some specified time. But the invariants applied within an AGGREGATE will be enforced with the completion of each transaction.

- "Domain-Driven Design: Tackling Complexity in the Heart of Software" 1 by Eric Evans, Chapter "Six. The Life Cycle of a Domain Object :: Aggregates"

Leave transaction control to the client. Although the REPOSITORY will insert into and delete from the database, it will ordinarily not commit anything. It is tempting to commit after saving, for example, but the client presumably has the context to correctly initiate and commit units of work. Transaction management will be simpler if the REPOSITORY keeps its hands off.

- "Domain-Driven Design: Tackling Complexity in the Heart of Software" 1 by Eric Evans, Chapter "Six. The Life Cycle of a Domain Object :: Repositories"

А здесь он говорит о корне агрегата во множественном числе:

Schemes have been developed for defining ownership relationships in the model. The following simple but rigorous system, distilled from those concepts, includes a set of rules for implementing transactions that modify the objects and their owners.

- "Domain-Driven Design: Tackling Complexity in the Heart of Software" 1 by Eric Evans, Chapter "Six. The Life Cycle of a Domain Object :: Aggregates"

Такую же причину озвучивает и Vaughn Vernon:

Transactions across distributed systems are not atomic. The various systems bring multiple Aggregates into a consistent state eventually.

- "Implementing Domain-Driven Design" 3 by Vaughn Vernon, Chapter "10 Aggregates :: Rule: Reference Other Aggregates by Identity :: Scalability and Distribution"

Accepting that all Aggregate instances in a large-scale, high-traffic enterprise are never completely consistent helps us accept that eventual consistency also makes sense in the smaller scale where just a few instances are involved.

- "Implementing Domain-Driven Design" 3 by Vaughn Vernon, Chapter "10 Aggregates :: Rule: Use Eventual Consistency Outside the Boundary"

Кстати, автором идеи агрегата является даже не Eric Evans, а David Siegel.

Schemes have been developed for defining ownership relationships in the model. The following simple but rigorous system, distilled from those concepts, includes a set of rules for implementing transactions that modify the objects and their owners. [1] (David Siegel devised and used this system on projects in the 1990s but has not published it.)

First we need an abstraction for encapsulating references within the model. An AGGREGATE is a cluster of associated objects that we treat as a unit for the purpose of data changes. Each AGGREGATE has a root and a boundary. The boundary defines what is inside the AGGREGATE. The root is a single, specific ENTITY contained in the AGGREGATE. The root is the only member of the AGGREGATE that outside objects are allowed to hold references to, although objects within the boundary may hold references to each other. ENTITIES other than the root have local identity, but that identity needs to be distinguishable only within the AGGREGATE, because no outside object can ever see it out of the context of the root ENTITY.

- "Domain-Driven Design: Tackling Complexity in the Heart of Software" 1 by Eric Evans, Chapter "Six. The Life Cycle of a Domain Object :: Aggregates"

Оригинальная работа David Siegel к сожалению, не опубликована (по крайней мере, мне ее отыскать не удалось). Но он упоминается также в PoEAA, где определение агрегата звучит так:

Eric Evans and David Siegel [Evans] define an aggregate as a cluster of associated objects that we treat as a unit for data changes. Each aggregate has a root that provides the only access point to members of the set and a boundary that defines what's included in the set. The aggregate's characteristics call for a Coarse-Grained Lock, since working with any of its members requires locking all of them. Locking an aggregate yields an alternative to a shared lock that I call a root lock (see Figure 16.4). By definition locking the root locks all members of the aggregate. The root lock gives us a single point of contention.

- "Patterns of Enterprise Application Architecture" 10 by Martin Fowler, David Rice, Matthew Foemmel, Edward Hieatt, Robert Mee, Randy Stafford, Chapter "16. Offline Concurrency Patterns :: Coarse-Grained Lock"

Здесь говорится про единицу изменения, про бизнес-транзакцию и блокировку, но о связи бизнес-транзакции с системной транзакцией говорится только то, что "the system transaction in which you commit the business transaction", т.е. границы системной транзакции включают в себя границы бизнес-транзакции, но не ограничиваются ими.

Eventual Consistency предпочтительней

С одной стороны, Vaughn Vernon настоятельно рекомендует использовать Eventual Consistency между Агрегатами. И тут же объясняет - агрегаты в высоконагруженных масштабируемых распределенных приложениях, устойчивых к разделению, никогда не бывают доступны и согласованы между собой одновременно.

Thus, if executing a command on one Aggregate instance requires that additional business rules execute on one or more other Aggregates, use eventual consistency. Accepting that all Aggregate instances in a large-scale, high-traffic enterprise are never completely consistent helps us accept that eventual consistency also makes sense in the smaller scale where just a few instances are involved.

- "Implementing Domain-Driven Design" 3 by Vaughn Vernon, Chapter "10 Aggregates :: Rule: Use Eventual Consistency Outside the Boundary"

An invariant is a business rule that must always be consistent. There are different kinds of consistency. One is transactional consistency, which is considered immediate and atomic. There is also eventual consistency. When discussing invariants, we are referring to transactional consistency.

<...>

The consistency boundary logically asserts that everything inside adheres to a specific set of business invariant rules no matter what operations are performed. The consistency of everything outside this boundary is irrelevant to the Aggregate. Thus, Aggregate is synonymous with transactional consistency boundary.

<...>

When employing a typical persistence mechanism, we use a single transaction to manage consistency. When the transaction commits, everything inside one boundary must be consistent. A properly designed Aggregate is one that can be modified in any way required by the business with its invariants completely consistent within a single transaction. And a properly designed Bounded Context modifies only one Aggregate instance per transaction in all cases. What is more, we cannot correctly reason on Aggregate design without applying transactional analysis. Limiting modification to one Aggregate instance per transaction may sound overly strict. However, it is a rule of thumb and should be the goal in most cases. It addresses the very reason to use Aggregates.

- "Implementing Domain-Driven Design" 3 by Vaughn Vernon, Chapter "10 Aggregates :: Rule: Model True Invariants in Consistency Boundaries"

Все решают бизнес-правила

С другой стороны, все решают бизнес-правила:

The main point to remember from this section is that business rules are the drivers for determining what must be whole, complete, and consistent at the end of a single transaction.

- "Domain-Driven Design Distilled" 4 by Vaughn Vernon, Chapter "5. Tactical Design with Aggregates :: Why Used"

Принцип "Ask Whose Job It Is"

Тем не менее, Vaughn Vernon не считает вопрос Strong (Transactional) Consistency vs Eventual Consistency однозначным, и приводит четыре причины, по которым выбор может отдаваться в пользу Strong (Transactional) Consistency. Цитировать все не буду - слишком много текста. Кому интересно - глава "Chapter 10 Aggregates :: Rule: Use Eventual Consistency Outside the Boundary :: Ask Whose Job It Is" и далее, вплоть до главы "Gaining Insight through Discovery". Приведу только отрывок:

Ask Whose Job It Is

Some domain scenarios can make it very challenging to determine whether transactional or eventual consistency should be used. Those who use DDD in a classic/traditional way may lean toward transactional consistency. Those who use CQRS may tend toward eventual consistency. But which is correct? Frankly, neither of those tendencies provides a domain-specific answer, only a technical preference. Is there a better way to break the tie?

Discussing this with Eric Evans revealed a very simple and sound guideline. When examining the use case (or story), ask whether it's the job of the user executing the use case to make the data consistent. If it is, try to make it transactionally consistent, but only by adhering to the other rules of Aggregates. If it is another user's job, or the job of the system, allow it to be eventually consistent. That bit of wisdom not only provides a convenient tie breaker, but it helps us gain a deeper understanding of our domain. It exposes the real system invariants: the ones that must be kept transactionally consistent. That understanding is much more valuable than defaulting to a technical leaning.

- "Implementing Domain-Driven Design" 3 by Vaughn Vernon, Chapter "10 Aggregates :: Rule: Use Eventual Consistency Outside the Boundary :: Ask Whose Job It Is"

В цитате Вона Вернона видно, что Эрик Эванс не спешит разделять стремление к одному агрегату на транзакцию, и предлагает рассматривать каждый случай отдельно.

Можно заметить, что принцип "When examining the use case (or story), ask whether it's the job of the user executing the use case to make the data consistent. If it is, try to make it transactionally consistent, but only by adhering to the other rules of Aggregates." не противоречит приведенному ниже принципу "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."

Здесь же Vaughn Vernon напоминает нам, что во главе угла стоит, опять же, масштабирование и распределенность:

We'll have consistency where necessary [имеется ввиду CAP-theorem], and support for optimally performing and highly scalable systems.

- "Implementing Domain-Driven Design" 3 by Vaughn Vernon, Chapter "10 Aggregates :: Reasons to Break the Rules :: Adhering to the Rules"

Далее, в главе "Chapter 10 Aggregates :: Gaining Insight through Discovery :: Is It the Team Member's Job?" книги, он демонстрирует применение принципа "Ask Whose Job It Is" на практике.

Strong Consistency - новичкам

Вот что советует новичкам Vaughn Vernon:

There is nothing incredibly difficult about using eventual consistency. Still, until you can gain some experience, you may be concerned about using it. If so, you should still partition your model into Aggregates according to business-defined transactional boundaries. However, there is nothing preventing you from committing modifications to two or more Aggregates in a single atomic database transaction. You might choose to use this approach in cases that you know will succeed but use eventual consistency for all others. This will allow you to get used to the techniques without taking too big an initial step. Just understand that this is not the primary way that Aggregates are meant to be used, and you may experience transactional failures as a result.

- "Domain-Driven Design Distilled" 4 by Vaughn Vernon, Chapter "5. Tactical Design with Aggregates :: Rule 4: Update Other Aggregates Using Eventual Consistency"

Интересы performance

Ранее упоминалось, что одной из ключевых причин fine-grained транзакций является performance. Но всегда ли? На самом деле, все зависит от конкретных условий. Забегая наперед, рассмотрим такое утверждение:

NOTE: Try not to confuse this guideline with loading or creating aggregates. It is perfectly fine to load multiple aggregates inside the same transaction as long as you save only one of them. Equally, it is permissible to create multiple aggregates inside a single transaction because adding new aggregates should not cause concurrency issues.

- "Patterns, Principles, and Practices of Domain-Driven Design" 6 by Scott Millett, Nick Tune, Chapter "19 Aggregates :: Special Cases"

Какое значение имеет это утверждение для performance? Я обращусь к статьям двух известных организаций в области highload:

This consistent insert throughput also persists when writing large batches of rows in single operations to TimescaleDB (instead of row-by-row). Such batched inserts are common practice for databases employed in more high-scale production environments, e.g., when ingesting data from a distributed queue like Kafka. In such scenarios, a single Timescale server can ingest 130K rows (or 1.3M metrics) per second, approximately 15x that of vanilla PostgreSQL once the table has reached a couple 100M rows.

- "Time-series data: Why (and how) to use a relational database instead of NoSQL" by Mike Freedman, Timescale CTO and co-founder. Professor of Computer Science at Princeton.

  1. Insert rows in batches.

In order to achieve higher ingest rates, you should insert your data with many rows in each INSERT call (or else use some bulk insert command, like COPY or our parallel copy tool).

Don't insert your data row-by-row – instead try at least hundreds (or thousands) of rows per INSERT. This allows the database to spend less time on connection management, transaction overhead, SQL parsing, etc., and more time on data processing.

- "13 tips to improve PostgreSQL Insert performance" by Mike Freedman, Timescale CTO and co-founder. Professor of Computer Science at Princeton.

It is of note here that each insert is a transaction. What this means is Postgres is doing some extra coordination to make sure the transaction is completed before returning. On every single write this takes some overhead. Instead of single row transactions, if we wrap all of our inserts in a transaction like below, we'll see some nice performance gains:

begin;
insert 1;
insert 2;
insert 3;
...
commit;

This took my inserts down from 15 minutes 30 seconds to 5 minutes and 4 seconds. We've suddenly boosted our throughput by 3x to about 3k inserts per second.

<...>

By batching our inserts into a single transaction, we saw our throughput go higher. But hold on, there is even more we can do. The \copy mechanism gives a way to bulk load data in an even more performant manner.

<...>

Running this copy completes in 82 seconds! We're now processing over 10k writes per second on some fairly modest hardware.

- "Faster bulk loading in Postgres with copy" by Craig Kerstiens, CitusData

Вот что говорит по этому вопросу документация по PostgreSQL:

When using multiple INSERTs, turn off autocommit and just do one commit at the end. (In plain SQL, this means issuing BEGIN at the start and COMMIT at the end. Some client libraries might do this behind your back, in which case you need to make sure the library does it when you want it done.) If you allow each insertion to be committed separately, PostgreSQL is doing a lot of work for each row that is added.

- "PostgreSQL 11 Documentation :: 14.4. Populating a Database :: 14.4.1. Disable Autocommit"

Целесообразность использования Eventual Consistency в интересах performance нужно рассматривать в каждом конкретном случае отдельно. Универсального рецепта не существует. Этот вопрос особенно актуален при разработке сертифицированных приложений, где свобода выбора базы данных ограничена списком сертифицированных решений (зачастую вся свобода выбора сводится к RDBMS PostgresPro). Организовать пакетирование запросов можно на уровне Unit of Work.

В контексте этого вопроса можно еще раз вспомнить утверждение Eric Evans:

Discussing this with Eric Evans revealed a very simple and sound guideline. When examining the use case (or story), ask whether it's the job of the user executing the use case to make the data consistent. If it is, try to make it transactionally consistent, but only by adhering to the other rules of Aggregates.

- "Implementing Domain-Driven Design" 3 by Vaughn Vernon, Chapter "10 Aggregates :: Rule: Use Eventual Consistency Outside the Boundary :: Ask Whose Job It Is"

Обратная совместимость формата объектов событий

Другим достоинством Strong Consistency является отсутствие потребности в обеспечении обратной совместимости формата объектов событий, ведь их время жизни ограничено одной транзакцией. При использовании же шины сообщений всегда сохраняется вероятность того, что обновленная версия программного обеспечения, после ее развертывания, получит из шины устаревший формат сообщения, отправленный в шину еще предыдущей версией программного обеспечения. Кроме того, возникает потребность поддерживать оба формата сообщений для организации blue-green deployment.

Подробнее о версионировании сообщений смотрите в книге "Versioning in an Event Sourced System" by Greg Young ("читать online", "конспект книги"), а так же в главе "Event versioning книги "CQRS Journey".

Рекомендации от ".NET Microservices"

".NET Microservices: Architecture for Containerized .NET Applications" 7 явно разделяет внутренние Domain Events (для подписчиков внутри Bounded Context) и внешние Integration Events. Внутренние Domain Events рекомендуется использовать для синхронизации Агрегатов внутри Bounded Context.

Domain events as a preferred way to trigger side effects across multiple aggregates within the same domain

If executing a command related to one aggregate instance requires additional domain rules to be run on one or more additional aggregates, you should design and implement those side effects to be triggered by domain events. As shown in Figure 7-14, and as one of the most important use cases, a domain event should be used to propagate state changes across multiple aggregates within the same domain model.

- ".NET Microservices: Architecture for Containerized .NET Applications" 7 by Cesar de la Torre, Bill Wagner, Mike Rousos, Chapter "Domain events: design and implementation :: Domain events as a preferred way to trigger side effects across multiple aggregates within the same domain"

Причем, Strong Consistency является приемлемым для внутренних Domain Events, синхронизирующих Агрегаты внутри Bounded Context:

Be aware that transactional boundaries come into significant play here. If your unit of work and transaction can span more than one aggregate (as when using EF Core and a relational database), this can work well. But if the transaction cannot span aggregates, such as when you are using a NoSQL database like Azure CosmosDB, you have to implement additional steps to achieve consistency.

- ".NET Microservices: Architecture for Containerized .NET Applications" 7 by Cesar de la Torre, Bill Wagner, Mike Rousos, Chapter "Domain events: design and implementation :: Implement domain events :: The deferred approach to raise and dispatch events"

Оба подхода, и Strong Consistency, и Eventual Consistency, являются приемлемыми для синхронизации Агрегатов внутри Bounded Context:

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 are 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 is an issue and the event handlers cannot commit their side effects, you will 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 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 is easier to implement and valid in many business cases. It is also the approach used in the ordering microservice in eShopOnContainers.

- ".NET Microservices: Architecture for Containerized .NET Applications" 7 by Cesar de la Torre, Bill Wagner, Mike Rousos, Chapter "Domain events: design and implementation :: Implement domain events :: Single transaction across aggregates versus eventual consistency across aggregates"

Мнение Scott Millett и Nick Tune

Sometimes it is actually good practice to modify multiple aggregates within a transaction. But it's important to understand why the guidelines exist in the first place so that you can be aware of the consequences of ignoring them.

When the cost of eventual consistency is too high, it's acceptable to consider modifying two objects in the same transaction. Exceptional circumstances will usually be when the business tells you that the customer experience will be too unsatisfactory. You shouldn't just accept the business's decision, though; it never wants to accept eventual consistency. You should elaborate on the scalability, performance, and other costs involved when not using eventual consistency so that the business can make an informed, customer‐focused decision.

Another time it's acceptable to avoid eventual consistency is when the complexity is too great. You will see later in this chapter that robust eventually consistent implementations often utilize asynchronous, out‐of‐process workflows that add more complexity and dependencies.

To summarize, saving one aggregate per transaction is the default approach. But you should collaborate with the business, assess the technical complexity of each use case, and consciously ignore the guideline if there is a worthwhile advantage, such as a better user experience.

NOTE: Try not to confuse this guideline with loading or creating aggregates. It is perfectly fine to load multiple aggregates inside the same transaction as long as you save only one of them. Equally, it is permissible to create multiple aggregates inside a single transaction because adding new aggregates should not cause concurrency issues.

<...>

You should try to align your aggregate boundaries with transactions, because the higher the number of aggregates being modified in a single transaction, the greater the chance of a concurrency failure. Therefore, strive to modify a single aggregate per use case to keep the system performant.

<...>

If you find that you are modifying more than one aggregate in a transaction, it may be a sign that your aggregate boundaries can be better aligned with the problem domain.

<...>

In a typical business use case there are often multiple actions that need to succeed or fail together inside a transaction. By managing transactions in application services, you have full control over which operations that you request of the domain will live inside the same transaction boundary.

This can be demonstrated using an updated RecommendAFriendService. Imagine the business has decided that if the referral policy cannot be applied, it should not create the new account. Therefore, the transactional boundary encapsulates creating the new account and applying the referral policy to both accounts, as shown in Figure 25-3.

- "Patterns, Principles, and Practices of Domain-Driven Design" 6 by Scott Millett, Nick Tune, Chapter "19 Aggregates :: Special Cases"

Мнение Jimmy Bogard

Вот что говорит ".NET Microservices: Architecture for Containerized .NET Applications" со ссылкой на Jimmy Bogard:

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 raising the domain event [...] Just before we commit our transaction, we dispatch our events to their respective handlers.

- ".NET Microservices: Architecture for Containerized .NET Applications" 7 by Cesar de la Torre, Bill Wagner, Mike Rousos, Chapter "Domain events: design and implementation :: Single transaction across aggregates versus eventual consistency across aggregates"

Сам Jimmy Bogard говорит следующее:

Domain events are similar to messaging-style eventing, with one key difference. With true messaging and a service bus, a message is fired and handled to asynchronously. With domain events, the response is synchronous

- "Strengthening your domain: Domain Events" 18 by Jimmy Bogard

Transactions are handled in our unit of work wrapping each HTTP request. Since our domain events are synchronous and on the same thread, they are part of the same transaction as the entity that first raised the event.

- "Strengthening your domain: Domain Events", comment of Jimmy Bogard

With our domain event in place, we can ensure that our entire domain model stays consistent with the business rules applied, even when we need to notify other aggregate roots in our system when something happens. We've also locked down all the ways the risk status could change (charged a new fee), so we can keep our Customer aggregate consistent even in the face of changes in a separate aggregate (Fee).

This pattern isn't always applicable. If I need to do something like send an email, notify a web service or any other potentially blocking tasks, I should revert back to normal asynchronous messaging. But for synchronous messaging across disconnected aggregates, domain events are a great way to ensure aggregate root consistency across the entire model. The alternative would be transaction script design, where consistency is enforced not by the domain model but by some other (non-intuitive) layer.

- "Strengthening your domain: Domain Events" 18 by Jimmy Bogard

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 raising the domain event. If I cared enough to have the side effects occur, I would instead just couple myself directly to that other service as an argument to my domain's method.

Instead of dispatching to a domain event handler immediately, what if instead we recorded our domain events, and before committing our transaction, dispatch those domain events at that point? This will have a number of benefits, besides us not tearing our hair out. Instead of raising domain events, let's define a container for events on our domain object:

<...>

Just before we commit our transaction, we dispatch our events to their respective handlers.

- "A better domain events pattern" 19 by Jimmy Bogard

Мнение Kamil Grzybek

Вот что говорит Kamil Grzybek:

The way of handling of domain events depends indirectly on publishing method. If you use DomainEvents static class, you have to handle event immediately. In other two cases you control when events are published as well handlers execution – in or outside existing transaction.

In my opinion it is good approach to always handle domain events in existing transaction and treat aggregate method execution and handlers processing as atomic operation. This is good because if you have a lot of events and handlers you do not have to think about initializing connections, transactions and what should be treat in "all-or-nothing" way and what not.

- "How to publish and handle Domain Events" 15 by Kamil Grzybek

Thanks for question Andreas!

I know both books of Vaughn Vernon - they are great and must read for every DDD practitioner. From the DDD Distlled book (chapter 5 about aggregates):

...business rules are the drivers for determining what must be whole, complete, and consistent at the end of a single transaction.

So in general this is good rule to have separate transactions, but sometimes it is impossible or very hard to achieve.

My approach is similar to Vaughn Vernon - I try always handle event in separate transaction if it is possible. To do that I have two types of events: Domain Events (private, handled in the same transaction) and Domain Events Notifications (handled outside transaction). Domain Event Notification often becomes an Integration Event which is send to Events Bus to other Bounded Context. This way I support all cases - immediate consistency, eventual consistency and integrations scenarios.

- "How to publish and handle Domain Events" 15, comment of Kamil Grzybek

Aggregates can publish multiple Domain Events, and for each Domain Event there can be many handlers responsible for different behavior. This behavior can be communication with an external system or executing a Command on another Aggregate, which will again publish its events to which another part of our system will subscribe.

- "Handling Domain Events: Missing Part" 16 by Kamil Grzybek

Let's assume that in this particular case both Order placement and Payment creation should take place in the same transaction. If transaction is successful, we need to send 2 emails – about the Order and Payment.

<...>

  1. Command Handler defines transaction boundary. Transaction is started when Command Handler is invoked and committed at the end.

  2. Each Domain Event handler is invoked in context of the same transaction boundary.

  3. If we want to process something outside the transaction, we need to create a public event based on the Domain Event. I call it Domain Event Notification, some people call it a public event, but the concept is the same.

The second most important thing is when to publish and process Domain Events? Events may be created after each action on the Aggregate, so we must publish them:

  • after each Command handling (but BEFORE committing transaction)

  • after each Domain Event handling (but WITHOUT committing transaction)

<...>

The second thing we have to do is to save notifications about Domain Events that we want to process outside of the transaction.

- "Handling Domain Events: Missing Part" 16 by Kamil Grzybek

Обратите внимание, что, по приведенной им ссылке, под термином "public event" понимается сообщение, выходящее за пределы Bounded Context (к этому вопросу мы еще вернемся):

Set up separate messaging channels for inside the Bounded Context and outside. Keep all events private by default, and indicate the ones you want to make public with an explicit @Public annotation, a marker interface, or an isPublic():bool method. When emitting events, the event publishing mechanism knows to read the annotation and either send the event on the private channel only, or on both the private and the public channel.

—"Patterns for Decoupling in Distributed Systems: Explicit Public Events" by Mathias Verraes

И, в своем демонстрационном приложении sample-dotnet-core-cqrs-api, он демонстрирует обработку Domain Event в одной транзакции с агрегатом.

Мнение Udi Dahan

> This might be a bit of a late question. But shouldn't domain events be handled after the transaction ends? Is there any specific reason for handle domain events within the same transaction scoping DoSomething?

Domain events get handled by service layer objects in the same process which usually send out other messages – as such, we want those messages to be sent (or not) in the same transactional context.

- "Domain Events – Salvation" 22 comment of Udi Dahan

> In message number 120 above, Lars asks about how to access the data if the event is fired before the commit. I didn't understand your response. Maybe my situation is different so I'll explain.

> I have 2 BCs. One context deals with the merging of employee information. I'd like to fire a domain event specifying that the employee was merged. I'd like the 2nd BC to react to this event. The issue is that the data won't be committed at that point, and this data that changed is vital to the 2nd BC to react.

> Am I going down the wrong path by attempting to use domain events? Is there another solution you could suggest?

The question is whether you need both your BCs to be consistent with each other at *all* times – ergo in the same transaction.

If the answer is yes, then you absolutely do want the event to be raised and handled in the same transaction – you'd also be deploying both BCs together.

If the answer is no, then you should use some kind of message bus between the BCs. The handler for the domain event would publish a message using the bus, and that would be enlisted in the same transaction – thus is the first BC rolled back, the message wouldn't be sent. The second BC would be invoked by the bus when the message arrives at its queue where its handling would then be done in a separate transaction.

- "Domain Events – Salvation" 22 comment of Udi Dahan

> Shouldn't the event only be handled when the transaction commits? Until the transaction commits, the change to the domain object isn't really permanent, right?

Not necessarily – sometimes you want loose-coupling within the same transaction.

I do agree that often where we find a place ready for logical decoupling it coincides with separate transaction boundaries. In those cases, using a transactionally-aware technology like NServiceBus will be a better choice for publishing events.

- "Domain Events – Salvation" 22 comment of Udi Dahan

> Domain event could alter multiple aggregates which is common, wouldn't you be updating multiple aggregates in a single transaction?

The more common case is where those multiple aggregates are updated in separate transactions, usually as a result of some kind of "service bus" event being transmitted from the domain events. That service bus event gets routed to multiple subscribers, behind which you'd have each of the respective aggregates that would updated in their own transactions.

- "Domain Events – Salvation" 22 comment of Udi Dahan

Мнение Cesar De la Torre

When handling the event, any event handler subscribed to the event could run additional domain operations by using other AggregateRoot objects, but again, you still need to be within the same transaction scope.

<..>

for in-memory event based communication across disconnected aggregates that are part of the same domain model and part of the same transaction, domain events are great ensuring consistency across a single domain model within the same microservice or Bounded-Context.

- "Domain Events vs. Integration Events in Domain-Driven Design and microservices architectures" 23 by Cesar De la Torre, Principal Program Manager, .NET

Ссылки по теме:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK