10 min read

The 12-Factor App: A Blueprint for Modern Web Applications

The 12-Factor App: A Blueprint for Modern Web Applications
Photo by Roman Mager / Unsplash

Introduction

Ever wondered why we have three separate check stages before our code reaches the end user? Why do we use package.json instead of just adding dependencies via a <script> tag in index.html?

Twelve-factor app is a methodology that consists of a set of rules, designed to make our web applications more scalable, portable, robust, and easier to deploy and maintain. Originally developed by engineers at Heroku, it has greatly influenced the way that most web applications are developed today.

Today, these rules feel natural to us. I, for instance, cannot imagine other way to develop an application today. Still, it is a good idea to get familiar with these rules and what they mean, so that we can better understand why we do things the way that we do them.


1. Codebase: One Codebase Tracked in Revision Control, Many Deploys

One codebase tracked in revision control, many deploys

An application should always be tracked in a version control system (mostly git in practice), in a repository.

codebase is any single repo (in a centralized revision control system like Subversion) or any set of repos that share a root commit (in a decentralized revision control system like Git).

A twelve-factor application should consist of a single codebase. If there are multiple codebases present, it's not an application, but a distributed system, although each component of this system can be an app that is compliant with the 12-factor methodology.

Although only one codebase per application is allowed, multiple deployments of a single application are allowed. Usually, this is implemented through having separate "production", "staging", and local developer environments. All of these environments share a single codebase, although their versions may vary.


2. Dependencies: Explicitly Declare and Isolate Dependencies

Explicitly declare and isolate dependencies

Let's imagine that we develop an application using Node.js. If we want our application to be a 12-factor one, we should be sure that it doesn't depend on any system-wide (or global) package. No implicit dependencies are allowed. All dependencies should be declared exhaustively in a package.json file. Furthermore, we should have some sort of isolation tool that makes sure that no system-wide package leaks into the application. In our case, this is achieved with the usage of npx or npm cli.

This approach significantly simplifies onboarding for new developers. A developer can clone the repository, install Node.js and npm, and then run a single deterministic command—npm install—to set up the environment with the exact dependencies required by the application.

Twelve-factor apps also avoid relying on the implicit existence of system tools like imagemagick, ffmpeg, or curl. Even though these tools may be available on many systems, their presence and version compatibility cannot be guaranteed across all deployment environments. If the application needs such tools, they should be included as dependencies in the project, either through npm packages that provide bindings or wrappers, or by vendoring binaries directly into the application’s build.


3. Config: Store Config in the Environment

Store config in the environment

In a 12-factor application, configuration should be separated from the other application code. Obviously, this raises a question: how do we differentiate between application code and config?

Generally speaking, config is a set of values that change between deployments. If a variable has different values on staging, prod, e.t.c. This variable is most definitely a part of the application's config; therefore, it should be separated from application code.

This separation between config and code is achieved via the use of environment variables.


4. Backing Services

Treat Backing Services as Attached Resources

The fourth principle of the 12-factor-app methodology is "Backing Services". It states that a twelve-factor app should treat backing services—such as databases, messaging systems, caching services, or external APIs—as attached resources. These services are external to the app and can be attached or detached at will. In this model, the app makes use of these resources via a URL or another locator, treating them as replaceable and not hard-coded or tightly integrated into the application codebase.

This separation means that switching from one backing service to another (e.g., from a local PostgreSQL database to a hosted one like Amazon RDS) should require minimal configuration changes and no code modification. By using environment variables to store service credentials and URLs, developers can swap services between different environments—like development, staging, and production—without rewriting the application. This promotes portability and makes the application more adaptable to infrastructure changes.


5. Build, Release, Run: Strictly Separate Build and Run Stages

This principle emphasizes the separation of an application’s lifecycle into three distinct stages: build, release, and run. The build stage is where the app’s source code is compiled and dependencies are resolved, resulting in a build artifact (like a binary or directory). The release stage combines this build artifact with configuration specific to a particular environment (e.g., environment variables or settings), producing a release. Finally, the run stage is when a release is executed in the target environment.

