DDD Process Managers: Are they part of business logic?
D

2

10

Several sources claim that process managers do not contain any business logic. A Microsoft article for example says this:

You should not use a process manager to implement any business logic in your domain. Business logic belongs in the aggregate types.

Further up they also say this (emphasis mine):

It's important to note that the process manager does not perform any business logic. It only routes messages, and in some cases translates between message types.

However, I fail to see why translations between messages (e.g. from a domain event to a command) are not part of the business logic. You require a domain expert in order to know what the correct order of steps and the translations between them are. In some cases you also need to persist state in-between steps, and you maybe even select next steps based on some (business) condition. So not everything is a static list of given steps (although that alone I’d call business logic too).

In many ways a process manager (or saga for that matter) is just another aggregate type that persists state and may have some business invariants, in my opinion.

Assuming that we implement DDD with a hexagonal architecture, I‘d place the process manager in the application layer (not adapter!!) such that it can react to messages or be triggered by a timer. It would load a corresponding process manager aggregate via a repository and call methods on it that either set its (business) state or ask it for the next command to send (where the actual sending is done by the application layer of course). This aggregate lives in the domain layer because it does business logic.

I really don‘t understand why people make a distinction between business rules and workflow rules. If you delete everything except the domain layer, you should be able to reconstruct a working application without the need to consult a domain expert again.

I‘d be happy to get some further insight I might be missing from you guys.

Dire answered 12/7, 2021 at 5:42 Comment(2)
Could you provide an example that somehow shows where you think such a process manager would contain business logic?Incensory
From the linked article: „For example, when it receives a SeatsNotReserved event, it sends an AddToWaitList command.“. It can only know this translation given a business rule.Dire
D
2

Domain logic is not only to be found in aggregates and domain services. Other places are:

  • Appropriately handling domain events. Domain events can be translated into one or multiple commands to aggregates; they can trigger those commands based on some condition/policy on the event itself and/or the state of other aggregates; they can inform an ongoing business process to proceed with its next step(s), and so forth. All of these things are part of domain logic.
  • A business process is a (potentially distributed) state machine that may involve various actors/users/systems. The allowed states and transitions between them are all a core part of the domain logic.
  • A saga is an eventually consistent transaction spanning multiple local or foreign aggregates that completes either successfully or compensates already executed steps in a best-effort manner. The steps that make up a saga can only be known to domain experts and thus are part of the domain logic.

The reasons why these three things are – in my opinion – mistaken as an application-layer-only concern are the following:

  • In order to handle a domain event, we must load and later save the affected aggregates. Because of this, the handler must also be part of the application layer. But crucially, not only. If we respect the idea behind a hexagonal architecture, then for each domain event handler residing in the application layer, there must be a corresponding one located in the domain layer. Even for the most trivial case where one domain event translates to exactly one command method invocation on some aggregate. This is probably omitted in many examples because it initially adds little value. But just imagine that later on, the translation will be based on some further business condition. Will we also just place that in the application layer handler? Remember: All of our domain logic should be in the domain layer.
    • A side note: Even if we respect this separation of concerns, we still have the choice of letting the domain event be handled by an aggregate itself or let it be translated into an aggregate command by a thin domain service. This choice however is based on an entirely different concern: Whether or not we want to couple aggregates more tightly. There is no right or wrong answer here. Some things just naturally are coupled more tightly while others might benefit from some extra indirection for increased flexibility.
  • In order to correctly implement a business process or saga, we have to take care of various application-specific concerns like message de-duplication, idempotency, retries, timeouts, logging etc. Although one might argue that the domain logic itself should be responsible for dealing with at least some of these aspects first-hand (see Vaughn Vernons excellent talk about modelling uncertainty). Remember, however, that the essence of the sequence of (allowed) steps/actions is entirely based on domain logic.

Finally, a word about coupling. In my view, there is a tendency in the community that coupling is a bad thing per-se and thus must be avoided/mitigated by all means. This might lead to solutions like placing event-command translations (remember: domain logic!) out in the adapter layer of a hexagonal/onion/clean architecture. This layers responsibility is to adapt something to something else with the same semantics/function, but with slightly different form (think power adapters). It is not the place to host any type of domain logic even if it is dead simple. Businesses have dependencies and coupling all over the place. The art is to embrace it where it actually is, and avoid it otherwise. There is a reason why we have partnership or customer/supplier relationships in DDD. And if we care about domain logic isolation, those dependencies are reflected right where they belong: In the domain layer.

  • A side note: An anti-corruption layer (DDD) is a valid example of an adapter. For example, it may take a bunch of remote domain events and transform/combine them in any way necessary to suit the local model. They still remain events that happened in the past, and don't just magically become commands. The transformation only changes form, not function. And it doesn't eliminate the inevitable coupling from a domain perspective. It just rephrases the same thing in a slightly different language.
Dire answered 21/7, 2021 at 21:15 Comment(0)
B
2

A fair portion of the confusion here is a consequence of semantic diffusion.

The spelling "process manager" comes from Enterprise Integration Patterns (Hohpe and Woolf, 2003). There, it is a messaging pattern; more precisely, it is one possible specialization of a message router. The motivation for a message router is a decoupling of the sender and receiver.

If new message types are defined, new processing components are added, or routing rules change, we need to change only the Message Router logic, while all other components remain unaffected.

Process manager, in this context, refers to a specialization of message router that sits in the middle of a hub and spoke design, maintaining the state of the processing sequence and "determining the next processing step based on intermediate results".

