I'm trying to design a workflow engine using a DDD approach.


  • (B1) Assign items to a workflow
    • Invariant: depending on their types, items can only start the workflow on specific steps.
  • (B2) Attach/detach items to other items
    • Invariant: all items (container and elements) must be on the same step.
  • (B3) Transition items from one step to another
    • Invariant 1: the transition should also be applied to their children.
    • Invariant 2: items inside a container can't be transitioned directly.

Clients may have several active workflows. They just know about items (or some VO representing an item). Everything should be done through a facade.

Preferred solution

So far, I designed it as a typical graph.

Workflow (AR) holds all steps. Steps (entity) contains the items & allowed transitions. Items (entity) knows its parent and its children.

Pros & cons

The consistency boundary looks ok. Even though I could be changing the same workflow on different transactions, as long as the items aren't the same everything should work.

B2 and B3 may have a race condition, but it might be ok they do as the invariants kind of depend on each other.

Usually, you work on one step at the time. So loading the whole workflow seems unnecessary. Also, I can't think of a natural way of loading it with the items in the correct steps.

This solution looks fine. However, I feel something is missing.

Another things I've thought about

  • Create a Associations AR

    The Associations AR would have a reference to the parent item and a collection of children items. Here I could query the workflow and check if the items are in the same step to keep B2 invariant.

    However, this is a bit fragile as the entities could be transitioned on a different transaction and I'd not know it.

  • Step as AR

As you usually work on one Step at a time, this could be a possible solution. However, B2 2nd invariant can't be 100% enforced. Also I'd need to raise an event to perform B3.

Any thoughts/opinions about it? Thanks!

1 Answer 1


IMHO, enclosing Steps and Items within the Workflow aggregate results in a large aggregate structure but keeps the overall complexity low. Raising Events and backgrounding jobs is fine, but it is always better to enforce invariants upfront - it leads to less complexity.

The only connection between the Workflow entity and its children seems to be that you need to start the Workflow on specific items/steps (I may be reading your invariants wrong, let me know). As and when the complexity in the Step entity increases or you find that you are loading the workflow aggregate for no other reason than operating on a Step, you could elevate Step to be an aggregate as well.

This structure fits well with working with one Step at a time. You can start the Workflow with a background Event Handler that listens to events published by the Step Aggregate.

  • thanks for the reply. I agree the workflow aggregate may be too big. My biggest concern with Step as AR is that for transitioning items between Steps (and enforcing the invariants) I'd need to change 2 aggregates within the same transaction.
    – freakmind
    Feb 24 at 18:21
  • Maybe I'm trying to hard to adhere to the "single AR per transaction" rule?
    – freakmind
    Feb 24 at 18:38
  • Single AR per transaction is pretty invaluable, so your thought process is correct. When you say two aggregates, are you meaning two steps or a step and its associated workflow?
    – Subhash
    Feb 24 at 18:46
  • 1
    @Subhash Single AR per transaction is pretty invaluable if you foresee the need to shard your data between ARs. That seems very unlikely in this scenario. OP - the single AR per transaction "rule" is, without a doubt, the most cited reason systems undertake incredible amounts of unnecessary complexity. The above is just a guideline and only really applies in systems where a distributed transaction between ARs is likely to occur should boundaries be drawn. It's about scaling! Are you worried about scaling? Even if you are, there are better sharding strategies to consider (e.g. ID ranges). Feb 27 at 16:21
  • 1
    I see. You are worried about concurrency (more specifically version control but I digress...). The high-level strategy is to validate the inputs "server-side" before persisting any data. We can either validate using our "database": locking can provide a guarantee that the data sent to the client matches the data held in the server for the lifetime of the transaction (methodsandtools.com/archive/archive.php?id=83). Mar 1 at 22:09

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.