0

I am confused about where to handle domain events in an application that is based on the hexagonal architecture. I am talking about the bounded-context-internal domain events, and not about inter-context integration/application/public events.

Background

As far as I understand, application logic (i.e. use case logic, workflow logic, interaction with infrastructure etc.) is where command handlers belong, because they are specific to a certain application design and/or UI design. Command handlers then call into the domain layer, where all the domain logic resides (domain services, aggregates, domain events). The domain layer should be independent of specific application workflows and/or UI design.

In many resources (blogs, books) I find that people implement domain event handlers in the application layer, similar to command handlers. This is because the handling of a domain event should be done in its own transaction. And since it could influence other aggregates, these aggregates must be loaded via infrastructure first. The key point however, is this: The domain event is torn apart and turned into a series of method calls to aggregates. This important translation resides in the application layer only.

Question

I consider the knowledge about what domain events cause what effects on other aggregates as an integral part of the domain knowledge itself. If I were to delete everything except my domain layer, shouldn't that knowledge be retained somewhere? In my view, we should place domain event handlers directly in the domain layer itself:

  • They could be domain services which receive both a domain event and an aggregate that might be affected by it, and transform the domain event into one or many method calls.

  • They could be methods on aggregates themselves which directly consume the entire domain event (i.e. the signature contains the domain event type) and do whatever they want with it.

Of course, in order to load the affected aggregate, we still need a corresponding handler in the application layer. This handler only starts a new transaction, loads the interested aggregate and calls into the domain layer.

Since I have never seen this mentioned anywhere, I wonder if I got something wrong about DDD, domain events or the difference between application layer and domain layer.

EDIT: Examples

Let's start with this commonly used approach:

// in application layer service (called by adapter)
public void HandleDomainEvent(OrderCreatedDomainEvent event) {
    var restaurant = this.restaurantRepository.getByOrderKind(event.kind);
    restaurant.prepareMeal(); // Translate the event into a (very different) command - I consider this important business knowledge that now is only in the application layer.
    this.mailService.notifyStakeholders();
}

How about this one instead?

// in application layer service (called by adapter)
public void HandleDomainEvent(OrderCreatedDomainEvent event) {
    var restaurant = this.restaurantRepository.getByOrderKind(event.kind);
    this.restaurantDomainService.HandleDomainEvent(event, restaurant);
    this.mailService.notifyStakeholders();
}

// in domain layer handler (called by above)
public void HandleDomainEvent(OrderCreatedDomainEvent event, Restaurant restaurant) {
    restaurant.prepareMeal(); // Now this translation knowledge (call it policy) is preserved in only the domain layer.
}
domin
  • 753
  • 1
  • 4
  • 12

3 Answers3

1

Your description sounds very much like event-sourcing.

If event-sourcing (the state of an aggregate is solely derived from the domain events), then the event handler is in the domain layer, and in fact the general tendency would be to have a port/adapter/anti-corruption-layer emit commands; the command-handler for an aggregate then (if necessary) uses the event handler to derive the state of the aggregate, then based on the state and the command emits events which are persisted so that the event handler can derive the next state. Note that here, the event handler definitely belongs in the domain layer and the command handler likely does, too.

More general event driven approaches, to my mind, tend to implicitly utilize the fact that one side's event is very often another side's command.

It's worth noting that an event is in some sense often just a reified method call on an aggregate.

Levi Ramsey
  • 7,898
  • 11
  • 18
  • Thanks for you answer. I regard event sourcing as entirely different to domain events. In event sourcing, your events are technical/mechanical/internal, they just have to say what changed from state X to the next state Y. Domain events on the other hand are much more descriptive and also selective. The entirety of domain events doesn't give you anything near the current state of an aggregate. The translation from domain event to command ist exactly what I consider part of domain logic, and not application logic. – domin Apr 19 '21 at 18:18
1

The problem with most even handler classes is that they often are tied to a specific messaging technology and therefore often placed in the infrastructure layer.

