getting to know the twelve factors
As raised in the previous post, scaling a system is not simply deploying more machines or splitting services without criteria. Real scalability requires structure, strategy, planning and good practices.
One of those approaches is the twelve-factor app methodology. It proposes a set of practices for building modern, portable and resilient applications, especially in cloud environments. The 12 factors are not a silver bullet, but they work as a practical guide that can be applied to well-built monoliths as much as to distributed architectures.
Following the 12 factors is part of preparing a system for scale, and I recommend following them even if you do not have millions of users yet. It is a way of making sure that when growth comes you will not have to rewrite everything from scratch, buy new fire extinguishers for your self-hosted mac mini cluster running proxmox, or pray that your application stops getting new customers so you can turn their money down.
A good share of the headaches developers face day to day (environment variables, broken deploys, logs that vanish, badly resolved dependencies, bugs you cannot reproduce) can be avoided by following these practices from the start.
So let us go through each of the 12 factors with practical examples and a critical eye:
What actually makes sense to adopt today? What can be ignored or postponed? How do you apply these concepts in real environments where we do not always have the ideal infrastructure?
Part 1 was about understanding when and why to scale. This one is about how to prepare to scale properly.
1. codebase
One codebase tracked in revision control, many deploys
Just use git, together with semver and either git flow or trunk based development.
2. dependencies
Explicitly declare and isolate dependencies
Use a package manager and keep only its lockfile as the source of truth. Here that means pnpm-lock.yaml, which prevents some future developer from hitting the "works on my machine" problem and having to hunt down which dependencies are missing or which version actually works in your application.
3. config
Store config in the environment
Everything that changes between environments (production, staging, local) belongs in environment variables, never hardcoded in the source. If you want to be a little more careful, you can use something like AWS Secrets Manager.
4. backing services
Treat backing services as attached resources
Databases, cache (Redis), queues (RabbitMQ): all of that is external and should be treated as a service, swappable for another option and preferably without coupling in the implementation. This is the point where some developers get lost in code architecture questions, thinking this is about creating abstractions.
5. build, release, run
Strictly separate build and run stages
The deploy process should have multiple stages:
build(compile, install dependencies)test(run the automated tests)release(combine the build with the configuration)run(execute the code)
This separation makes rollbacks easier and guarantees reproducibility. Each stage can surface a problem in the process, which helps prevent production systems from going down with the application offline and no estimate for its return.
6. processes
Execute the app as one or more stateless processes
Avoid keeping local state (such as user sessions) in the application's memory. That is what allows horizontal scaling, and we talked about it in the previous post.
7. port binding
Export services via port binding
Your app should expose a port and run on its own. That makes it easy to deploy anywhere, from Kubernetes to Heroku.
8. concurrency
Scale out via the process model
This is where horizontal scaling is suggested, where scaling means starting new instances of the same process. Well-designed systems should be able to scale horizontally without trouble, though there is some debate here.
9. disposability
Maximise robustness with fast startup and graceful shutdown
Systems should start and stop quickly. That makes deploys, restarts and resilience easier. Avoid slow shutdown procedures. A good suggestion for this factor is using containers: Docker, Compose, or other tools that replicate your production environment as closely as possible.
10. dev/prod parity
Keep development, staging and production as similar as possible
Self-explanatory: it reduces how hard bugs are to reproduce.
11. logs
Treat logs as event streams
Logs should be emitted to stdout/stderr, never written to local files.
Collection should be handled by external systems, which reinforces the idea behind the other factors above.
It is also worth standardising the log structure depending on which system will consume it; that pays off later.
12. admin processes
Run admin/management tasks as one-off processes
Migration scripts, data cleanup and maintenance should run in the same environment as the application, with access to the same config. They can be scheduled, used as background jobs, or even wired in to run during container startup.