AWS Lambda – Lean Single Purpose or Monolithic Function

Small functions that do one thing only are the indisputable general best practice. But more monolithic functions can sometimes be a better fit for your solution. Friday, February 15, 2019

Small functions that do one thing only are the indisputable general best practice. But what "one thing only" means is open for discussion. Also "general" best practice does not mean it is best for your use case. I know this very debatable subject well and there is clearly no single simple answer for this. We all come with a set of predispositions based on our previous experiences on projects, which of course variates drastically by all factors — size, complexity, technology, and management, for example.

A lean single purpose function means following a single responsibility principle (SRP). Your function does one thing only. You divide your system into microservices or even further to nanoservices. You have small and autonomous, independent units that can be individually managed, monitored, and scaled. That does not mean you should separate a system to the smallest possible piece and functions and then call one function from another function. Calling functions from another function is valid and preferred in some patterns like fan-out. But in other cases, it is redundant and you are just paying double for functions — once for calling a function that is waiting and another for a function that is executing. You make debugging more complex and remove the value of the isolation of your functions.

A lean single purpose function can be individually managed, monitored, and scaled

Monolithic function means it does more than one thing. This could mean serving several HTTP endpoints and methods or different types of processing. In the beginning of the handler, you identify what kind of request it is and then process it accordingly. In its extreme, it could mean handling whole web applications, so you have only one function per application.

Here are arguments for lean single purpose functions that are considered best practice:

  • Separate of concerns

    Each function addresses its separate problem. Functions are loosely coupled. They can work independently. They know as little about the other services as possible. Change to service should not have any effect on others. Only the communication interface is common and well defined. That presents an opportunity to modify each function independently, optimize, and scale.

  • Fast cold start

    Functions are not blotted by unnecessary libraries and code so they start faster.

  • More options for optimization and security

    Each function can have its own memory settings that also define processor and network resources and, of course, price. For each function, we can set indicial timeouts and security settings. With all of this, we can optimize a need for resources for better scale with minimum price.

Reasons for monolithic function:

  • Better for high cohesion

    If services communicate with each other frequently, share a lot of common logic, depend deeply on each other, have to be deployed together, then maybe they should be one service after all. With coupling, you avoid unnecessary traffic between functions.

  • Faster deployment

    Bundling and packing functions and associated libraries takes time. If you commonly deploy functions together, it is much faster if you deploy only one function compared to many. Shared libraries and modules can be deployed as Layers, so this can partly speed up deployment of multiple functions.

  • Smaller cumulative cold start

    If we call function sequentially in a low traffic environment, we could hit a lot of cold starts that add up. This is not the case if you combine functions into one. But this argument is only valid for low traffic and can be mitigated by the plugin lamda serverless-plugin-warmup. If you are concerned by a cold start, consider changing language, optimize code, and try not to use VPC. Be aware that you pay for a cold start when the need for concurrency rises, not just when you get a first request.

  • Resolving CloudFormation limit of 200 resources per stack

    When deploying a serverless solution with CloudFormation, you have 200 resources per stack limitation. Serverless Framework also uses CloudFormation and 200 seems high, but the resource takes into account the function version, permission, API Gateway path and method, IAM roles, and DynamoDB tables. With each HTTP event, you end up creating six or more resources. So, you can easily hit the limit with 30–35 HTTP functions or less. You can read more about this problem and how to resolve it here.

  • Faster development and debugging

    When you develop in your favorite code editor or IDE and start debugger in a much simpler to debug bigger Lambda and just flow though functions and have clear call stack and context, it is much harder if functions are separate and in fact independent programs.

CloudFormation limit of 200 resources you can easily hit with 30–35 HTTP functions or less

Cannot decide what is best for you? As IT professionals, we tend to overengineer things and sometimes quick and dirty is the best solution for some scenarios. You can always refactor things later. Basic logic should be separate modules anyway, regardless if they end up in some Lambda.

How many functions are needed per project is another very opinioned decision. One project for microservice that is autonomous and independent and contains few functions is a good rule of the thumb to start with.

 

Technical Details Behind Each Decision

 

Lean Single Purpose Functions

When developing single purpose HTTP API and offering it via an API Gateway, you have options on how you receive input values:

A) Input processed by API Gateway before passing to Lambda

This is the default option. API Gateway parse request with mapping templates and send to Lambda. After Lambda processes the request, API Gateway forms a response based on mapping templates. API Gateway uses a Velocity Template Language (VTL) engine to process body mapping templates.

This integration requires a lot of work setting up. The benefit is it decouples the API Gateway from Lambda. Additionally, the schema of the service can be easily exported as Swagger specification, which simplify documentation.

API Gateway schema can be exported as Swagger

B) Lambda Proxy integration

This is the default option if you create Lambda with Serverless Framework. Request is forwarded straight to the Lambda and the response is sent from Lambda. No modifications to the request and response are done by the API Gateway. This is in most cases preferred by developers, because they can manage everything in code, including response HTTP status codes.

Micro Moonlit Function

If you are offering HTTP API via AWS API Gateway and want to process multiple paths, you should configure API Gateway accordingly.

This is a configuration for Serverless Framework.

functions:
  app:
    handler: handler.main
    events:
      - http: ANY /
      - http: 'ANY {proxy+}'

Then you need some kind of routing. A simple if statement can be a good solution.

TypeScript sample:

import { APIGatewayProxyHandler } from 'aws-lambda';

export const main: APIGatewayProxyHandler = async (event, context) => {
  const path = event.path;
  const method = event.httpMethod;

  if (path === '/post') {
    if (method === 'GET') {
      //...
    } else if (method === 'POST') {
      //...
    }
    //...
  } else if (path === '/comment') {
    if (method === 'GET') {
      //...
    } else if (method === 'POST') {
      //...
    }
    //...
  }
};

If your routing is more complex, you can use lambda-router.

For complex routing in Lambda use lambda-router

If your logic is more complex and divided into many classes and with middleware for security and similar purposes, you probably need a dependency injection library. Java and .NET developers cannot live without it. InversifyJs for Node.js and TypeScript works great with Lambda.

InversifyJs is great dependency injection library for AWS Lambda

Heavy Monoliths Function

You can tread your function as a fully functional server or, even better, a cluster of servers, which essentially it is. You can host your Koa, Express etc. application on it. You need a proxy library like aws-serverless-express or serverless-http that transforms a Lambda request to a classical Node.js request.

Of course, this is not the best solution. Apart from being heavy and against best practices, it presents an overhead of processing incoming requests twice. Once in a proxy library and once in Koa.js, Express. Koa.js, Express, etc. are quite heavy frameworks for Lambda, which is meant to have zero or just a few lightweight libraries. But it is a way to host your legacy solution in Lambda. Or you can build a universal application that can run either on a server or in Lambda. In this case, it would be better to separate logic to separate modules and use a lightweight web framework for your serverless applications like lambda-api by the awesome Jeremy Daly when hosting in Lambda. It is like web frameworks Express.js, but is significantly stripped down to maximize performance with Lambda's stateless, single run executions. When you host on servers, you use Koa.js or Express and then call shared modules.

lambda-api by @jeremy_daly is great lightweight web framework for your serverless applications