disclaimer: the site is being internationalized. you may still come across typos or untranslated text.

scalability in action - part 2: the twelve factors

May 23, 2025

read in portuguese ›

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.