How to design multistage tasks in DDD

Viewed 171

It's about year that we have refactored our back-end code base to have an architecture more inline with DDD and we also implemented a combination of Hexagonal architecture and Ports & Adapters pattern in most of our services. Until now most of our uses cases were quite easy and straight forward; so using simple entities and Command and Query handlers etc worked well. But as the software's features' grew, We now need more complex structures.

So we want to implement some features that the best description I can give of them is multistage tasks. For instance we have a two-step verification for signup and login: The users enter their info, then an a verification code is sent to them (by sms) and they have to enter it to get a token. Or for some feature, we need to get a users driver licences and photo etc, Then an admin has to verify them and send notification to the user and then the user has to do the payment and so on.

We observed that we can't treat this kind of features(tasks?) like simple entities or CQRS patterns but we also failed to find a good and idiomatic way to implement this features. So we are seeking for some information and guidelines to help us.

2 Answers

The broad approach is to model a long-running process as a saga, which can track its state (durably: event sourcing is often really useful here, as the persisted events give you audit logging and observability for nearly free) while sending commands to other services (as well as receiving commands). The saga can autonomously recover from failures to leave the system in a defined (ideally good/functional state, though sometimes it might be useful to leave the system inoperable and signal that things need to be fixed...).

For example, you might model an unverified user as a saga where the states could be:

  • Created with profile information, SMS not sent
  • SMS sent and expecting code (retains profile information)... an SMS with a new code could be sent from this, but it would invalidate the old code
  • Verified (probably doesn't need profile information beyond the ID of the verified user)

In either of the first two states a "send verification SMS" command is valid and would transition to the second state (after generating a code and sending the SMS). In the second state, a "verify code from SMS" command can be validated and if valid, transition to the third state (after forwarding the profile information so that a verified user with that profile information can be created).

You can basically view a saga as a "todo list" (there are DDD-adjacent schools which prefer this terminology). As in a lot of DDD, think about how you would solve a problem without using a computer. For the ID verification process, you'd probably have some sheet of paper with a checklist that would eventually get attached to the user's file; processes of that sort have survived/thrived for so long that there's good reason to incorporate aspects of them into our software designs.

Most Aggregates can be considered Finite State Machines in some sense.

Aggregates typically involve reasonably complex workflows which cycle through many (but finite) states, with each transition associated with well-defined business rules.

In your example, the User record moves through states like REGISTERED, VERIFIED, ADMIN APPROVED, PAID, etc. This also gives you the freedom to transition from any state to any state, allowing you to add new rules over time (Discounted users who never pay, Invited users who don't have to verify, etc.)

The state machine is built into the Aggregate. Commands initiate transitions. Command handlers call aggregate methods that perform the transition and change aggregate state before persisting them (and raising events, if any).

On a side note, this is a good heuristic to find if your app is a good fit for using DDD/CQRS patterns. For simple cases like CRUD operations, a State Machine does not make sense. Most CRUD records would cycle between ACTIVE-ARCHIVED or CREATED-UPDATED-DELETED states.

Read more:

Related