The Trifecta of Stupidity

Lambdas are cheap, poor decisions aren't

Since rolling out Lambda in 2014, engineers have found plentiful ways to butcher and massacre one of AWS's core services in new and interesting ways.

The Trifecta of Stupidity is a description of a person(s) who clearly embraces one or multiple of these three traits;

  • Willfully Ignorant
  • Happily Misinformed
  • Painfully Unreasonable

The Basic Laws of Human Stupidity

In Carlo Cipolla's 1976 essay titled "The Basic Laws of Human Stupidity", Cipolla stated there are five foundational pillars to human stupidity. For brevities sake, we're only going to focus on three of them:

  • The probability that a certain person (will) be stupid is independent of any other characteristic of that person.
  • A stupid person is a person who causes losses to another person or to a group of persons while himself deriving no gain and even possibly incurring losses.
  • Non-stupid people always underestimate the damaging power of stupid individuals.

As engineers, inherently we aren't stupid. We do however commit stupid acts and temporarily embrace what it means to be stupid.

In my view, this can be attributed to three factors:

  • Willful Ignorance
  • Being Happily Misinformed
  • Acting painfully unreasonable

The result of this abomination of nature is the Trifecta of Stupidity.

Engineers who embrace this development philosophy, will create a slow monolithic system thats mere existence is a crime against nature.

The History of λ

Lambda calculus was introduced by mathematician Alonzo Church in the 1930's, as part of an investigation into the foundations of mathematics.

It's a formal system in mathematical logic for expressing computation in mathematical notation.

Lambda calculus provides some pretty simple semantics for formally declaring properties of computation.

It treats everything as an anonymous function e.g is not valid, and most importantly it only allows the use of a single input

Keep a note of this, it's going to be important later...

In lambda calculus, a abstraction roughly represents the concept of defining anonymous functions in most modern day programming languages.

We can define a minimal lambda abstraction as the expression

Where is the parameter, and is the resulting term.

Stripping back all the lambda notation, we could even simplify that further and just write the basic mathematical notation for it

Relating this to a modern day language such as TypeScript. We could express this using ES6 arrow function syntax.

const expression: y => (x) => {}

So, you may be asking. Why am I banging on about the formal standards for expressing computation in mathematical notion.

It's for one simple reason...

Tesler's Law

The law of conservation of complexity was theorised by Larry Tesler, who in the mid 1980's realised that the way that users interact with applications was just as important as the application itself. On a surface level, we could just palm this off and say as long as the UI is simple the end user will interact with the system and achieve the level of complexity they desire.

However digging deeper, Tesler uncovered one of the most foundational truths about software development.

Every application has an inherent amount of complexity that cannot be hidden nor removed. It must be dealt with, either in development or through user interaction.

Although i'm paraphrasing here. Tesler argues that, in most cases engineers should spend time reducing the complexity of an application. Instead of forcing the user to confront it head-on, charging into battle with Fortunate Son playing in the background.

One often overlooked aspect of this law is that by simplifying the system, it will not and can not reduce the overall complexity. It's simply moved to the user, who must bare the brunt of this complexity.

The easiest way as engineers we can reduce complexity in systems, is by not thinking we're the next Linus Torvold or Edsger Dijkstra and just to stop and think to take the work you're doing and breaking it down into it's constituent parts.

On Lambda, an essay...

Picture this, you've been bestowed the honour of building the next business-critical feature for your companies hyper-scaleable, cutting-edge, multi-tenanted, SaaS internal-internal tool builder implementing next-generation blockchain technology powered by the latest developments in AI.

One of the requirements you've been given in the 17 page annex to the specifications document is that the component must be built using serverless technologies, so you collectively decide on AWS Lambda.

For this sprint at least you're focusing on the User Service. The new User Service needs a way to be able to create users, update their personal information, allow them to enable 2FA and finally be able to easily retrieve a user.

Management decided deleting users is out of scope as no one in there right mind would not want to use the service.

So you and your team break away and come up with 3 potential solutions.

The Ephemeral Server