This separation ensures consistency and repeatability across deployments. A build should be reproducible and produce the same output every time, while releases allow you to apply environment-specific configurations without altering the build itself. By keeping these stages distinct, you can roll back to a previous release without rebuilding, or reuse a build across multiple environments (like staging and production) simply by applying a different configuration.

Following this principle helps prevent "it works on my machine" issues, since developers don't modify builds directly during testing or production. Instead, every change follows a formal process: a new build, then a new release, then a run. This structured approach enhances reliability, traceability, and simplifies debugging and deployment automation, which are all essential for robust, modern application delivery pipelines.


6. Processes

Execute the App as One or More Stateless Processes

The sixth principle of the 12-Factor App methodology is "Processes". It states that a twelve-factor app should execute as one or more stateless processes. These processes should not rely on any internal memory or file system state to function correctly. Instead, any data that needs to persist must be stored in a stateful backing service like a database or object store (e.g., Amazon S3). This ensures that processes can start, stop, and scale independently without causing data loss or inconsistency.

By keeping processes stateless, the application becomes more resilient and scalable. Statelessness allows for horizontal scaling—spinning up multiple instances of the same app to handle increased load—because no single process holds unique, irreplaceable state. If a process crashes or is replaced by another (common in cloud platforms), there’s no risk of losing critical data or session state, since all persistent state is stored externally and centrally accessible.

This principle also simplifies deployment and maintenance. It eliminates the need for shared disk access or sticky sessions, and aligns with modern cloud-native practices like container orchestration (e.g., Kubernetes). Ultimately, designing the app around stateless processes encourages modularity, easier fault recovery, and better alignment with distributed system architecture, all of which are essential for reliable and scalable services.


7. Port Binding: Export Services via Port Binding

The seventh principle of the 12-Factor App methodology is "Port Binding". It dictates that a twelve-factor app should be self-contained and expose services via a port, rather than relying on an external web server (like Apache or Nginx) to handle HTTP requests on its behalf. The application itself should bind to a port and listen for incoming requests directly, making it fully responsible for its network interface.

This design enables each app to operate independently as a service, which can be deployed in any environment without needing to be embedded in a larger server infrastructure. For example, a web app written in Node.js or Python Flask would start its own server and listen on a port defined by an environment variable (e.g., $PORT). This makes the app highly portable and easy to containerize, deploy, or run in platform-as-a-service (PaaS) environments like Heroku or Cloud Foundry.

Port binding also encourages clearer ownership of application behavior. Since the app is responsible for handling its own network traffic, developers have more control over routing, security, and performance. This principle aligns closely with microservice architectures, where each service is independently deployed, scaled, and accessed through well-defined interfaces—typically via HTTP or similar protocols over bound ports.


8. Concurrency

Scale Out via the Process Model

The eighth principle of the 12-Factor App methodology is "Concurrency". It emphasizes that an app should be designed to scale out via the process model, meaning it should handle increased workload by running multiple discrete, stateless processes rather than by increasing the complexity or size of a single process. This is typically achieved by dividing the app into types of processes—such as web processes, worker processes, or scheduled jobs—that can be scaled independently based on demand.

This approach encourages modularity and flexibility. For example, if an app’s web traffic grows but background job volume stays constant, only the web processes need to be scaled up. This division of labor makes it easier to isolate performance bottlenecks and optimize resources. It also aligns well with container orchestration platforms, which manage scaling individual process types dynamically based on resource usage or queue length.

Concurrency in this context is not just about multithreading or parallel processing within a single instance, but rather about horizontal scaling through multiple instances of lightweight, stateless processes. By embracing this model, a 12-factor app becomes more resilient, easier to monitor, and simpler to scale both vertically (with more resources) and horizontally (with more instances), supporting modern cloud infrastructure best practices.


9. Disposability

Maximize Robustness with Fast Startup and Graceful Shutdown

The ninth principle of the 12-Factor App methodology is "Disposability". It emphasizes that twelve-factor apps should be fast to start up and graceful to shut down. This ensures that processes can be quickly started or stopped without causing issues, which is crucial for rapid scaling, deploying new code, and recovering from failures in dynamic cloud environments.

