Avoiding the Serverless Workflow Antipattern
Tags: aws, serverless, stepfunctions, sqs
Published: 2023-02-27 12:16:45

A common pattern for teams moving from cron-based jobs to event-driven architectures is to start using AWS Lambda as the compute engine for asynchronous event handlers. This can work out very well for simple event handlers, but I've observed a common antipattern that can appear when workflows grow more complex. Sooner or later workflows with multiple steps enter the picture and we'll need to be careful in our approach lest the complexity get the best of us.

## Asynchronous Event Handler
Using Lambda as an event handler can be a pretty good plan. A [large number of AWS services](https://docs.aws.amazon.com/lambda/latest/dg/lambda-services.html#listing-of-services-and-links-to-more-information) can target Lambda, making integrations easy. For third parties, you can use a [FURL](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html) to create a low-friction webhook. Lambda of course has a pricing model that is ideal for irregularly-scheduled events. It's relatively easy to compose and deploy a Lambda function.

![Event triggering a Lambda Function](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/egtw7w95x7laomfu6u79.png)

## Fan-Out with SQS
This simple architecture can cover a lot of use cases, but not all of them. What happens when complexity grows? The main driver I've seen for adopting more complex architectures is when the job won't complete within 15 minutes, but there can be other reasons for doing this as well, such as asynchronously communicating with some other system in the middle of the job.

Let's say our job is running long and we decide to adopt a fan-out pattern. We can achieve this by putting messages in a queue using SQS and subscribing another Lambda function to those messages. This will give us the opportunity for batching and throttling to make sure our workflow can run in parallel, but not overwhelm other resources.

![Event triggering a Lambda Function which uses SQS to fan out work to more Lambda Functions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6j7fp7uxfmtubjjsequa.png)

## Fan-Out with Job State Tracking
Okay, now our workload is broken down some, but we have a new problem. How do we know when the job has finished? How do we know the success/failure rate of each step in the job? What if we only want one instance of our job to run at a time? We can't prevent a new job from kicking off if we don't know whether or not the old one has finished.

SQS is a great service, but it's not a workflow engine! It won't maintain job state for us, so if that's important, it will be up to us to implement it. We could store the current job state in a DynamoDB table, writing the number of expected downstream jobs and tallying them as we go, maybe including another step at the end to give us a summary report. Okay, now our architecture looks like this:

![Event triggering a Lambda Function using SQS for fan-out, storing job state in DynamoDB and finally using another Lambda function to provide a summary](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jxpto7t8nbo0wmjev8gy.png)

To achieve this architecture, each of our Lambda Functions is now responsible for job tracking. We've more or less had to write our own workflow engine. We'll own that code and need to maintain it. This is an **antipattern** that violates the promise of managed services, misuses SQS, and forces us to add boilerplate, workflow-aware code to every Lambda Function.

## Enter Step Functions
AWS Step Functions is a great tool to solve this problem. Step Functions allow us to focus on our business logic while workflow and job state are handled by a managed service. We can query the service or check out the console visualization to find out whether or not a job is in progress or finished.

![Step Function with a Map state](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j2ytlwb1ketgn74k7g0o.png)

All the problems outlined above are solved by building the workflow with Step Functions. In the provided example, we still have the Job Start function, which perhaps performs an initial database query to drive the rest of the job, or it could query the Step Functions service to see if another execution is underway (for use cases where we want to limit concurrency). If none of that applies to our workflow, we can skip this step! Likewise we could skip the summary step since Step Functions will be able to provide the output of the work that was done by each of our workers and as such a summary step would only be needed if we wanted to format a human-readable report.

## Learning Step Functions
Step Functions can be complex and challenging to learn. They are described using [Amazon States Language](https://states-language.net/). This complexity can drive developers to avoid this service, leading to subpar architectures. I acknowledge there's a lot to learn here, but it's well worth it! And it doesn't need to be an uphill battle.

The [Workflow Studio](https://docs.aws.amazon.com/step-functions/latest/dg/workflow-studio.html) is a great way to get get productive fast. You may also be interested in an [article I wrote](https://dev.to/elthrasher/exploring-aws-cdk-step-functions-1d1e) about learning to write Step Functions using [AWS CDK](https://aws.amazon.com/cdk/). If that doesn't do it for you, there are a number of other free resources on the web that can put you on the right path.

## You Also Get...
I left off a lot of the great features of Step Functions by focusing on the comparison to an SQS-driven workflow. Step Functions also support:
- [synchronous express workflows](https://docs.aws.amazon.com/step-functions/latest/dg/concepts-express-synchronous.html)
- [native integrations](https://docs.aws.amazon.com/step-functions/latest/dg/supported-services-awssdk.html) (Lambda-less)
- [asynchronous invocation and callback with Task Tokens](https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-wait-token)
- time-based [Wait steps](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-wait-state.html)
- [Intrinsic Functions](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html) (again Lambda-less)
- automatic [Retry](https://docs.aws.amazon.com/step-functions/latest/dg/concepts-error-handling.html#error-handling-retrying-after-an-error)

## Know and Plan Your Architecture
The best path to success in cloud is to understand our objectives and know which services will help us to achieve them. Sometimes all we need is a single Lambda handler. If "fire and forget" is good enough, then an SQS-based fan-out is probably fine. Still, the use-cases for more complex workflows is indelible and more often than not, we'll need the ability to audit them. Step Functions is a great tool for achieving that aim.

## More on Step Functions
As mentioned above, there's a lot of great articles out there on Step Functions to help in your journey. Here are a few of my favorites - and please feel free to add to the list in comments below!

- https://matt.martz.codes/orchestrating-choreography-with-event-driven-step-functions
- https://theburningmonk.com/2020/08/choreography-vs-orchestration-in-the-land-of-serverless/
- https://www.readysetcloud.io/blog/allen.helton/when-not-to-use-step-functions/
- https://dev.to/lukvonstrom/best-practices-for-using-aws-stepfunctions-2io
- https://dev.to/aws-builders/testing-aws-step-functions-flows-2kpn
- https://dev.to/aws-builders/aws-step-function-vs-aws-lambda-benchmark-54hj
- https://dev.to/mathem/an-approach-to-loosely-coupled-cloudwatch-alarms-and-contextual-alerts-2fh2
- https://dev.to/aws-builders/using-step-functions-to-eliminate-your-nat-gateway-12ge
- https://dev.to/aws-builders/step-functions-for-making-your-text-based-images-searchable-334
- https://serverlessfirst.com/serverless-email-scheduler/