One solution thats presented back to the team is a hybrid between a Docker container and a bad hangover.

A Single lambda that handles all of the requirements, you can add new users, retrieve them and modify any data you so wish.

The engineer who wrote it is insistent it's the best solution. It encompasses every requirement, you have a centralised place for logging and it just works.

They even boast about how they've created a polymorphic input validation strategy that will correctly validate against any payload you throw at it.

Then you look at the code...

type Response = CreateUserWithoutTwoFactorAuthenticationResponse 
              | CreateUserWithTwoFactorAuthResponse 
			  | RetrieveUserByInternalPrincipalInvocationResponse
			  | RetrieveUserByAuthenticatedRequestInvocationResponse
			  | SensitiveInformationStripped<GenericUserRetrievalResponse>
			  | Partial<GenericUserRetrievalResponse> & UpdatedUserFields;

export const handler = async (event: unknown) => Promise<Response> {
  const input = LambdaInputValidationSchema.parse(event);
  const { type, data } = input;

  switch (type) {
	  const userCreationResponse = await handleUserCreation(
			parseInt(8.2e % 2), 
			Math.floor(Math.random() * 7.289) / 1000 * 2)
		return responseFactoryBuilderService()
				mapBeforeChristDateFormattingToAfterDeath('2023 AD'), 
			.persist('DYNAMODB', true, process.env.AWS_ACCESS_KEY_ID)
			.save(undefined, null);
		return responseFactoryBuilder()
				mapBeforeChristDateFormattingToAfterDeath('2023 AD'), 
	  else {
		throw new GenericHandlerErrorFactory()

  // This is hurting to write you get the picture...

The Decoupled Facade

Another solution presented to you, at a first glance looks good. It's comprised of 5 Lambdas

  • create-user
  • retrieve-user
  • enable-user-2fa
  • disable-user-2fa
  • update-user

The engineer who wrote it is open to suggestions on how to improve it but is again insistent it's the best solution. They're incredibly proud of the fancy architectural patterns that have been implemented within it.

It's got circuit breakers, saga workflows for failing backwards and ensuring a clean errored state, event source mapping for handling any unexpected errors that may pop up. You name it, it's been implemented.

You delve into the code and discover something... It has separate lambdas to handle the different events, but it shares the same underlying code for most of the functionality. The engineer argues it helps increase development velocity by functionally grouping related code making it easier to see the bigger picture.

You ask yourself the question, sure this is elegant now but how big is too big?

The Paleolithic Lambda

The last solution presented was seemingly very basic, in fact it is.

Similar to The Decoupled Facade, it has the exact the same lambdas (the developers paired on the design, but one didn't follow the design ticket and decided to just ignore it and to raw-dog it instead).

However the solution has a completely different internal structure. Virtually none of the code is shared between lambdas, apart from the types and a couple shared utility functions.

Every lambda appears more like a shell script, albeit written in TypeScript.

Your first thought is, jesus get with the times, we're a cloud native company why aren't we leveraging AWS? We're creating cutting-edge, multi-tenanted, internal-internal tools that implement next-generation blockchain technology powered by the latest developments in AI, the least you could do is add an event source mapping.

Occam's Razor

Ultimately as engineers, we're going to try and overcomplicate the work in pursuit of 'clean' code but we often forget that the simplest solution is more often then not the correct one.

Thing goes in, thing comes out

The modern day luxuries we enjoy and take for granted in the nerds haven that is AWS, come at a steep cost.

Time to architect a new system what components am I going to need; Hmm, let me think. We're gonna need some serverless compute so lets chuck in a Lambda, maybe a DynamoDB table too while we're at it. Oh how could I forget, I nearly forgot to add the f&#%!ng supercomputer into my metaphysical shopping basket.

It's so easy to overcomplicate things with the 200+ different weird and wonderful services AWS provides but we always seem to forget the core tenants of one of AWS's most popular services.

If we're going by the simple nature and elegance of the bare bones, mathematical definition for what a λ actually is.

Or in other words thing goes in, thing comes out

For crying out loud, it's in the name.