The conventional implementation of "domain events" in DDD as formulated in lots of articles (such as this one, this one, and this one), seems as though it's at odds with DDD. Because the very thing it apparently accomplishes is to enable making cross-aggregate changes synchronously and then persisting them as part of the same database transaction.
But is this sound, according to DDD principles, given that an aggregate is supposed to be a transaction boundary?
For example, to quote the first article I linked to:
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.
(emphasis mine)
Corect me if I'm wrong, but it seems like the notion of "synchronous messaging across disconnected aggregates", and by extension persisting changes to them in a single transaction, is just in opposition to the idea of an aggregate being a consistency boundary, which is fundamental to DDD, as far I understand. Yet, it looks like this variation of the "domain events" idea is pretty popular in DDD-adjacent spaces; and nobody seems to have noticed this apparent contradiction — or at least I couldn't find anything on the web that explicitly acknowledged it.
What am I missing here? Is this actually a sound thing to do, or are people misguided?
-
@VoiceOfUnreason Would love your feedback on this one. Thank you in advance.Arad– Arad08/12/2024 16:36:02Commented Aug 12, 2024 at 16:36
-
Within an aggregate, everything has to be consistent at all times. This does not mean it’s problematic to also have some consistency with other aggregates at the same time.Rik D– Rik D08/12/2024 17:54:28Commented Aug 12, 2024 at 17:54
-
Do domain events achieve database transactional consistency or domain transaction consistency? Because those two are not the same thing, and that might be part of the confusion.user1937198– user193719808/14/2024 02:24:19Commented Aug 14, 2024 at 2:24
3 Answers 3
Domain Events, by themselves, have nothing to do with transactional boundaries. You are free to choose whether you process them synchronously or asynchronously.
DDD, in its original form, did not explicitly choose one over the other. However, nowadays, the default behavior is to consume events in the background to achieve eventual consistency.
Every rule in DDD has exceptions. When you get sufficiently entrenched and comfortable in DDD, you will know when to make exceptions to give back that extra advantage to the business.
CONS
Synchronous event processing has the following side effects:
- You have introduced potential bottlenecks, which can impact performance under heavy load conditions. This is the price of moving from a problem of eventual consistency to a problem of perpetual domain integrity.
- You are adding potential blocking operations to the overall processing time of a request. You have introduced coupling between different parts of the system. Multiple aggregates and their behavior are tightly coupled, making the system harder to change and evolve. Redrawing context boundaries in the future will be difficult, essentially simulating a monolith.
- You have restricted the "distributability" of processing. You are giving up on some of the benefits of asynchronous processing, such as better I/O, task independence, and resource utilization.
PROS
- The application's overall design is simpler and follows a predictable path.
- You will side-step the need to compensate for transaction failures and domain rules violations.
- The domain always maintains its integrity. It is easier to code invariants because the flow path is straightforward.
- API responses and the overall interaction with the UI are much simpler and more accurate.
In most complex domains, you can argue for synchronous processing for some events while async for the rest, and vice versa. For example, it does not make sense to add the task of refreshing read models based on events to the primary transaction itself (in most cases, read models are stored in a different kind of storage anyway). On the contrary, there can be use cases where generating compensating transactions is difficult. Handling those parts synchronously may be the best way to go.
I would say you should go the synchronous path under specific conditions:
- Integrity is paramount, like finance.
- Complexity is high, making adjusting/compensating transactions challenging to build.
- Write and read parts of the domain must be consistent 100% of the time.
I believe it is beneficial for most DDD applications to process events asynchronously. Only based on need should one revert to processing events synchronously.
-
How does this answer the question "Don't domain events violate "aggregate = transactional boundary"?"Rik D– Rik D08/13/2024 10:43:36Commented Aug 13, 2024 at 10:43
-
The title does mention domain events, but the essence of the question is "synchronous messaging across disconnected aggregates" and "persisting changes to them in a single transaction", which is what I was trying to address. I have now tweaked the answer to make this explicit.Subhash– Subhash08/15/2024 19:01:11Commented Aug 15, 2024 at 19:01
But for synchronous messaging across disconnected aggregates, domain events are a great way to ensure aggregate root consistency across the entire model.
[..] given that an aggregate is supposed to be a transaction boundary?
Very much take note of the mention of "aggregate root".
This is a point of confusion that has plagued DDD guidelines for some time, in my opinion. In its simplest terms, DDD deals with independent aggregates and that's that. But when things get more complex, to the point where an aggregate needs to be subdivided into separate (nested) classes, the nomenclature shifts towards referring to the top class of the nested structure ("aggregate root") differently from any of the lower nested classes, which then get referred to as "subaggregates" but people do sometimes also just call them "aggregates".
This is a naming game. I'm a stickler for using the names "aggregate root" and "subaggregate" specifically to avoid ambiguity. However, others think of it more like all classes within that bounded context being aggregates, with one of them being the aggregate root (as a special additional role).
The short answer here is that when aggregate roots are being mentioned, you're dealing with a bounded context that contains multiple aggregates.
As part of the quote you references, "aggregate root consistency" doesn't actually go outside of the bounded context, it's referring to an interaction between aggregates of the same bounded context.
-
I understand that the "aggregate root consistency across the model" means it doesn't go beyond the bounded context, but which one is actually the transactional boundary according to DDD, the aggregate, or the bounded context that encompasses it? In other words, would it be sound to transactionally update one or more aggregates (albiet within the same bounded context)?Arad– Arad08/13/2024 05:24:23Commented Aug 13, 2024 at 5:24
-
1@Arad: Your question is built on the supposition that "aggregate" has only one meaning. Annoyingly, it does not. When there is mention of aggregate roots, the bounded context fits that of the root (i.e. the root plus anything nested under it). When there is no explicit mention of aggregate roots, generally this means that "the aggregate" is really just referring to the whole boundary, i.e. as if it were an aggregate root without anything nested under it (functionally equivalent, but in practice it tends not to get referred to as a "root" then).Flater– Flater08/13/2024 05:26:46Commented Aug 13, 2024 at 5:26
-
1@Flater I disagree. In the context of DDD, the aggregate and bounded context concepts are clearly defined and only mean one thing. The aggregate is described in the literature as a consistency boundary; everything inside the aggregate has to be consistent at all times. Now, some bloggers or whatnot may have misinterpreted this and spread some false information about what aggregates actually are, but we can always look up the original definitions as described by Eric Evans in the blue book. Note that in other contexts (outside DDD) the term aggregate can have different meanings.Rik D– Rik D08/13/2024 10:22:19Commented Aug 13, 2024 at 10:22
-
1@Flater "when aggregate roots are being mentioned, you're dealing with a bounded context that contains multiple aggregates", also disagree: every aggregate has a root, if an aggregate is a single class, that class is the aggregate root.Rik D– Rik D08/13/2024 10:28:33Commented Aug 13, 2024 at 10:28
-
1What it comes down to, imho, is that the quote doesn't make sense. Change it to "But for synchronous messaging across disconnected aggregates, domain events are a great way to ensure consistency across the entire model (within the bounded context)." and it suddenly makes more sense and avoids all the confusion.Rik D– Rik D08/13/2024 10:39:07Commented Aug 13, 2024 at 10:39
According to foundational definition of Evans, aggregates should be boundaries for data consistency:
A cluster of associated objects that are treated as a unit for the purpose of data changes.
The "unit" can be dealt with in different manners, but with a transactional databas, eaccording to this definition, all the changes that happen in an aggregate should be in the same transaction.
It is however not forbidden according to this definition to group several aggregate changes in a single transaction. This could for example make sense when a DDD_Service is used, i.e. a standalone operation that does not really belong to either of the involved aggregates. The challenge is then to know in which case the transaction shall be done at aggregate level and in which case at the service level.
The drawback of such grouping is that an aggregate root may perform an operation that is valid but needs afterwards to be reverted to a previous stage due to a failure of an operation on another aggregate. This is not always desirable, nor even possible, and moreover, it is then difficult upon rollback to revert to a consistent object model in memory. Fortunately the unit of work pattern can help.
When domain events come into play, you could also think to adopt such a strategy. But it might be quickly very difficult to manage cross aggregate transactions, because these create some coupling. Domain events are here to allow scaling of complexity by decoupling the sender and the subscriber. Imho, nightmare guaranteed if you stick to the grouped transaction with domain event, unless your domain is very simple.
The recommendation would be to avoid synchronicity of changes across aggregates when domain events are involved. Prefer to opt for the saga pattern: all involved aggregates are responsible of their local transaction, but if one of them in the chain fails, "compensating" (reverse) operations are to be performed for all those already committed.