The twelve-factor app
A 2011 Heroku manifesto (12factor.net) on building services that are portable, disposable, and scalable — deployable to any platform without code changes. It predates Kubernetes but describes exactly the app shape containers and orchestrators assume, which is why it’s still an interview staple: “is this service 12-factor?” is shorthand for “will it survive being scheduled, scaled, and killed?”
The twelve factors
| # | Factor | Rule | In a Python service |
|---|---|---|---|
| 1 | Codebase | one repo per app, many deploys | same image for dev/stage/prod |
| 2 | Dependencies | declare explicitly, never rely on system packages | lockfile (uv.lock/poetry.lock), no “it’s on the box” |
| 3 | Config | config lives in the environment, not code | DATABASE_URL env var, pydantic-settings |
| 4 | Backing services | treat DB/queue/cache as attached resources | swap Postgres for RDS by changing a URL only |
| 5 | Build, release, run | strict separation of stages | CI builds image → release = image + config → run |
| 6 | Processes | app is stateless processes | no local session/file state; state in DB/Redis |
| 7 | Port binding | app exports itself via a port | uvicorn app:app --port 8000, no app server “installed into” |
| 8 | Concurrency | scale out via the process model | more replicas/workers, not a bigger box first |
| 9 | Disposability | fast startup, graceful shutdown | handle SIGTERM, finish in-flight, exit |
| 10 | Dev/prod parity | keep environments as similar as possible | docker-compose with real Postgres, not SQLite-in-dev |
| 11 | Logs | logs are event streams, write to stdout | no log files, no rotation in-app; platform ships them |
| 12 | Admin processes | run one-off tasks in the same environment | alembic upgrade as a job/exec, same image |
The ones interviews actually probe — 3, 6, 9, 11 — in more depth below.
Factor 3 — config in the environment
Anything that differs between deploys (credentials, hostnames, feature toggles) must come from the environment, so the same build artifact runs anywhere. Litmus test from the manifesto: could you open-source the repo right now without leaking credentials?
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str # required — crash at boot if missing (good)
redis_url: str = "redis://localhost:6379/0"
debug: bool = False
settings = Settings() # reads env vars, validates types
Failing fast at startup on missing config beats a KeyError at 3 a.m. on the first request that touches Redis.
Modern caveat: env vars are visible in /proc, crash dumps, and docker inspect; hardened setups inject secrets as mounted files or fetch from a secrets manager — the principle (config outside the artifact) stands, the mechanism evolved. See ../../system_design/04_secrets_config/01_secrets_and_configuration.md and ../25_security/07_secrets_rate_limiting.md.
Factor 6 — stateless processes
Nothing that must survive a single request may live in process memory or on local disk. Sticky sessions are explicitly called an anti-pattern: any replica must be able to serve any request, or you can’t scale horizontally, do rolling deploys, or tolerate a killed pod. Session data → Redis/DB; uploads → object storage; caches in-process are fine only as disposable optimizations.
Full treatment: ../07_rest_apis/09_stateful_vs_stateless.md.
Factor 9 — disposability
Processes start fast and die gracefully, because the platform will kill them (deploy, rescale, node drain).
# what "graceful" means concretely for a worker
import signal, sys
shutting_down = False
def handle_sigterm(signum, frame):
global shutting_down
shutting_down = True # stop taking new work
signal.signal(signal.SIGTERM, handle_sigterm)
while not shutting_down:
job = queue.get(timeout=1)
process(job) # finish current job, then exit loop
sys.exit(0)
Web frameworks/servers (uvicorn, gunicorn) handle SIGTERM for you — your job is to keep startup cheap and make handlers idempotent so a kill mid-request is safe (18_idempotency_keys.md). This factor is what makes rolling and canary deploys routine (../27_cicd/05_deployment_strategies.md).
Factor 11 — logs as event streams
The app writes one stream of events to stdout and never manages files, rotation, or shipping. The execution environment (Docker log driver, Kubernetes, systemd) captures the stream and routes it to aggregation (../15_observability/05_elk_stack.md). Structured JSON per line makes that stream queryable — ../15_observability/13_structured_logging.md.
What aged, what didn’t
- Aged well: config in env, statelessness, disposability, stdout logs, declared dependencies — Kubernetes basically enforces them.
- Dated / extended: “Beyond the Twelve-Factor App” (Pivotal, 2016) adds API-first, telemetry, and auth as factors 13–15 — worth name-dropping. Port binding reads oddly for serverless; env-var config yields to secret managers; factor 1’s “one codebase” is debated in monorepo shops (the unit of deploy still maps to one app).
- 12-factor says nothing about service boundaries — it’s per-service hygiene, orthogonal to monolith vs microservices (08_monolith_vs_microservices.md). A 12-factor monolith is a perfectly good thing.
Common pitfalls
- Baking a
config.prod.yamlinto the image — that’s config in the build, so every credential rotation is a redeploy and the artifact isn’t portable. - “Stateless” service that writes uploads to local disk — works with 1 replica, corrupts UX at 2.
- Logging to files inside a container — invisible to
kubectl logs, lost on pod restart. - Running migrations at import time instead of as an explicit admin process — every replica races to migrate on boot.
Interview angle
- “What is a 12-factor app? Name the factors that matter most.” — Don’t recite all twelve; give the model (portable, disposable, stateless processes) and go deep on config/statelessness/disposability/logs.
- “Why config in environment variables?” — Same artifact across environments; secrets out of VCS; then show you know the modern secrets-manager caveat.
- “What does graceful shutdown look like in your service?” — SIGTERM → stop accepting → drain in-flight → exit; tie to rolling deploys and readiness probes.
- “Is 12-factor still relevant with Kubernetes?” — Yes: k8s assumes it. Mention Beyond-12-factor additions to show currency.