Key Takeaways
- Building applications with serverless technology is not all about implementing functions for every purpose.
- Overusing interdependent functions coupled with other services causes the architecture to devolve into spaghetti.
- It is essential to develop a mindset that recognizes functions as code liability. Eliminating them where possible reduces cost and complexity.
- There are use cases where functions are not the best fit. Assessing and avoiding them in such situations helps build well-architected applications that optimize cost and efficiency and promote sustainability.
- Business logic is not always concentrated as functions in modern distributed and event-driven systems. Service orchestration, for example, is well suited for handling distributed business logic.
Services operated and managed by cloud providers - known as fully managed services - are not new. For example, Amazon Simple Queue Service (SQS) and Amazon Simple Storage Service (S3) are fully managed services from AWS and are nearly two decades old.
As platforms and infrastructure became available as services from Amazon Web Services (AWS), Microsoft Azure, Google Cloud Platform (GCP), and other cloud providers, it promoted the growth of independent software vendors (ISVs) offering several products as Software as a Service (SaaS).
Though the term serverless appeared in 2012 - inspired by the evolving cloud landscape and how SaaS applications eliminated the need to own and manage physical hardware - its association with the existing fully managed cloud services wasn’t evident.
While the growth of the SaaS market was fueling innovations, the industry was missing a crucial piece in the managed services repertoire - a fully managed compute service in the cloud. In 2014, when Amazon released AWS Lambda - the fully managed compute service in the cloud, popularly known as Function as a Service (FaaS) - it changed the cloud industry forever, instantly elevating serverless to new heights! Soon after, Microsoft and Google followed with their FaaS offerings, Azure Functions, and Google Cloud Functions, respectively.
As the industry's first FaaS offering, AWS Lambda gained instant popularity. As a result, several case studies, lessons, patterns, and anti-patterns on serverless development revolved around Lambda functions. Hence, its name became associated with some of the early industry thoughts referenced in this article. However, these thoughts are equally applicable and valid with every FaaS offering.
With the release of API gateway and management services from the leading cloud providers, the world was open to consuming functions' computational logic. Thus, the serverless revolution became unstoppable - together, the overutilization of FaaS!
The Unglamorous Side Effects of FaaS
Programming purists often pay the least attention to non-computational managed cloud services, viewing the services as configuration-oriented and suited for infrastructure engineers. However, these purists are now equipped with a computation service in the cloud to explore the programming model behind functions.
As the adoption of FaaS increased, cloud providers added a variety of language runtimes to cater to different computational needs, skills, etc., offering something for most programmers. Language runtimes such as Java, .NET, Node.js, Python, Ruby, Go, etc., are the most popular and widely adopted. However, this also brings some challenges to organizations adopting serverless technology. More than technology challenges, these are mindset challenges for engineers.
The following sections highlight some of the side effects of the overuse of FaaS in serverless applications.
The Lambda Hammer mindset
"If the only tool you have is a hammer, everything looks like a nail". - Abraham Maslow.
Writing FaaS functions is easy for engineering teams, allowing for a quick turnaround in implementing business logic and releasing features, which improves team velocity. This newfound fame and the speed of development inevitably create a mindset of doing anything and everything as functions - the only solution to every business problem. On AWS, Lambda functions were considered the one-fix remedy for everything. Hence, the term Lambda Hammer!
One of the primary reasons engineers develop a Lambda Hammer mindset can be attributed to their industry experience and programming competency. Many engineers developing traditional applications in a non-cloud environment mainly focus on the programming aspects of software engineering. In such environments, siloed teams share the responsibilities of bringing an application to production or releasing a feature to customers. Developers - confined to programming - implement a solution according to an architect's vision, test it by QA engineers, and hand it over to a platform or infrastructure team to deploy and operate it.
When programming-focused engineers transition to working with serverless technology, the Lambda programming model and the concept of FaaS become natural attractions - they are perceived as the tools to solve every problem!
The Lambda Pinball Architectures!
In a pinball game, a ball is propelled into a cabinet where it bounces off various lights, bumpers, ramps, and other obstacles. - Wikipedia.
Thoughtworks initially coined the term Lambda Pinball Architecture to warn of a situation where an interdependent and tangled web of Lambda functions, along with services such as S3 buckets and SQS queues, bounce requests around, resulting in a complex and distributed monolith application that is hard to test and maintain.
Though Thoughtworks highlighted Lambda and other AWS services, the pinball situation can also occur with Azure, Google Cloud, and other providers. All major cloud providers offer services with similar capabilities. Hence, the serverless development best practices are relevant and applicable to all.
The above condition occurs due to a poorly architected approach to employing functions to implement computational logic, data ingestion, data transformation, data transport, service mediation, and other activities without clear service boundaries. The dreadful outcome of a prolonged pinball situation is a tangled architecture, the serverless ball of mud, as shown in Figure 1.
Figure 1: An entangled serverless architecture
Unintentional increase in cloud cost
With FaaS functions, you only pay for the execution (if you don’t use some advanced features). However, implementing more functions than necessary increases your cloud cost and total cost of ownership (TCO).
With most cloud providers, a function’s cost has two main parts:
- The price for each request made to the cloud service to invoke the function
- The time to complete each invocation and the memory (RAM) allocated
Let’s say your function is
- Allocated with 1,024 MB of memory
- Invoked 3 million times a month (100,000 invocations per day)
- Each invocation executes for 10 milliseconds (ms) on average
For an AWS Lambda function deployed in the Europe Central Region, the above will cost 0.40 USD per month (with the free-tier allowance). The cost is the same for an Azure Function with the above configuration running in the Germany West Central Region.
However, if the average execution duration of the function were 10 seconds (instead of 10 ms), then the monthly cost of the AWS Lambda function would jump to 493 USD, and the Azure Function would increase to 474 USD.
Now, think of the cost savings if several such functions perform non-computational tasks that can be achieved by other means - i.e., in a function-less way!
Note: The above Lambda cost is for the use of an x86 processor. By switching to the Arm processor, the price will be reduced to 395 USD.
Impact on sustainable cloud operation
Sustainability is a crucial aspect of modern cloud operation. Consuming renewable energy, reducing carbon footprint, and achieving green energy targets are top priorities for cloud providers. Cloud providers invest in efficient power and cooling technologies and operate an efficient server population to achieve higher utilization. For this reason, AWS recommends using managed services for efficient cloud operation, as part of their Well-Architected Framework best practices for sustainability.
As a service consumer, you are also responsible for sustainability. Hence, the major cloud providers have set out well-architected best practices for sustainability that promote a shared responsibility between the provider and its consumers.
While the cloud providers manage the resources for FaaS to achieve sustainability of the cloud, you have equal responsibility to operate the functions sustainably in the cloud. This includes,
- The optimal use of the number of functions
- Optimizing the memory allocation of each function
- Optimizing performance to the required level and not to the maximum
When you implement a FaaS function for unsuitable purposes or operate in a suboptimal way, you indirectly impact the three core elements of the cloud: compute, storage, and network. Each consumes energy for its operation.
Increasing your code liability
"All the code you ever write is business logic". - Dr. Werner Vogels, CTO, Amazon.com.
As a software engineer, you hear similar statements and arguments in the industry about program code.
"Code is a liability, not an asset, for an organization".
"The code you write today becomes legacy tomorrow".
"The more code a company has, the more money it spends to maintain adding to its TCO".
Regardless of such arguments, one cannot avoid writing code - be it function code, data code, integration code, or infrastructure code. However, the focus here is on unnecessary and unwanted function code.
As discussed earlier, the Lambda hammer mindset often drives engineers to implement a function instead of an alternate cloud service or feature. The function now becomes an unnecessary burden for the team to maintain.
When you think of a function, think beyond your joy of coding it - the testing, the CI/CD process, releasing, observing, and your cloud account’s concurrency limit, etc.
How to Develop a Low-code Mindset
For engineers new to serverless, equipping their minds to its needs can be challenging. Hence, you hear about the serverless mindset as a prerequisite to adopting serverless. This is because working with serverless requires a new way of thinking, developing, and operating applications in the cloud. You must think of serverless applications as orchestrations of managed cloud services knitted with infrastructure code.
Serverless as a technology ecosystem
Often, engineers view serverless environments with a programming eye, ignoring the capabilities of the managed services and the roles and responsibilities of the people involved.
In the book Serverless Development on AWS (O’Reilly, 2024), which I co-authored with Luke Hedger, we advise seeing serverless technology as an ecosystem that contains the cloud provider, FaaS, and other managed services, tools, frameworks, engineers, stakeholders, etc. This brings a sociotechnical mindset to technology, which is vital to working with modern technology and engineering practices.
Figure 2: The view of the serverless ecosystem
Another way of viewing it is through systems thinking. Diana Montalion's book Learning Systems Thinking (O’Reilly, 2024) states that a system is a group of interrelated hardware, software, people, organization(s), and other elements that interact and/or interdepend to serve a shared purpose.
Not all microservices need functions
When you build microservices, depending on your domain and your team’s bounded context, as explained in my talk, The Set Piece Strategy, at QCon 2024, not all services perform computations that require functions. As you will learn below, the business logic of a service can be orchestrated and collaborated with other services via native integration and event-driven communication.
Fast flow teams seek quick turn around to incidents. Less is more for such teams.
Modern product teams that are stream-aligned (as advocated by Team Topologies) and autonomous develop and release new features and updates in quick iterations. Serverless is a great technology that enables fast flow.
As the teams operate their workloads in the cloud, observing and remediating issues in production is crucial. The square of balance is a term used in Serverless Development on AWS (O’Reilly, 2024) to explain how the four activities of testing, delivering, observing, and recovery are key to achieving balance.
Figure 3: The serverless square of balance for fast flow
Having minimal code liability is advantageous when serverless teams operate in a fast-paced development environment. It reduces the potential sources of bugs and failure points and shifts more responsibility to the cloud provider.
What is Functionless?
In a serverless context, the terms functionless, codeless, Lambda-less, no-code, low-code, etc., express the idea of reducing the number of FaaS functions in an application with native service integrations.
I first heard about Functionless in 2019 at the Serverless Days conference in Helsinki. Realizing the need to educate engineers about the side effects of too many functions, I have been advocating in the community, reiterating the benefits of eliminating functions where feasible.
In addition to understanding the nuances of cloud computing, a crucial part of architecting and developing serverless applications is thinking beyond FaaS and remembering that functions are a part of the serverless ecosystem, not all of it.
Doesn’t functionless increase lock-in?
The moment a business decides to consume services from a cloud provider or a product from a vendor, it introduces some form of lock-in. Businesses are aware of this and consciously choose to grow their business with a stable and cordial partnership with the provider - not to switch products, services, and providers frequently.
It is true that if you use native service integrations and cloud platform-specific features, your dependency increases. Even if you make the computational logic of a function cloud-agnostic, it is hard to make it pure agnostic. As you learned above, a function must collaborate with one or more cloud services to offer business functionality. When you focus on making a function cloud-agnostic, you are not capitalizing on the reasons for being in the cloud. It is counterintuitive and harms the business if it chooses a cloud provider and then decides to work against its core benefits.
Architecting serverless applications with fewer functions
"Simplicity is better than elaborate embellishments. Sometimes, something simple is better than something advanced or complicated". - From the web.
Though it is easy to advocate simplicity, arriving at simplicity is not always easy. It requires experience dealing with complex architectures, knowledge, and awareness of the areas where simplicity can be practiced.
Your experience building serverless applications helps you view architectures with a simple lens. This section will examine areas where you can use patterns to avoid functions and make your architecture functionless.
Handling data
An advisory in the serverless community says you should use a function to transform data, not transport it. If you use a function to move data between services, you use it for the wrong purpose, or the cloud provider lacks a native feature.
Data handling is a core part of the cloud, and modern data sets' volume and growth rate are exponential. From a cost and scale point of view, it is not advisable to employ functions for every data operation in such situations. Hence, you should find ways to leave the heavy lifting to the core cloud services.
Traditionally, databases refer mainly to relational database systems (RDBMS) and, to a lesser extent, other variants such as vector and graph data stores. However, several data storage options, such as relational or SQL, NoSQL, object, graph, vector, document, and more, are now available in the cloud.
In addition to the above, services that ingest, buffer, and route messages and events also offer temporary storage. You can apply functionless and native operations for many of these service operations, and this must be considered in your architectural thinking.
Storing and retrieving data
Many engineers have the preconceived mindset that a function is needed to perform data operations. With such thinking, functions creep in where they have no role. The two places where a function is commonly used to transport data are API gateway backends and workflow orchestrations.
As explained below, several ways exist to handle data operations without the help of functions.
Storing API request payload in a data table: Figure 4 shows a simple serverless pattern in which a function receives the API request payload, say, a new book title, and persists in a table. Most API gateways offer schema validation on the incoming data. Once validated, you can store the data from the API directly in the table using native service integration. Thus, the function in the middle becomes unnecessary.
Figure 4: Eliminating unnecessary functions in data operation
Like above, you can perform fetching data from data tables to send as API response payload without a function in the middle.
Atomic data counter operations without a function: Amazon DynamoDB, for example, has no built-in feature to generate unique sequence numbers like traditional relational databases. However, this can be accomplished with the atomic counter concept in DynamoDB, as shown in Figure 5. If you have a service that generates unique values for orders, customer registrations, site visitors, etc., this can be achieved between the API and a DynamoDB table.
Figure 5: Native service integration between API Gateway and DynamoDB table to generate sequence numbers
{
"TableName": "sequence-numbers",
"Key": {
"id": {
"S": "visitor"
}
},
"ExpressionAttributeValues": {
":val": {
"N": "1"
}
},
"UpdateExpression": "ADD sequence :val",
"ReturnValues": "UPDATED_NEW"
}
Sample VTL script to perform the atomic counter increment.
Buffering high-volume API requests: Another widespread use case is handling spikey API requests and processing them asynchronously, as shown in Figure 6. In such situations, it is vital to control the load to protect the downstream systems. Adding a queue behind the API can be a buffer to control the data flow. In this scenario, the data is stored before processing, known as the Storage First pattern. It is common to see this pattern implemented with other data stores.
Figure 6: API requests buffered in a queue for asynchronous processing
Data expiry
One traditional approach to purge expired data from databases is to run a task on a scheduler to query data with specific parameters and perform delete operations. Depending on the volume of data, such clean-up tasks can take minutes or hours to complete.
When engineers bring the above experience into serverless development, the apparent choice for querying and deleting data is with a suite of purpose-built functions. However, modern data stores offer automated data purging at no cost and without impacting performance. Opting for such native features avoids implementing functions.
For example, Amazon DynamoDB has the option to set time-to-live (TTL) for every data item (record) in a table. Though some deletes may take up to 48 hours, it is far more efficient than handling programmatically by yourself. Azure Cosmos DB also offers a similar TTL feature to remove unwanted data.
The most popular object store in the cloud, Amazon S3, supports data retention policies for data buckets. With data retention policies, you let S3 manage the lifecycle of data objects to either delete expired objects or move them to an archive or low-cost storage to comply with your business requirements.
Use a service’s native feature to perform data cleanup with every data store you work with. Beyond the benefits of dealing with fewer functions, this also brings sustainability benefits to your cloud operation. Though data is central to everything we do, not all data is valuable or remains valuable beyond a certain point. Hence, thinking about your data lifecycle is crucial to removing unwanted data and reducing compute storage, and network use.
Thinking beyond the CRUD Operations
An API implementation includes endpoints that perform typical operations, such as Creating, Reading, Updating, and Deleting (CRUD) data in a database system, aligning with the appropriate HTTP methods. Though every API contract and the invocation type - synchronous or asynchronous - can be categorized into one of those four distinct data operations, in modern distributed systems, the backend operations are not always strictly CRUD.
As you saw earlier, when a synchronous API fetches a new order number, it performs an atomic data operation. However, the behavior need not be the same in an asynchronous invocation. For example, when a customer places an order, one or more services, owned and operated by several teams from multiple business domains, collaborate to fulfill the request. In this case, not every activity within or across various services needs to be atomic.
From a modularity and extensibility point of view, a monolith function comprising complex logic interacting with several services to coordinate tasks is not an ideal solution. Service orchestration is the apt design pattern to consider in such scenarios. Instead of a function, a service workflow becomes the backend orchestrator, as shown in Figure 7. Cloud services such as AWS Step Functions and Azure Logic Apps are prominent workflow orchestrators available today.
Figure 7: A workflow that initiates async operations and handles an API request
Filtering and transforming events
The popular fully managed cloud services that ingest and deliver events offer native integration with many other services. Event filtering, transformation, archiving, and delivery to several destinations are some of the core features of these services that eliminate the need for custom functions.
Amazon EventBridge, Azure EventGrid, Google Cloud Pub/Sub, etc., are cloud services that provide event transport and act as event brokers to easily coordinate with multiple applications in a loosely coupled event-driven communication.
Low-code Integration Trade-Offs
One primary motivation for using cloud services is to delegate as much as possible to the cloud provider. In this regard, native service integration that reduces unnecessary function code must be a factor when architecting serverless and cloud applications.
However, while developing a functionless mindset, it is equally important to be aware of some limitations and trade-offs, as highlighted below.
- Most native service integration acts as a black box without visibility of the integration code at runtime. This can make investigating bugs challenging.
- In AWS, for example, integration code is written using Velocity Template Language (VTL). Because VTL's syntax differs from familiar programming languages, learning and becoming familiar with it takes time.
- Though API gateway and management services support native integration with several cloud services, each target service's integration format is not uniform.
- Knowing the differences in permitted data payload sizes between the source and target services is crucial to preventing processing failures.
- Like the payload size, the service timeout and throttling limits can also differ between the integrated services.
- Sometimes, you may require a function for nonfunctional operations, such as security checks on parts of the request context data.
Conclusion
"The first step toward change is awareness. The second step is acceptance". - Nathaniel Branden.
As an evolution of the cloud, serverless technology enables organizations to quickly add value by removing the unnecessary baggage of server and hardware management. However, while building applications using serverless technology, you must continue exploring ways to simplify your architecture and operational overheads.
In this article, capturing every use case that reduces the FaaS footprint is impossible. It intends to raise awareness. According to industry feedback, newly adopted serverless teams will likely make costly architectures and implementation mistakes. Though this is understandable to some extent, the motivation should be to avoid such errors by employing apt patterns and principles from the beginning.
One size does not fit all. The business domains and use cases differ from team to team. While implementing a pattern - functionless or another - you must first analyze its applicability. When you know the possibilities, you can take the necessary actions to simplify your architecture.