Handling workflows in Node.js using the builder design pattern_ A Comprehensive Guide _ by Omar MOKHFI _ Medium
Handling workflows in Node.js using the builder design pattern_ A Comprehensive Guide _ by Omar MOKHFI _ Medium
Become a member
109 2
A workflow is basically a list of tasks that run in order, and optionally, each
task has the ability to provide its output as an input to the next task. You can
find different use cases for these workflows, of which we will mention few
examples:
The workflow of user registration
• When a user sends a message via a website’s contact form, we first add
them to the CRM as a new lead, push their message to the sales channel
on Slack , and then determine whether they are a new contact and send
out marketing emails accordingly.
In this step-by-step guide, we will walk through the process of making our
own workflow handler using the builder design pattern, and to make things
interesting, we will use the registration example.
Prerequisites: Before diving into the implementation, make sure you have
the following:
Project Setup
Before jumping to the real thing, let’s try to see how we can achieve the
workflow with the usual basic approach:
mkdir workflow-sample
cd workflow-sample
npm init -y
2- Add TypeScript
First, we will install the packages we need
Next, we will create a new tsconfig.json file in our root folder and paste this
code:
{
"compilerOptions": {
"target": "es2022",
"module": "commonjs",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
To be able to run the code with the command “npm run dev”, we will add
this script to our package.json.
{
...
"scripts": {
"dev": "ts-node index.ts"
}
}
There are multiple concerns inside the registration process, and for that, we
will separate the main function into multiple sub-functions and only care
about the input and output of each one.
So, our main function in the index.ts file will look like this:
// The registration takes the email, password and image file of the user
// This can be acheived by creating an API and using a library like multer
const register= async (email:string, password: string, image: Blob) => {
try {
// 1. this function uploads the image file and returns the link of that image
let imageLink = await uploadImage(image);
// 2. this function saves the user to the database
let savedUser = await saveUser({ email, password, imageLink });
// 3. this function sends a verification email to the user
let result = await sendVerificationEmail(email);
} catch (error) {
console.log("Process failed");
}
};
Now, we will try to simulate the input and output of each function. And since
each sub-function takes some time to run, we will emulate that with a
custom timeout function.
The first issue is that there is no retrying mechanism. So, if for example, the
storage service doesn’t respond because of a connectivity issue, the whole
process will fail. At this point, you will say — well, we can just try re-running
the process when it fails, we can yes but although this solution works, it still
has a drawback, which is an infinite loop if the error is not a temporary
connectivity issue.
The second issue is that, for example, when the second step fails, we will
need to re-run all steps from the beginning. So, if the first step is complex
and takes 10 seconds to run then we will have to re-run it every time the next
step fails.
In the next part, we will create a class that handles all the mentioned issues
using a simplified useful design pattern which is the builder.
• the workflow can be configured with a number of retries limit (it means
if the retries limit is 3 then we can only re-run a function 3 times, if it still
persists the workflow will be aborted)
constructor(retryLimit: number = 3) {
this.retryLimit = retryLimit;
}
static createWorkflow(
retryLimit: number,
callback: (workflow: Workflow) => void
): Workflow {
const workflow = new Workflow(retryLimit);
callback(workflow);
return workflow;
}
constructor(retryLimit: number = 3) {
this.retryLimit = retryLimit;
}
static createWorkflow(
retryLimit: number,
callback: (workflow: Workflow) => void
): Workflow {
const workflow = new Workflow(retryLimit);
callback(workflow);
return workflow;
}
finally(callback: FinallyFunction) {
this.finallyCallback = callback;
}
async run(): Promise<void> {}
}
You might have noticed that the “create” method returns “this”, and the reason
behind that is to create chaining (you will see how it works when we use the class)
// core/workflow.ts
if (this.finallyCallback) {
try {
this.finallyCallback(input);
} catch (error) {
console.error("Error in final step:", error);
}
}
}
// core/workflow.ts
// retrying the step as long as it is not successful and retries's limit is not reached yet
// this also makes sure we re-run the workflow starting from the failed step
while (attempts < this.retryLimit && !success) {
try {
// passing step's input to the next step
input = await step(input);
success = true;
} catch (error) {
attempts++;
if (attempts === this.retryLimit) {
console.error(
`Step ${i + 1} failed after ${attempts} attempts:`,
error
);
}
}
}
// if the step is still not successful after all attempts, the workflow will be aborted
if (!success) {
console.error("Workflow aborted due to step failure.");
break;
}
}
if (this.finallyCallback) {
try {
this.finallyCallback(input);
} catch (error) {
console.error("Error in final step:", error);
}
}
}
With that, our Workflow class fits all conditions and handles all issues.
Although this way works just fine, it is recommended to keep the inputs and
outputs of all steps visible. We can modify our registration function this way:
What’s next ?
I know! A boring guide with too many details. Of course you can just take the
code of workflow.ts and check the last part on how to use it, but the goal of
this guide was that you can understand each part of the code so you can
modify it yourself.
For example, you can add another condition to this workflow so you can test
your comprehension.
• If any step fails after all attempts, the workflow should run a rollback
65 Followers
Apr 15 53
Lists
Stories to Help You Grow as a General Coding Knowledge
Software Developer 20 stories · 1663 saves
19 stories · 1436 saves
Sep 6 50 1 Oct 13 34
Vitaliy Korzhenko Nidhi Jain 👩💻 in Code Like A Girl
Help Status About Careers Press Blog Privacy Terms Text to speech Teams