1

I want to understand the consequences of allowing handlers to be direct or indirect source of new domain events.

In all examples I see in the literature Domain Events are usually published by Aggregate Roots. Actually, events may be just returned and then later be published before or/and after transaction is committed.

Let's consider the flow:

AR returns event -> event is published -> handler modifies another AR which belongs to same Bounded Context -> modified AR produces another event -> event is published/persisted -> ... -> commit (all or nothing).

Event dispatching and processing continues until no new events occur. (It somewhat reminds me database triggers behaviour; and triggers are hated for cascades of state changes, but to be honest, I find eventual consistency with all these event and message flows to be not that easier to reason about.)

Ok, you may say:
"Changing multiple AR's in same transaction is an indication of broken AR design as its main task is to be a transaction boundary."

I may agree and suggest another scenario:

Imagine two requirements:

  • Client address must be unique.
  • (recently introduced req.) Some addresses (not only client's) must be automatically substituted with another address.

We have multiple ARs and thus multiple workflows where address is set. Address is modelled as a value object, it is validated and created by Address IAddressService.New(string address).

The logic of substitution partially depends on AR's state and some external state.

Option1.
Create a set of address services each working with particular AR type and returning coerced address. Unfortunately, some ARs require an Address to be provided as a process of their creation. It means service has to work with 'raw' data. This would be a possible solution, unless we needed to create an event about address substitution and reference AR in this event. It is impossible, if AR's Id is generated by the store.

Option 2.
Adopt eventual consistency. Save AR as is, run address substitution after commit, maybe fail if unique constraint is broken.
Personally, I have no idea how to notify downstream Bounded Context about the failure when this BC belongs to third party - they just make a call to HTTP endpoint and expect either success or failure.

Option 3.
Let handler change address in AR. The whole flow is:

AR is created -> event is handled by the handler which checks for uniqueness of address, then same event is handled by new handler, and it changes AR's address+emits new event AddressSubstituted about performed substitution -> AR emits a regular AddressChanged event -> check for uniqueness is done once again, but no need for substitution this time -> no new events are emitted -> commit -> Id is assigned to AR -> publish/persist AddressSubstituted event (this event has a reference to AR, thus when publishing/persisting, Id is already has a valid value).

Note that all critical parts run inside same transaction. Handler produces one event itself AddressSubstituted, and leads to another event indirectly - AddressChanged.

AddressSubstituted is published outside transaction, however it is still possible to save everything within same transaction. EF core is quite a clever beast. In case of configured relationships it will figure out the order of persistence, and will first save AR, then use its Id for saving. EF can also generate Guid automatically when AR is added to DbContext (but still after AR is created).

It's worth mentioning that order of execution of event handlers may or may not be important, but it is a separate subject for discussion.

Pros of this solution: - Old code is almost not touched, just develop new handlers. - Atomicity. Caller is immediately notified about the status of operation. - Intent of the change is captured.

Cons: - Extremely hard to reason about the flow (as with any event driven solution, I assume.) - Event generation may never end because of some weird nonbreaking loop created by handler. - <your items here>

I find this approach to be legit, but I may be missing something very important here. As I never met this - let's call it - exhaustive event dispatching before, I am in doubt.

3

DDD is a design methodology and has nothing to say about transaction management outside the domain. In DDD terms, the "transactional boundary" is meant to mean that an Entity is the sole, authoritative, and unambiguous representation of its state. This simply means that an Entity "owns" its data, and really has nothing to do with database transactions.

The reason many argue for only changing one Aggregate per database transaction has to do with scaling. Of course, like any other design decision, this comes with trade-offs. Modifying multiple Aggregates in a single database transaction means you don't have to deal with eventual consistency at the cost of not being able to shard your system. Whether or not that benefit is worth the cost is a function of your system's requirements, and should be considered carefully by your stakeholders. This does not have to be an "all or nothing" decision.

As far as Domain Events are concerned, again, they represent a trade-off. One of the main objectives of DDD is to "make the implicit explicit". Domain Events work against this idea by introducing implicit coupling. This can result in quite a bit of added complexity as well as to reduce the cohesiveness of a system. Put another way, I often see Domain Events introduced into a system to make up for poor design. That is, instead of taking the care to model the domain according to behavior and vectors of change, a complex web of Domain Events are rigged together ad-hoc in order to meet business requirements. As you astutely point out, the entire purpose of modelling Aggregates is to bring together data that changes together. If this isn't happening, we can look at another principal of DDD to help guide us: continuous refactoring.

Domain Events are best-used to trigger orthogonal (async) processes like sending an email when a CustomerRegistered event is raised, NOT simply keeping state in sync. In this way, they are not like database triggers. I wouldn't go so far as to say there is no situation where Domain Events should be used to keep state in sync, but it's important to understand the trade-offs and to always be on the lookout for an alternative approach.

As for your example problem. It's a bit too vague and convoluted to address (I had to). It reads like a problem contrived backwards given a faulty premise. Of course, starting with a poor design or not changing your design to accommodate "recently introduced requirements" will create problems down the road. You mention a number of Services/Aggregates in your explanation that only seem to create the problem you are outlining. Why design a system that has a problem? Start with the requirements. THEN model the system around them.

Systems are not designed to meet future requirements. They are designed to meet the current requirements and, hopefully, are designed according to principals that make them easy to change so that when new requirements crop up, they can be quickly integrated into the system. This is the entire purpose of DDD!

  • Now I use 'precommit' domain events extensively for verifying refrerential constraints. When one AR references another one, I can check and generate user friendly error with information what and where is missing. I think this kind of checks do not belong to AR's. – Pavel Voronin Nov 13 '18 at 7:35
  • @PavelVoronin Could further describe what you mean by "verifying referential contraints". I tend to understand that phrase in more in the context of database development than DDD. An aggregate having some sort of dependency on another aggregate is exactly the kind situation DDD seeks to avoid through careful modelling. The premise being that nothing within the boundaries of an aggregate (that requires validation) can be changed in any way other than through the aggregate itself. The fosters one-way data flow which can greatly simplify a system. – king-side-slide Nov 13 '18 at 15:36
  • It actually is. Database concerns, yes. But I see it ok, if one AR references another one with just a key. Consider AR which models a concrete device with S/N. But device has a model, which is in turn an AR. – Pavel Voronin Nov 13 '18 at 19:00
  • 1
    @PavelVoronin It is impossible to prevent a developer from writing bad code. Organization.PlaceOrder -> OrderPlaced is about as good as you can get. The basic idea is that nothing that can be done by calling new Order from your service layer. Simply "newing" up an object doesn't matter as long as nothing can be done with it. Which brings me back to my first statement. It should be clear to anyone working in your code base that publishing events from your service layer is wrong. – king-side-slide Nov 15 '18 at 19:11
  • 1
    @keelerjr12 It warms my heart knowing I can help people. I hold my blessings in gratitude. Thank you! I presented the Inventory-StockItem as a solution for the pricing invariant. In it's absence it may make sense to just return Product! Of course it can still be useful to explicitly model the difference between an actual item in a warehouse/backroom (maybe has a SKU) from the general concept of that item. That is, an Inventory context cares about managing the actual physical products, where an Order context just cares about the classification of a product (lamp vs that lamp). – king-side-slide Nov 28 '18 at 17:02
1

You touch on a number of different problems.

  1. What is a Domain Event as opposed to an Event?

  2. Should I commit before or after raising Domain Events?

  3. Should events trigger other events?

Lets take the easy one first. 3. Obviously the nature of events is such that one may trigger another, but also obviously this can lead to endless loops and difficult to track behaviour.

We need to be aware of all the events in a system when we change event handlers so we can avoid problems.

2 is a problem with no solution. Again the nature of an event is that anything can potentially be triggered by it. Those effects can easily be outside the scope of a database transaction. eg sending an email.

So If I want to potentially rewrite history by rolling back a transaction I have to either save the event publishing untill after the commit..

OR! restrict event listeners to things which will also be rolled back by the transaction.

This neatly brings us to 1. Unfortuantely the definiton of Domain Event seems pretty vague. People use it to mean inter AR - non transactional communication, intra AR communication as well as a stepping stone to Event Sourcing and all sorts of stuff.

Clearly it would be nice to have a definition such that you could say "All Domain Events should be published after the AR commits!" or "Domain Events MUST NOT trigger other events!!" or vice-versa.

Lets restrict Domain Events to the following rules.

  • Must only be emmited by ARs
  • Must only be listened to by other Domains
  • Must be the only type of thing Domains listen for.

Then we can start to make these kind of calls.

A. We can safely defer publication untill after transactions have been commited.

B. We have to use 'normal' events for doing stuff within the transaction.

C. Normal events can trigger other normal events and be safely within the AR / Transaction

D. Domain Events can trigger other Domain Events but the effects are allways outside the transaction.

This neatly solves your address substitution problem in that the AddressChanged event is a Domain Event and happens only when you commit the changed address

The address substitution must happen within the AR prior to commit, so you can cascade normal intra AR events from it, but any Domain Events it triggers block the cascade untill after commit, giving you a limited scope to think about.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.