backend / architecture design / 20_twelve_factor_app.md

The twelve-factor app

4 min read source

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.yaml into 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.