The trade-offs with functionless integration patterns in serverless architectures
A functionless (a.k.a. Lambda-less) integration pattern is where a direct integration between two AWS services is configured instead of using custom code in an intermediary Lambda function.
Examples of functionless integration patterns include:
- API Gateway service integrations — Maps API Gateway requests directly into services such as DynamoDB and EventBridge
- Step Functions AWS SDK integrations — Maps Step Functions tasks directly to any of the 200+ AWS services which can be called from the AWS SDK
- AppSync VTL resolver — Maps AppSync GraphQL requests to services such as DynamoDB (the most common), EventBridge or HTTP endpoints
Most developers new to serverless (or to cloud development in general) will default to using Lambda over functionless because that’s where they’re most comfortable and/or they simply aren’t aware of the existence of direct integrations. As a consequence, many experienced AWS serverless advocates are keen to promote the benefits of these functionless patterns and increase their usage.
However, I’ve seen some developers at the other end of the spectrum using functionless integrations for purist reasons (“it’s the serverless way!”) without fully understanding or giving enough weight to the downsides.
In this article, I’ll walk you through these benefits and downsides as I see them, so you can hopefully make a more informed decision in your own integration use cases.
Benefits of functionless integrations
If you use a direct service-to-service integration, you can expect these benefits:
- Lower Latency. Lambda can introduce a little latency, through occasional cold starts and also simply by being an extra network hop. By going directly from one AWS service to another, you avoid this.
- No Code rot. You own the code for a Lambda function and its dependencies and so it’s on you to update your dependencies when new versions are available. With direct service integrations, AWS takes care of this for you.
- Free (in terms of cloud bill). You have to pay for Lambda invocations whereas most, if not all, Lambda-less direct integrations are free (you just pay for the service usage at each end of the integration).
- Even higher scalability. Lambda functions are very scalable to begin with, but they are subject to an account-wide soft limit of 1,000 concurrent executions. Once this limit is hit, functions get throttled. So if you have a very high throughput integration, a functionless pattern won’t hit this limit (though the services at each end of the integration will likely have their own limits that you’ll need to heed).
- Less IaC code is needed if you don’t have to provision and wire up the Lambda function, e.g. creating a dedicated IAM role for your function. This benefit is arguable based on how much IaC the functionless component requires to set up.
Downsides of functionless integrations
If you use a direct service-to-service integration, you can expect these drawbacks:
- Longer learning curve for functionless “code” format. While they are considered “low-code”, some code is still required to define the mappings for these integrations and some mapping languages are quite esoteric and limited in features.
- Poor tooling. The tooling for these mapping languages (e.g. VTL) is generally poor relative to full-blown programming languages (e.g. no linting or typechecking), resulting in a slower inner loop of development.
- No composability. The mapping languages don’t support features such as import/include or ability to define library functions, and so make re-use difficult or impossible.
- Risk to data integrity. This lack of composability can make direct integrations to databases such as DynamoDB problematic. A single-table design approach in particular requires several index attributes (e.g. PK, SK) to be dynamically calculated before writing an item. With direct service integrations, this business logic gets spread across the entire codebase rather than centralised within a single module. The end effect is that data integrity may be compromised if, say, a wrong format is used for setting an index attribute.
- Difficult/impossible to test (in isolation). Testing Step Functions is already hard in and of itself, so Step Functions which integrate directly to DynamoDB can be even harder to test. If you instead invoked a Lambda function from your state machine, you could easily test the Lambda function in isolation.
- Variable debuggability. Whereas Lambda functions can be monitored, traced and logged in a standard fashion, debugging functionless integrations is always specific to the service integration being used, and is sometimes not as optimal as Lambda. For example, AppSync resolver logging is almost a must-have if you’re using VTLs, but the automatic logging to CloudWatch is very verbose and coarse-grained, and so potentially significantly expensive to enable in production, requiring code-heavy workarounds.
Rules of thumb
So we’ve covered the main pros and cons of using functionless integration patterns. As with many (most?) architectural decisions in serverless-land, there is no generic best answer and you need to evaluate each of the pros and cons with respect to your specific use case. To help with your decision, here are a few rules of thumb you can start with:
- If the integration is purely for transportation without performing any transformation, then it’s almost always best to use a functionless approach. An example of pure transportation is routing a message as-is to an SQS queue. An example of transformation is appending new fields or modifiying existing ones within a payload before transporting it to DynamoDB, say.
- Use a Lambda function if you are performing any non-trivial business logic where bugs could be hard to detect through automated testing. Examples of such logic would be calculations and validations.
- Consider the future “ejection” path if you’ve already decided upon a functionless approach for your use case, and what the impact would be (in terms of development effort and operational delivery) to switch to using a Lambda function in the future if your use case becomes more complex.
Further reading
If you’d like to learn more about functionless, check out these blog posts and Twitter discussions:
- Don’t wait for Functionless. Write less Functions instead by Sheen Brisals
- Some code is more equal than others by Paul Swail
- Twitter thread arguing in favour of functionless by Zack Kanter, CEO of Stedi, a startup which pioneers many new serverless features and practices
- Twitter discussion on Lambda-less patterns in context of data access, with responses generally arguing against Lambda-less, started by Alex DeBrie
Other articles you might enjoy:
Free Email Course
How to transition your team to a serverless-first mindset
In this 5-day email course, you’ll learn:
- Lesson 1: Why serverless is inevitable
- Lesson 2: How to identify a candidate project for your first serverless application
- Lesson 3: How to compose the building blocks that AWS provides
- Lesson 4: Common mistakes to avoid when building your first serverless application
- Lesson 5: How to break ground on your first serverless project