Fast startup allows new instances of the app to come online quickly, which improves responsiveness to load spikes or infrastructure changes. Just as important, the app should shut down cleanly, meaning it should properly release resources—like database connections, message queues, or temporary files—and finish in-flight requests or jobs without data corruption. This makes the system more robust, especially in scenarios where processes are frequently cycled, such as rolling deployments or autoscaling events.

Disposability also plays a critical role in resilience and fault tolerance. If a process crashes or needs to be restarted, it should do so without requiring manual intervention or putting the system into an inconsistent state. This principle supports a more agile and stable deployment pipeline, helping developers deliver updates more safely and maintain operational reliability under varying loads or unexpected failures.


10. Dev/Prod Parity: Keep Development, Staging, and Production as Similar as Possible

The tenth principle of the 12-Factor App is about dev/prod parity, which means keeping the development, staging, and production environments as similar as possible. Differences between environments often lead to bugs that are hard to reproduce and fix, especially when something works locally but fails in production. The goal is to reduce these differences so that developers can have confidence that their code will behave the same way in all environments.

For modern web developers, this means using the same tools, services, and configurations across all stages of development. For example, if you're using PostgreSQL in production, you should also use PostgreSQL locally instead of something like SQLite. It also means automating deployments and syncing environment variables so that the behavior in staging mirrors what happens in production. This consistency helps catch problems earlier and reduces the “it works on my machine” issue.

Maintaining dev/prod parity also includes deploying often and integrating code frequently, which helps ensure that differences don’t have time to build up. When developers push code regularly and environments stay aligned, it becomes much easier to test and release confidently. The smaller the gap between writing code and seeing it run in production-like conditions, the more reliable and stable your app will be.


11. Logs: Treat Logs as Event Streams

The eleventh principle of the 12-Factor App is about logs, specifically treating logs as event streams. Instead of managing log files directly or writing logs to a specific location on the file system, an app should simply output its logs as a continuous stream to standard output (stdout). This lets the environment where the app runs decide what to do with the logs—whether that's writing them to a file, collecting them in a logging service, or analyzing them in real time.

For modern web developers, this means your application shouldn't be responsible for storing or rotating logs. You just log information to the console using tools like console.log, console.error, or similar logging libraries. In development, you can view logs in your terminal, while in production, the platform (like Vercel, Heroku, Docker, or Kubernetes) collects and routes those logs to external systems like Datadog, Loggly, or AWS CloudWatch for monitoring, alerting, or debugging.

By treating logs as streams, you make your app simpler and more flexible. It allows logs to be handled consistently across environments and avoids problems with file system access or disk space. It also supports real-time log analysis and alerting, helping teams respond quickly to issues in production. This principle reinforces the separation of concerns—your app generates logs, and the platform decides how to manage them.


12. Admin Processes: Run Admin/Management Tasks as One-off Processes

The twelfth and final principle of the 12-Factor App is about running admin or management tasks as one-off processes. These are tasks that you don’t run all the time, like database migrations, data cleanup scripts, or batch jobs. Instead of building special interfaces into the app for these tasks, you should run them in the same environment and with the same codebase as the rest of your app, but as standalone, short-lived processes.

For modern web developers, this means using the same deployment and execution context for admin tasks as for the web app itself. For example, if your app is containerized with Docker, you might run a command like docker-compose run app npm run migrate to apply database migrations. In platforms like Heroku, you'd use a one-off dyno to run a script. The key idea is that these tasks shouldn’t require setting up a different environment or writing one-off scripts on your local machine that don’t reflect production conditions.

This principle ensures consistency and avoids unexpected behavior caused by running admin tasks in an environment that differs from production. It also fits well with automated deployment pipelines and makes your operations more predictable and maintainable. By treating these tasks as disposable and isolated, you reduce risk and keep your app focused on running services, while still supporting important operational needs.


Conclusion

The 12-Factor App is a methodology for building modern, scalable, and maintainable web applications that are optimized for deployment in the cloud. It promotes best practices such as keeping code in version control, declaring dependencies explicitly, separating configuration from code, treating backing services as interchangeable resources, and ensuring dev/prod parity. By emphasizing stateless processes, disposability, and clear separation of build and run stages, it enables rapid deployment, easy scaling, and high portability across environments, making applications more robust, resilient, and manageable over time.