However, nothing prevents you to write technology-agnostic handlers and use technology-aware adapters that dispatches to them.

For instance, in one application I've built I had the concept of an Action Required Policy. The policy drove the assignment/un-assignment of a given Work Item to a special workload bucket whenever the policy rule was satisfied/unsatisfied. The policy had to be re-evaluated in many scenarios such as when documents were attached to the Work Item, when the Work Item was assigned, when an external status flag was granted, etc.

I ended up creating an ActionRequiredPolicy class in the domain which had event handling methods such as void when(CaseAssigned event) and I had an even handler in the infrastructure layer that simply informed the policy.

I think another reason people put these in the infrastructure or application layers is that often the policies react to events by triggering new commands. Sometimes that approach feels natural, but some other times you want want to make it explicit that an action must occur in response to an event and otherwise can't happen: translating events to commands makes it less explicit.

Here's an older question I asked related to that.

plalx
  • 39,329
  • 5
  • 63
  • 83
  • I would argue for the benefit of explicit domain event handlers merely because of the fact that some commands **DO** occur in response to an event, whether or not they could also occur in other use cases. – domin Apr 21 '21 at 18:16
  • I am also a bit confused about the terminology used in event storming. E.g., the sticky notes representing commands probably aren't the application layer commands, but rather simple public methods on the aggregates themselves. So when translating an event into one or more commands (in event storming), this could be implemented simply as handling a domain event in the application/domain layer and then calling a method on an aggregate. – domin Apr 21 '21 at 18:20
1

I follow this strategy for managing domain events:

First of all, it is good to persist them in an event store, so that you have consistence between the fact that triggered the event (for example, a user was created) and the actions it triggers (for example, send an email to the user).

Assuming we have a command bus:

  • I put a decorator around it that persists the events generated by the command.
  • A worker processes the event store and publish the events outside the bounded context (BC).
  • Other BCs (or the same that published it) interested in the event, subscribe to it. The event handers are like command handlers, they belong to the application layer.

If you use hexagonal architecture, the hexagon is splitted into application layer and domain.

