At CloudZero, we love working with Serverless Systems for all the reasons, including but not limited to focus on business value, ease of responsibility segregation, and shift in engineering focus. When I’m ready to write and execute code in a Serverless System, I reach for a FaaS platform like AWS Lambda. Tools like Serverless Framework and AWS SAM CLI help prime the pump1, but I’m still not immediately writing business value code. Instead, I find myself poring over inconsistent, incomplete, often incomprehensible documentation to understand function execution environment and input/output constraints. Every company using AWS Lambda eventually comes up with conventions to ensure code consistency and to take this cognitive load off each developer.
This is a story of how our company (and yours too probably) created those conventions, how to create better abstractions, and how we hope pyfaaster can help you deliver business value faster.
First, we noticed each Event Handler function had the same first and last 3 lines of code. It’s happening. Boilerplate! I hate boilerplate code. I want to excise it from the universe, banishing it back to the nether regions of hell from whence it came. We all feel this way. It is the low hanging fruit1 of code reviews and static analysis tools everywhere: “Code Duplication”. It’s the first one we attack because it seems like the easiest2. However, after 10 minutes, we find ourselves standing in a open field west of a white house, with a boarded front door; that is, halfway down the road to crappy indirection.
Our first attempt is the “cut/paste abstraction”. We see the same 10 lines of code in 3 places. Easy. Cut one of them out and paste it into a common module. For example, we often subscribe AWS Lambdas to API Gateway events. In short order, the following boilerplate pops up:
We get out our scissors, create a new common.py module, and replace the call site with our new utility function:
Voila! We have a nice cut/paste abstraction that saves us 3+ LOC in each handler method.
Building Better Abstractions
I often start with this sort of abstraction. I notice two problems: usage and generalization/composition. Usage peeks in when another developer asks, “How and When do I use common.apig_response?” “Oh”, I reply, “You of course should use it here and here, but definitely not there”. This is the beginning of a convention, an assumption that is not hidden away by the abstraction. Well documented and understood conventions are often more maintainable than boilerplate, but we can do better. We’ll come back to how in a few paragraphs. For now, let’s focus on generalization and composition. We notice generalization quickly when we need to change the common.apig_response function in order to service two clients.
For example, what if we need a different statusCode? For example 'statusCode': 500 when our domain code throws an exception? Our first thought might be to add statusCode as an argument to our common.apig_response function; however, I always pause before adding arguments to a function. We strive for our functions to do one thing only. Additional arguments trigger our Smell-O-Scope. How should we proceed? It turns out that the common.apig_response utility function itself is not the great breakthrough; the general purpose data structure common.apig_response introduced as its response type, i.e. a dict, is.
We first start by using ensuring our utility functions return the same dict data structure:
We can now merge the return values of the two utility functions. Because we’ve chosen to use Python’s dict, we have generalized abstraction, i.e one that operates on a common data structure. We introduced boilerplate back into our code, however once we ensure Event Handlers have a Composable Interface this will fall back out. The arguments and return values for Python Event Handlers are specified in the AWS Docs. We can use the arguments (event, context) as specified, but "Optionally, the handler can return a value" isn't helpful for code composition. Let's impose that the return type of our Lambda Handlers must be a dict. We can then compose functions appropriately. Let's explicitly type hint our Event Handler using PEP 0484 syntax:
Finally, we’re ready to put together a generalized composable abstraction that we could compose by chaining function calls:
Or we can sprinkle some sugar on these with one of my favorite Python constructs, the decorator. This now leaves us with a tiny Event Handler with minimal boilerplate:
I promised to come back to usage. First, a bit on usage direction.
Is my code the leaves of the tree or is my code the trunk of the tree? If I am using a framework, the framework code is calling my code: the framework is the trunk of the tree. If I am using a library, my code is calling the library code: my code is the trunk of the tree. All things being equal, I prefer to write and use libraries because I like my code to be the trunk of the application, which leads to the Effort/Scale trade-off:
Though libraries cost more work up-front, they come with benefits like reduced cognitive load through better understanding (what is my code doing vs their code) and more maintainability as the application scales to yuge5.
Although Python decorators are often used by Frameworks like Django or the popular Chalice microframework, we prefer to keep the dark magic to a minimum. Our preferred decorators are simple higher order functions that either (1) transform arguments and return values, or (2) isolate side-effects from the business logic code6.
To summarize usage, we want you to call us when you need to transform data or isolate side-effects.
We walked this abstraction path while building over 100 lambda functions for CloudZero’s systems. Over time we collected those abstractions into pyfaaster, a Python library that helps us write less boilerplate, deliver business value faster, and practice building generalized composable abstractions.
Though Serverless Systems help you deliver business value faster; you still need to create good abstractions to avoid repetitive error-prone boilerplate code.
We all start with cut/paste abstractions. Don’t stop there. Aim for Generalized Composable Abstractions.
Think about usage. Is your code the trunk or the leaves?
pyfaaster is our first attempt at a Library of Generalized Composable Abstractions in Python for AWS Lambda. We hope it helps you deliver more value to your customers.
At CloudZero, we name all our conference rooms business jargon.