Fat Functions are an Anti-Pattern

Simon MacDonald’s avatar

by Simon MacDonald
on

complex code Photo by Markus Spiske on Unsplash

The debate over whether to use a single-purpose function vs. a fat lambda continues to this day, and in this post, we hope to convince you that Fat Lambda is truly an anti-pattern.

What is a Fat Lambda?

A fat lambda is a pattern in which you group related code together in one or more files. This lambda uses internal routing to decide what code to execute based on the invocation event.

The internal routing may be hardcoded:

Fat Lambda Example

exports.handler =  async function (event, context) {
    const { path, httpMethod: method  } = event
    if (path === '/apps' && method === 'GET') {
        // get apps
    }
    else if (path === '/apps' && method === 'POST') {
        // create app
    }
    else if (path.startsWith('/apps') && method === 'PUT') {
        // update app
    }
    else if (path.startsWith('/apps') === '/user'
             && method === 'DELETE') {
        // delete app
    }
}

or it could be using some middleware like Express:

Fat Lambda Express Version

let arc = require('@architect/functions')
let express = require('express')

let app = express()
app.post('/apps', (req, res) => // Add App)
app.get('/apps', (req, res)=> // List Apps)
app.put('/apps/:id', (req, res)=> // Update App)
app.delete('/apps/:id', (req, res)=> // Delete App)

exports.handler = arc.http.express(app)

Pros

  1. Related code is grouped together.
  2. Code is shared between event handlers.

Cons

  1. Cold start time increases for every extra byte of code you add to your lambda.
  2. Changes to the way you handle one event necessitate updating the handlers for all the events.
  3. Fat functions do not follow the single responsibility principle.
  4. Higher cognitive burden when you need to modify the function.
  5. Routing logic needs to be hardcoded or delegated to another package like express.

So What’s the Alternative?

Instead of creating a single function that handles multiple responsibilities, we have the single-purpose function pattern where many functions do only one thing.

Single Purpose Lambda

// add-app.js
exports.handler =  async function (event, context) {
    // add app
}
// get-app.js
exports.handler =  async function (event, context) {
    // get apps
}
// update-app.js
exports.handler =  async function (event, context) {
    // update app
}
// delete-app.js
exports.handler =  async function (event, context) {
    // delete app
}

Pros

  1. Easier to optimize the function to reduce cold start times.
  2. Smaller lambdas make it easier to write testable code.
  3. It follows the single responsibility principle.
  4. Lower cognitive load when making changes to individual lambda.
  5. Routing logic is offloaded to CloudFormation/API Gateway.

Cons

  1. Harder to share code between lambda.
  2. Maintaining multiple lambdas can be tiresome.

Evaluating Fat Functions vs. Single Purpose Functions

Fat Functions have a few pros over single-purpose functions, but I would argue that they do not override their negatives. Proponents of Fat Functions say that grouping related code and sharing code between event handlers is a significant advantage. However, using tools like Architect or Serverless makes managing many functions and sharing code much more straightforward.

Now on to the cons.

Cold starts

Lambda functions are dynamically provisioned. When you request a lambda, it runs through the following steps:

  1. Downloads your code
  2. Start new execution environment
  3. Execute initialization code
  4. Execute the handler code

The time in which it takes to complete the first three steps is what we consider the cold start penalty.

This gives us a couple of levers we can pull to reduce cold start times. The first is memory allocated to the lambda.

Execution time plotted against memory

What is somewhat surprising about our findings is that the amount of memory allocated to your lambda has a negligible impact on cold start times.

The other lever we can pull is code size. We were looking at cold start times using a package with a JavaScript function and several large binary files to increase the package size. None of these binary files are referenced from the JavaScript function, so they are never parsed/interpreted. They are merely included to increase the package size.

We can see that downloading and unzipping the package does not affect the cold start time.

Execution time plotted against package size

However, when we increase the code complexity so that the runtime environment loads and parses more JavaScript, we immediately impact cold start times. We start with a base JavaScript function that includes no external dependencies and then increase the amount of code parsed during the code initialization phase.

Execution time plotted against dependency size

Avoiding fat functions and sticking with single-purpose functions limit the cold start penalty when running your lambdas.

Reduce Update Thrashing

When you deploy a change to a single-purpose function, you only update the code for handling a single event type. However, with fat functions, you update the code that handles multiple event types, which increases the likelihood of introducing bugs in unrelated code paths. Therefore you have to do more testing to ensure you are not affecting your production environment.

Single Responsibility Principle

The single-responsibility principle (SRP) states that every module, class or function in a computer program should have responsibility over a single part of that program’s functionality, and it should encapsulate that part. All of that module, class or function’s services should be narrowly aligned with that responsibility.

Fat functions don’t adhere to the SRP. In the above example, our fat function is responsible for creating, updating, reading, and deleting our apps. It also means our fat function does not follow the principle of least privilege as it requires the ability to read, write and delete apps from our database.

Fat Lambda permissions

Decomposing the fat function into single-purpose functions follows SRP and allows us to assign the lowest level of permissions to each function.

Single Purpose Lambda permissions

Cognitive Burden

Proponents of fat functions state that grouping related code together reduces the cognitive burden of maintaining the code, whereas we would argue that it is precisely the opposite:

  1. It is harder to tell from the outside exactly what responsibilities a fat function has as they are a multitude. In contrast, suitably named single-purpose functions like get-apps or delete-apps-appID are pretty self-explanatory.
  2. Debugging fat functions because of their branching structure could take more effort to understand, while a single purpose function is relatively straightforward. Fat functions often make debugging more difficult by bundling back-end code, another anti-pattern we will address in a future post. Whereas single-purpose functions generally product stack traces that point exactly to the line of code where the error occurred.

In Conclusion

The allure of fat functions to quickly convert a node application into “serverless” application is appealing, but the work doesn’t stop there. If you are currently running a monolithic node application, you can wrap your app in the Architect Express Migration Helper and then decompose it into single-purpose functions. By breaking the app down into single-purpose functions over time, you will reap the most benefits.