choquero70
  • 3,332
  • 2
  • 24
  • 40
  • Thanks for sharing your approach. I agree that this is a valid strategy, however, my question targets a more specific part: How do the event handlers in your application layer look like? Do they directly translate the event into aggregate commands/methods, or is the event passed into the domain layer? My proposal was to pass the event all the way from the adapter layer (hexagonal) through the application layer (which starts transactions and loads aggregates) to a designated domain event handler which finally handles the event. – domin Apr 23 '21 at 06:59
  • 1
    Event handler looks like a command handler. It takes a DTO (the publisher converted the domain event into a DTO) and performs whatever action in response to the event. The action may involve calling the domain, or do something implemented by the infra (for example, send an email). If you have to do several things, use a handler for each one. – choquero70 Apr 23 '21 at 07:49
  • What I am talking about is introducing a domain event handler in the **domain layer itself** (`public void HandleDomainEvent(DomainEventXY event)`). The app layer also has a corresponding handler that uses the DTO event class instead. It does all the infrastructure/workflow stuff (sending mails, loading data etc.) but – most crucially – calls into the aforementioned `HandleDomainEvent` in the domain layer (it has to transform the DTO into the original class before, although I would consider the original domain event as a POCO anyway). Now we have preserved everything important in the domain. – domin Apr 23 '21 at 08:20
  • "introducing a domain event handler in the domain layer itself"... I woudn't do that... I quote here Vaugh Vernon answer when I asked him about this some years ago... "In the case of Bounded Context A publishing Domain Events and Bounded Context B consuming those events, Context B would typically translate the events at a Port Adapter to something that the internal model B understands. It is a tool for deserializing events in a type-safe way" – choquero70 Apr 23 '21 at 09:31
  • We are talking different things here. I am only talking about **Bounded Context internal domain events**. I explicitly stated this in my second sentence in the question. The integration events need to go through an anti-corruption-layer, no question. However, there are still options to either translate them into calls to command-ports or event-ports internally. – domin Apr 23 '21 at 09:37
  • I understand what you say. But there isn't 2 types of events (integration and domain). There just one type: domain events, that are serialized into messages (dtos), and then the subscriber BC reacts to it, but there's no domain event handler inside the domain. It's not me who says it, it's Vaughn Vernon. – choquero70 Apr 23 '21 at 10:19
  • Okay, but its not like Vaughn Vernons opinion is the ground-truth and only way to do it. ;) There is plenty of literature online that says otherwise. Also, in his red book, Vernon has examples where he registers event handlers locally in the command handlers, which frankly is the worst idea IMO. – domin Apr 23 '21 at 10:22
  • I am interested in concrete arguments against (or even for) my proposal. Arguments by authority or by popular belief don't count for me. But I nevertheless appreciate that you shared your opinion, so thanks for that! – domin Apr 23 '21 at 11:18
  • Then if you don't mind about authority, me, John Doe :) will argument against your proposal: an event handler might have to orchestrate some domain objects, like a command hander, or do some infraestrure thing independent from the domain. So it should belong to a higher level layer, client of the domain. This higher level is the application layer. – choquero70 Apr 23 '21 at 13:24
  • It does! See my original post. And then it **also** appears directly one layer below — domain layer — again (either as a domain service or as an aggregate method), taking the very same domain event as input and doing only domain logic in response. That way, I handle the event explicitly in both layers, doing things only applicable in the respective layer. That is: workflow and infrastructure logic like sending mails or loading aggregates in app layer and applying business policies in the domain layer. – domin Apr 24 '21 at 05:42
  • I added an example in the original post which hopefully clarifies my point. – domin Apr 24 '21 at 06:01
  • First of all, prepare meal and email sending are 2 different things you have to do as a reaction to the domain event, so you should 2 handlers (Single Responsability Principle). Regardless of this, the knowledge of what to do when a domain events ocur belongs to app layer, because might imply many things (or a thing orchestrating many domain objects that is not domain knowledge but app layer knowledge) – choquero70 Apr 24 '21 at 06:30
  • Besides my last comment, your domain event handler receiving 2 args isn't correct. What if you call it with a restaurant as 2nd arg which is not the restaurant of the event? The restaurant that has to preparre the meal is the referenced in the event – choquero70 Apr 24 '21 at 06:40
  • Imagine that as a reaction to your domain event, several restaurants could be implied because each serves a part of the order, or you have to search the one nearest to the client or whatever, it is business logic belonging to the app layer, like a use case, the reaction to a domain event is not domain knowledge. What you are doing is just puttting apart a step of the reaction (a flow belonging to the app layer), which is the calling to prepare meal, but you don't know where that step fits in the whole reaction. – choquero70 Apr 24 '21 at 07:01
  • I think you have a valid point! However, the issue is coming from the decision that domain services cannot load aggregates by themselves. If they were allowed to talk to repositories directly (which is a valid alternative design also mentioned by Vernon in his book, but I don’t like that much), then you can handle the entire flow inside one domain event handler. But if not, then you are forced to play a game of ping pong between layers, which I agree is not helpful at all. I guess when deciding to keep repositories out of the domain, you have to handle events entirely in the application layer. – domin Apr 24 '21 at 07:21
  • But isn‘t this issue not also present for any other type of domain service that acts on multiple aggregates? Deciding which aggregates to load and operate on would then always be part of the app layer even though one can consider this important business logic. – domin Apr 24 '21 at 07:28
  • Calling repositories to load/save aggregates isn't domain logic, so it shouldn't be part of a domain service. The point of the 2 args on your domain handler is important too. – choquero70 Apr 24 '21 at 09:03
  • Not necessarily. Vernon for example makes a distinction between collection-based repos, and persistence-based repos (the ones we are talking about here). The former is allowed to be used in domain services, as they are treated like pure in-memory collections. Regarding the 2 arguments: You have this issue for any domain service, this has little to do with event handlers. In my example, if you want to make sure the caller is using the service correctly, simply do a pre-condition check in the method. A good read: https://medium.com/swlh/the-domain-driven-designs-missing-pattern-319bf16dad91 – domin Apr 24 '21 at 09:12
  • "You have this issue for any domain service"... I don't agree. For example, in a bank domain, a domain service would transfer an amount from a bank account to another... transfer ( Account from, Account to, Money amount ) – choquero70 Apr 24 '21 at 09:28
  • " The former is allowed to be used in domain services, as they are treated like pure in-memory collections"... so you couldn't say as an universal truth that domain event handlers could be domain services. You should say it just in case your repositories are collection based. – choquero70 Apr 24 '21 at 09:33
  • "The former is allowed to be used in domain services, as they are treated like pure in-memory collections"... you can have a collection based repository interface, but implemented with a persistence device, a db for example, and then it is not a real inmemory collection. – choquero70 Apr 24 '21 at 09:38
  • What about your transfer service example requires that both accounts belong to the same natural person? Voila, you have a constraint that you need to establish before calling the method. Sure, there are always samples that are simple enough to not have any more constraints, but that’s not the point. – domin Apr 24 '21 at 09:40
  • "this has little to do with event handlers"... it has all to do, since events can hold aggregate id, and so you need the 2nd arg for not load the aggregate from the id – choquero70 Apr 24 '21 at 09:41
  • In the end, I think there are multiple competing forces. I still regard my desire to encapsulate domain event handling logic into the domain layer as a justified force. In the end it will probably be a tradeoff, as always. But I certainly differentiate the issue more by now. I thank you for your time and your real arguments, this has helped me! ;) We can continue in private if you like. – domin Apr 24 '21 at 09:45
  • "if you want to make sure the caller is using the service correctly, simply do a pre-condition check in the method"... that check would imply to load the aggregate from the restaurant id of the event, to compare it with the 2nd arg. So why the 2nd arg? You "could" get the restaurant from the event – choquero70 Apr 24 '21 at 09:46
  • "What about your transfer service example requires that both accounts belong to the same natural person? Voila, you have a constraint that you need to establish before calling the method"... the issue was to add unnecessary args, not to do checkings, of course the transfer needs checkings – choquero70 Apr 24 '21 at 09:54
  • "I thank you for your time and your real arguments, this has helped me! We can continue in private if you like"... I think that I've given you many arguments (as a John Doe, like you claimed, not as an authority), but I think you will never surrender. And even finally you also quoted the authority :) – choquero70 Apr 24 '21 at 09:58
  • The event does not contain the aggregate ID, btw. The aggregate is determined by the kind of order. But that’s not relevant for the discussion. If you think that the full event contains too many details, then fine, transform it first into a slimmer event or simply extract the values you need. The main point is that you don‘t create commands out if the event outside of the domain logic, as this translation is what I claim should be part of the domain logic. If you regard this as a fight, then I will surrender here and now. – domin Apr 24 '21 at 11:02
  • What I mean is that you had a domain event handler that didn't use the event to react to it, but it used a 2nd arg added. That doesn't make sense. Regarding the event having the restaurant id or the kind of order, I didn't know, what I wanted to say is that it may be you have to search for the aggregate in some way from event attributes. Regarding the fight and surrender or not, it was just a metaphor, a joke. Discussions are good. – choquero70 Apr 24 '21 at 11:18
  • Our discussion was really missing the point of my question, but raised some other important question(s) instead. Therefore, I created another question asking about that one instead: https://stackoverflow.com/questions/67254749/ddd-the-problem-with-domain-services-that-need-to-fetch-data-as-part-of-their-b – domin Apr 25 '21 at 15:04