The "process definition" is, of course, something that the business cares about -- we're passing these messages around to coordinate activities in different parts of the enterprise, after all.

And yes... this thing that maintains the "state of the processing sequence", sounds a lot like an example of a "domain entity", this is true.

BUT: it is an entity of the message routing domain; which is to say that it is bookkeeping to ensure that messages go to the right place rather than bookkeeping of business information (ie: the routing of shipping containers).

Expressed in the language of hexagonal architecture, what a process manager is doing is keeping track of messages sent to other hexagons (and, of course, the messages that they send back).

Boaz answered 13/7, 2021 at 2:58 Comment(6)
I agree with the semantic diffusion. It is really hard to find clear definitions, so thanks for pointing to the source of it!Dire
If a workflow manager just routes messages between hexagons (or microservices or deploy units or whatever), then where do the translations between messages reside? They must be part of some domain, after all. Or does the workflow receive a kind of ready-cooked protocol definition from the initiator, telling it what to do upfront?Dire
The translations are ultimately in the ports/adapters of the hexagons. A port/adapter is in a no-man's land between the hexagons: it's defensible to say that the port/adapter is in one, both, or neither of the hexagons.Lithiasis
@Levi Ramsey: Why should the translations be there? Adapters adapt one interface to another with the same functionally but slightly different shape. Like from a WebAPI interface to a C# interface. Translating from events to commands is core business logic that must consequently reside in the core of the hexagon. I see no problem putting it there in the form of a saga aggregate or domain service. The technical orchestration (timeouts, retries, logs, etc.) around it belongs in the application layer.Dire
Events from hexagon A become commands in hexagon B. Hexagon A changing its events shouldn't force a change in the core of hexagon B, nor should hexagon B changing its commands force a change in the core of hexagon A. Thus an adapter to convert events from A to commands to B (the adapter of necessity changes when either changes, but the blast radius of a change stops at the adapter).Lithiasis
Whether or not a change in an event should force a change in downstream hexagon (or bounded context (BC) in DDD terms) depends on the kind of relationship these BCs have amongst them. It is absolutely valid to have some kind of coupling between BCs, as do the corresponding sub domains. On the other hand, for processes/sagas over BC-internal aggregates, we have the same discussion again! The translation logic should be in the domain layer or otherwise you have to admit that you tolerate leakage of business logic into application or even adapter layer.Dire
D
2

Domain logic is not only to be found in aggregates and domain services. Other places are:

  • Appropriately handling domain events. Domain events can be translated into one or multiple commands to aggregates; they can trigger those commands based on some condition/policy on the event itself and/or the state of other aggregates; they can inform an ongoing business process to proceed with its next step(s), and so forth. All of these things are part of domain logic.
  • A business process is a (potentially distributed) state machine that may involve various actors/users/systems. The allowed states and transitions between them are all a core part of the domain logic.
  • A saga is an eventually consistent transaction spanning multiple local or foreign aggregates that completes either successfully or compensates already executed steps in a best-effort manner. The steps that make up a saga can only be known to domain experts and thus are part of the domain logic.

The reasons why these three things are – in my opinion – mistaken as an application-layer-only concern are the following:

  • In order to handle a domain event, we must load and later save the affected aggregates. Because of this, the handler must also be part of the application layer. But crucially, not only. If we respect the idea behind a hexagonal architecture, then for each domain event handler residing in the application layer, there must be a corresponding one located in the domain layer. Even for the most trivial case where one domain event translates to exactly one command method invocation on some aggregate. This is probably omitted in many examples because it initially adds little value. But just imagine that later on, the translation will be based on some further business condition. Will we also just place that in the application layer handler? Remember: All of our domain logic should be in the domain layer.
    • A side note: Even if we respect this separation of concerns, we still have the choice of letting the domain event be handled by an aggregate itself or let it be translated into an aggregate command by a thin domain service. This choice however is based on an entirely different concern: Whether or not we want to couple aggregates more tightly. There is no right or wrong answer here. Some things just naturally are coupled more tightly while others might benefit from some extra indirection for increased flexibility.
  • In order to correctly implement a business process or saga, we have to take care of various application-specific concerns like message de-duplication, idempotency, retries, timeouts, logging etc. Although one might argue that the domain logic itself should be responsible for dealing with at least some of these aspects first-hand (see Vaughn Vernons excellent talk about modelling uncertainty). Remember, however, that the essence of the sequence of (allowed) steps/actions is entirely based on domain logic.

Finally, a word about coupling. In my view, there is a tendency in the community that coupling is a bad thing per-se and thus must be avoided/mitigated by all means. This might lead to solutions like placing event-command translations (remember: domain logic!) out in the adapter layer of a hexagonal/onion/clean architecture. This layers responsibility is to adapt something to something else with the same semantics/function, but with slightly different form (think power adapters). It is not the place to host any type of domain logic even if it is dead simple. Businesses have dependencies and coupling all over the place. The art is to embrace it where it actually is, and avoid it otherwise. There is a reason why we have partnership or customer/supplier relationships in DDD. And if we care about domain logic isolation, those dependencies are reflected right where they belong: In the domain layer.

  • A side note: An anti-corruption layer (DDD) is a valid example of an adapter. For example, it may take a bunch of remote domain events and transform/combine them in any way necessary to suit the local model. They still remain events that happened in the past, and don't just magically become commands. The transformation only changes form, not function. And it doesn't eliminate the inevitable coupling from a domain perspective. It just rephrases the same thing in a slightly different language.
Dire answered 21/7, 2021 at 21:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.