The Twelve-Factor App
The Twelve-Factor App
In 2012, engineers at Heroku distilled years of experience operating large-scale SaaS applications into a manifesto: the Twelve-Factor App. The methodology defines twelve practices that, when followed together, produce software that is portable, scalable, and operationally predictable. Every factor ultimately answers a single question: how do we make this service safe to run and easy to reason about at 3 a.m. during an incident?
This lesson walks through each factor, explains the production rationale behind it, and shows what compliance and non-compliance look like in real systems. The DORA metrics from Lesson 5 measure outcomes; twelve-factor is the engineering discipline that makes those outcomes achievable.
Factor I — Codebase: One Repo, Many Deploys
A twelve-factor app is tracked in a single version-control repository. That one codebase is deployed to multiple environments — development, staging, production — from the same commit. Multiple codebases means a distributed system, not an app. Shared libraries belong in a dependency manager, not checked into each service.
Failure mode: Teams that maintain a separate "prod branch" with hotfixes that never get merged back accumulate divergence. Within weeks no one knows what is actually running in production.
Factor II — Dependencies: Explicitly Declare and Isolate
Never rely on implicit system-wide packages. Declare all dependencies in a manifest (package.json, requirements.txt, go.mod, Gemfile) and use an isolation mechanism (venv, node_modules, Go modules, Bundler) so a fresh checkout plus one install command produces a complete, runnable environment.
curl | bash install scripts for system tools inside your app's runtime. If your Dockerfile runs apt-get install curl and then curls an installer, you have an implicit, unversioned dependency that will silently break when the upstream installer changes.Factor III — Config: Store in the Environment
Everything that varies between deploys — database URLs, API keys, feature flags, log levels — must live in environment variables, not in code or config files committed to the repo. A litmus test: could you open-source the codebase right now without exposing a secret? If no, config has leaked into code.
Factor IV — Backing Services: Treat as Attached Resources
Databases, queues, caches, SMTP servers — treat them all as attached resources accessed via a URL in the environment. Local MySQL and a hosted RDS instance are interchangeable from the app's perspective. Swapping from a local Redis to ElastiCache should require only an environment variable change, no code change.
Factor V — Build, Release, Run: Strict Separation
Transform a codebase into a running deployment through three separate, non-reversible stages:
- Build: compile, fetch dependencies, produce an artifact (Docker image, JAR, binary).
- Release: combine the build artifact with deploy-time config. Every release gets an immutable ID.
- Run: launch processes from the release in the execution environment.
Code never changes at runtime. The "deploy then edit config on the server" pattern violates this factor and makes rollbacks impossible to reason about.
Factor VI — Processes: Execute as Stateless, Share-Nothing Processes
App processes are stateless and share nothing. Any data that must persist lives in a backing service — the database or cache. In-memory session state, local filesystem uploads, and in-process caches all violate this factor. When a load balancer routes the next request to a different process instance, all sticky state is lost.
Real-world implication: sticky sessions are an anti-pattern. File uploads written to /tmp on one pod will not be visible to another. Move sessions to Redis/DynamoDB and uploads to object storage (S3, GCS) from day one.
Factor VII — Port Binding: Export Services via Port Binding
The app is self-contained and exports HTTP (or any protocol) by binding to a port. It does not rely on a runtime injection (e.g., Apache mod_php). A Node app runs its own HTTP server; a Python app runs Gunicorn internally. This makes the app trivially composable: it becomes a backing service to any other app.
Factor VIII — Concurrency: Scale Out via the Process Model
Scale horizontally by running more processes, not by vertically inflating a single monolith. Divide work into named process types — web (serves HTTP), worker (processes queued jobs), clock (scheduled tasks). Kubernetes Deployment replicas implement this factor directly. Each process type scales independently.
Factor IX — Disposability: Fast Startup and Graceful Shutdown
Processes should start in seconds and shut down gracefully when they receive SIGTERM. A web process finishes its current request then exits. A worker process returns its current job to the queue before exiting. This enables rapid scaling, deployment, and recovery from failures.
Factor X — Dev/Prod Parity: Keep Environments as Similar as Possible
The gap between development and production is the root cause of "works on my machine" bugs. Three dimensions of parity: time (deploy often, minimize lag), personnel (developers who write code deploy and observe it), tools (same database engine, same cache, same queue in dev and prod). Using SQLite in dev and PostgreSQL in prod is a classic violation — subtle SQL dialect differences cause bugs that only appear in production.
Factor XI — Logs: Treat Logs as Event Streams
An app never concerns itself with routing or storing its output stream. It simply writes to stdout, unbuffered. The execution environment captures the stream and routes it to its destination — log aggregators (Datadog, Splunk, Loki), long-term storage, or a terminal during development. Never open log files directly, never manage log rotation inside the app.
Factor XII — Admin Processes: Run Admin Tasks as One-Off Processes
Database migrations, one-time data fixes, console inspections — run them as one-off processes in the same environment as the regular app processes, using the same release artifact and config. They must be committed to the repo, not executed as ad-hoc SSH sessions.
Twelve-Factor in Practice: A Diagnostic Checklist
When onboarding a new service, engineers at scale run through these questions quickly:
- Can I clone and run the app with zero manual steps beyond
docker compose up? (I, II) - Can I deploy to staging and production with only an environment variable change? (III, IV)
- Is every release immutable and tagged? Can I roll back in one command? (V)
- Will the app survive any single process dying and being restarted on a different host? (VI, IX)
- Are logs going to
stdoutand being captured by the platform? (XI) - Are admin tasks reproducible scripts in version control? (XII)