Over a decade ago, Adam Wiggins from Heroku introduced the 12 Factor App, a set of best practices for building web applications. These principles aimed to create scalable and maintainable apps. But with technology evolving rapidly, it's important to see if these guidelines are still relevant today.
In this post, we'll review each of the 12 factors to see how they hold up today and suggest any needed updates. We'll also look at new tools and trends that support these principles, helping your applications stay current.
Codebase
One codebase tracked in revision control, many deploys
Today, Git is ubiquitous, and for many years, I haven’t seen Mercurial or Subversion mentioned in the original rule. It has become a standard. It looks like we can cross out this rule, but if you stop for a second, ask yourself about:
Configuration & secrets: discussed that in: Configuration management
Infrastructure: IaC is widely used in modern systems, but small manual changes are still too common.
Design: Many companies are switching to “design as code,” including diagrams, ADR, and analysis.
Models or Jupyter notebooks - Some companies are already using or trying MLOps because it has become a problem.
…
In my opinion, we made the first step by making Git a standard in source code, but we still have a lot to do in other areas. Probably EaC (Everything as Code) is coming 🙂
One more quote from this rule:
Multiple apps sharing the same code is a violation of twelve-factor. The solution here is to factor shared code into libraries which can be included through the dependency manager.
It looks almost the same as the “Don’t repeat yourself rule”, but as staff+ engineers, we already know that we cannot treat it literally/ Sometimes it is better to repeat code instead of creating a complex dependency chain or a huge utils library.
Dependencies
Explicitly declare and isolate dependencies
Today, almost every programming language has its own dependency declaration manifest, and we use it. Moreover, we found a way to manage and version system-wide packages or external tools in the operating system. We just use Docker, and some companies are starting to use it even during development using Dev Containers to ensure the same stack is used for development too.
Unfortunately, during the last 10 years, a new problem appeared: dependency chains. Fortunately, in software, we solved that using “lock” files (e.g., package-lock.json in npm, packages.lock.json in NuGet, composer.lock in PHP). Instead of committing the whole dependency chain, we can make our build repeatable in an easy way.
Is it enough? History shows that if we don't protect our dependency chain enough, even a lock file cannot save us. For example a man disrupted web development around the world by deleting 11 lines of code in 2016 (npm is already protecting this case) or Docker Hub limits can affect our deployments.
Again, we pushed boundaries a bit, but there is still a lot to do to ensure we can build and install our application in an idempotent way.
Config
Store config in the environment
We discussed this in our article Configuration management, so I won’t repeat myself here, except for one point: it is a bad idea to store secrets in env variables because almost all logging tools by default send environment variables to external tools.
Backing Services
Treat backing services as attached resources
When I first read this, it was already an obvious thing in a system with good design. In my opinion, it even evolved to the cloud-native approach. Instead of replacing one database with another unless the protocol is the same, like in the original quote: “A deploy of the twelve-factor app should be able to swap out a local MySQL database with one managed by a third party (such as Amazon RDS) without any changes to the app’s code.”, we are trying to make our apps in such a way that they can run in cloud or on-prem environments.
Of course, it requires some abstractions in the code (e.g., using ORM or queue abstractions) or in the vendor services (e.g., Amazon MQ has support for RabbitMQ and Active MQ, Azure Event Hub has support for Kafka clients).
To sum up, it is still a valid rule to consider in your system.
Build, Release, Run
Strictly separate build and run stages
What to talk about, everybody is doing that, isn't it? I was exactly thinking that way, but even last month I had a chance to work with the `gcloud app deploy` command and I was shocked. First of all, I did a deployment from the local version of my code, which was under the hood uploaded to the Google Cloud Storage bucket. Then GCP in a hidden way builds an app and releases it. In the end, when I tried to deploy it to a different environment, everything happened one more time. No artifacts at all. Moreover, if the build is not idempotent (see: dependencies) we could have 3 different versions of our app in 3 different environments even if we deploy from the same git commit.
To be honest, I shouldn’t be shocked because I recognize a few services which we can use in the same manner, including, for example, Azure Functions or GitHub Pages. Of course, it is a choice, not a must.
But this rule also mentioned the ability to rollback easily, which is usually a complex problem in distributed systems. We need to think about database changes, messages, or even configuration versioning.
Of course, if your CI/CD pipelines are mature enough, you can forget about all of the above, but it is not a one-day task, rather continuous work.
Processes
Execute the app as one or more stateless processes
In my perspective, the most important quote from this rule is: Twelve-factor processes are stateless and share-nothing. Any data that needs to persist must be stored in a stateful backing service, typically a database.
In my opinion, this rule is still valid, and I have to confess that too many times in my life I assumed that a sticky session would work without any problems. Fortunately, that assumption usually fails fast, and the stateless approach always wins.
Port Binding
Export services via port binding
Another rule that became a standard, and if it is a problem (e.g., PHP running inside Apache), we can wrap our application in Docker making it self-contained. That’s another rule which can explain why Docker became so popular. Especially that Docker allows us to run all apps in the same manner.
Concurrency
Scale out via the process model
Scaling out, or horizontal scaling, is the preferred solution. By following the guidelines of the Twelve-Factor App, your application can handle more requests simply by adding additional instances.
Is it the best option? In general, yes, but if we look at the Kubernetes development, you can see that VerticalPodAutoscaler was introduced. Because in my experience, especially when you can dynamically add resources, a mix of horizontal and vertical scaling is the best option.
At the end of the day, it looks like for cloud-native apps, fast scaling out is crucial, so the rule is still up to date.
Disposability
Maximize robustness with fast startup and graceful shutdown
Each time in my life when I didn’t implement graceful shutdown, I regretted it, so I fully endorse this rule based on my experience. Especially when queues were involved.
How fast is fast enough? It depends, so let’s stop it here. The rule, in my opinion, is valid, and we should consider it to avoid problems.
Dev/Prod Parity
Keep development, staging, and production as similar as possible
Of course, it is almost impossible to have identical environments. Still, these days it is much easier to have environments very similar, especially when we use IaC tools and cloud.
Again, it may be easier to use SQLite instead of Postgres, but at the end of the day, there are so many tools, and our machines are so powerful, or we can use cloud instead, that it looks like we just don’t have to optimize our local environments.
Logs
Treat logs as event streams
A rule that, in my opinion, should evolve. Today we probably should rename it to observability and extend it with basic & advanced practices to make it possible to find out what’s going on in our application. So instead of only logs, I would also add tracing and metrics.
Admin Processes
Run admin/management tasks as one-off processes
I already mentioned IaC in this post, so if we started to automate even infrastructure, it is difficult for me to argue that this rule is not valid. Especially that most examples were about database migrations.
Still valid, especially that instead of a lot of organizations trying to use CI/CD, each manual change can be just dangerous.
Summary
The 12 Factor App principles, introduced over a decade ago, have aged remarkably well. While some practices have evolved with advancements in technology, the core principles remain relevant. Modern tools and methodologies, such as Git, Docker, and IaC, have enhanced our ability to implement these principles effectively. However, areas like dependency management, configuration, and observability still require continuous improvement. Overall, the 12 Factor App provides a solid foundation for building robust, scalable, and maintainable applications.
Let's continue to refine and adapt these principles to meet the demands of today's software development landscape. If you have any comments or insights, please share them below. It would be great to discuss and learn together! 🙂