The 12 Factor App: an updated guide
A review of the 12 Factor App methodology for building SaaS apps. Their original formulation, and how they have evolved.
Hi Friends,
Welcome to the 111th issue of the Polymathic Engineer newsletter.
The Twelve-Factor App methodology is an approach for building software-as-a-service apps, which Adam Wiggins, the co-founder of Heroku, introduced.
This week, we talk about what these factors are and see how they have changed since then. Some of them are now a status quo, but we can still learn something from them and understand how they upset things the way they were before.
1. Codebase
The first factor stresses how important it is to keep applications in a single codebase that is organized in a version control system. Even though it was first used long ago, this idea is still very important in modern software development.
It may be hard to believe now, but code versions weren't always the norm. Version control systems were just starting to become popular when this advice was written. Many developers remember how hard it was to deal with systems like Subversion in the early days of their careers.
Since then, there have been significant changes in the way code repositories and apps work together. Today, everything from testing to deployment is often part of the same codebase.
However, there is still the open point of whether to use a mono-repo or multi-repo strategy. Managing multiple repositories can be a significant overhead in a world of microservices.
This has made a lot of teams rethink how they handle their codebases, weighing the pros and cons of using a single repository against the advantages of using multiple ones.
2. Dependencies
Dependencies are other software needed to run an application, and the second factor is how we should handle them. It states that we should clearly declare and isolate all the things our app needs to run properly.
While in the past it was a nightmare to set up systems and deal with different versions of libraries and operating systems, today this is a solved problem. Different types of files list exactly what our app needs and help us rebuild our application in a consistent and correct way.
But it doesn't stop there. Build tools such as Bazel has taken things even further. They give developers powerful ways to make releases that can be reproduced precisely. And thanks to containerization, now we can ship simple images that package just what we need for production.
The main takeaway here is that we only keep code that is unique to our app in our source control. Everything else, like Node.js packages, Java .jar files, or .NET DLLs, should be referenced in a dependencies manifest. This manifest is then loaded into memory when we develop, test, or run our app in production.
This approach keeps things neat, easy to handle, and much simpler to change when needed since we don't have to store external stuff alongside our source code in the repository.
3. Configuration
The third factor is about how configuration information is used within an application and tells us to keep our configuration separate from our code.
But what exactly counts as configuration? We're talking about things such as database connection details, credentials for external services, or even simple flags like if we are in debug mode.
There are several reasons to manage these settings outside of the codebase. First, it gives us more flexibility because we don't have to change our code to use our app in different environments. We only need to change how it is set up.
Second, it's safer. Our codebase doesn't have sensitive data like API keys or database passwords that anyone could see by mistake.
One common way to do this is to use environment variables, settings that live outside our app but are easy for our code to access while it's running.
Another popular method is using configuration files that are separate from our main codebase. A lot of frameworks and languages have .properties files or .yml files that can be used for this.
4. Backing Services
Backing services are extra resources an app connects to over a network and needs to run correctly. This could be things like databases, email servers, or even third-party services like Amazon S3 or a payment gateway.
The key idea here is that all of these services should be treated the same way. Whether they are run by us or someone else, we should be able to swap them without changing our code.
In practice, this often means using connection details like URLs in our configuration to specify how to reach these services. Our application code then uses these details to connect without knowing the specifics of where the service is or how it's run.
This is important because it gives flexibility. If we decide to change database provider or switch to a different email service, we shouldn't have to rewrite significant parts of our app. We just need to update the configuration that tells our app how to connect to these services.
This approach also makes an app more resilient. If we can think of these services as resources that can be added to or taken away as required, we can move to a backup service more easily if another goes down.
5. Build, Release, Run
When we take our code from development to production, we need three stages: Build, Release, and Run.