banner

🧭 Why the Twelve Factors Still Matter

The Twelve-Factor App methodology, first published by Heroku engineers, remains one of the most practical blueprints for designing cloud-native, maintainable, and scalable applications.

Even though the document is over a decade old, the principles apply perfectly to containerized and microservice architectures — especially when using Go, Docker, and Kubernetes.

Let’s break them down with a modern engineering lens.


⚙️ 1. Codebase

One codebase tracked in revision control, many deploys.

A single codebase represents one application. Multiple environments — development, staging, production — should all be deployed from that same repository.
This prevents divergence between environments and simplifies automation.

A codebase should live in a version control system like Git. For Go, it might be a monorepo structured by packages (/internal, /cmd, /pkg) or a polyrepo setup with each microservice as a standalone repository.

Modern workflows use:

  • GitHub Actions or Jenkins pipelines to automate builds.
  • Trunk-based development to reduce merge friction.
  • Feature flags to deploy safely across multiple environments from the same codebase.

💡 In short: if you have multiple codebases for the same app, it’s not one app — it’s multiple.


🔑 2. Dependencies

Explicitly declare and isolate dependencies.

Every dependency your app uses — libraries, runtime versions, or CLI tools — should be declared explicitly.
Go does this via go.mod; Python via requirements.txt; Node via package.json. Never rely on system-wide packages.

In Docker, this means:

1
2
3
FROM golang:1.23 AS builder
COPY go.mod go.sum ./
RUN go mod download

This ensures a reproducible environment across machines and CI/CD agents.

Dependency isolation can be achieved with:

  • Docker containers (each container has its own dependency graph).

  • Virtual environments (for non-containerized setups).

  • SBOM (Software Bill of Materials) generation for security compliance.

Explicit dependencies make your build deterministic — a cornerstone of DevSecOps.


🧩 3. Config

Store config in the environment.

Configuration changes across deployments (e.g., credentials, ports, URLs) should not require code changes. Use environment variables, .env files, or configuration management systems (Vault, AWS SSM, Kubernetes Secrets).

Example .env:

DATABASE_URL=postgres://user:pass@db:5432/app
PORT=8080
LOG_LEVEL=info

Then inject them via your Docker Compose or Kubernetes manifests. This approach improves separation of code and configuration — allowing you to promote the same image across environments simply by changing environment variables.

💡 Never commit secrets to Git. Instead, use sealed secrets or secret managers.


🛠 4. Backing Services

Treat backing services as attached resources.

Databases, message brokers, caches, or external APIs are replaceable services. Your app should treat them as external and configurable via URLs or connection strings.

For example:

DATABASE_URL=postgres://user:pass@localhost:5432/app
REDIS_URL=redis://localhost:6379

If you migrate from PostgreSQL to CloudSQL or from Redis to ElastiCache, no code should change — just the configuration.

This abstraction supports portability and disaster recovery because you can switch providers without redeploying code.


📦 5. Build, Release, Run

Strictly separate build and run stages.

Each deployment should follow three immutable stages:

  1. Build: Compile code, install dependencies, and produce a deployable artifact (e.g., Docker image or binary).

  2. Release: Combine the build artifact with environment configuration to form a release.

  3. Run: Execute the app in one or more environments.

In CI/CD terms:

1
2
3
4
stages:
  - build
  - release
  - deploy

Build artifacts should be immutable and tagged (myapp:v1.2.3). Never rebuild an artifact during promotion between environments — it breaks reproducibility and traceability.


⚡️ 6. Processes

Execute the app as one or more stateless processes.

Your application should not depend on local state or session memory between requests. Use external stores for persistence — databases, Redis, or object storage.

Example in Go:

1
2
3
4
func handler(w http.ResponseWriter, r *http.Request) {
    user := cache.Get("user") // external store
    fmt.Fprintf(w, "Hello %s", user)
}

Stateless processes make your system horizontally scalable and resilient to container restarts — crucial in Kubernetes where pods are ephemeral.

💡 Never write to /tmp or the local filesystem expecting it to persist.


📂 7. Port Binding

Export services via port binding.

An app should self-contain its web server and expose it via a port, rather than depending on a host container like Apache or Tomcat.

In Go, this is trivial:

1
log.Fatal(http.ListenAndServe(":8080", router))

This model allows easy containerization:

1
2
3
# Hint only - it doesn't expose ports automatically
EXPOSE 8080
CMD ["./server"]

By following this principle, the app becomes portable across Docker, Kubernetes, and PaaS environments, since all it needs to run is a port and a network.


🔄 8. Concurrency

Scale out via the process model.

Instead of scaling “up” by adding threads or CPU cores, scale “out” by spawning multiple processes or containers.

  • Use goroutines or worker pools for concurrency inside one process.

  • Use multiple replicas in Kubernetes to scale horizontally.

  • Use queue-based workloads for background jobs.

Example Kubernetes manifest:

1
2
spec:
  replicas: 4

The Twelve-Factor principle aligns perfectly with container orchestration, allowing dynamic scaling per workload type.


🧠 9. Disposability

Maximize robustness with fast startup and graceful shutdown.

Processes should start quickly and stop gracefully. Fast startup improves elasticity; graceful shutdown prevents data loss.

In Go:

1
2
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()

This pattern ensures that in Kubernetes, your pod responds correctly to SIGTERM, allowing a few seconds to finish requests before being killed.

💡 Disposability = resilience. You can recycle containers without hurting availability.


🧰 10. Dev/Prod Parity

Keep development, staging, and production as similar as possible.

The closer your dev environment mirrors production, the fewer deployment surprises you’ll face. Using Docker Compose, Taskfile, or Minikube, developers can reproduce the production stack locally.

For example:

1
docker compose -f docker-compose.prod.yml up

Keep:

  • The same OS base image (e.g., Alpine, Debian).

  • The same database version.

  • The same config format (ENV).

💡 Continuous delivery pipelines help maintain this parity automatically.


🧾 11. Logs

Treat logs as event streams.

Instead of managing log files on disk, write all logs to stdout and let the environment aggregate them. This enables observability tools like Loki, ELK, or CloudWatch to handle them consistently.

Example in Go:

1
log.Printf("INFO: user %s logged in", userID)

Your containers stay stateless, and logs become part of a centralized event stream — not stored locally.

This design simplifies debugging, auditing, and monitoring in distributed environments.


🔁 12. Admin Processes

Run admin tasks as one-off processes.

Maintenance tasks like data migrations or cleanup scripts should run in the same runtime and configuration as the app.

Example (Go migration runner):

1
docker run --rm myapp:latest migrate up

Or in Kubernetes:

1
kubectl run migrate --image=myapp:latest --restart=Never --command -- migrate up

This guarantees consistency between environments and prevents configuration drift.

Never run migrations manually from a developer laptop.


🚀 Modern Alignment

FactorModern Equivalent
Config.env, Secrets, ConfigMaps
Build/RunDocker, CI/CD, GitHub Actions
LogsLoki, ELK, CloudWatch
ProcessesKubernetes Pods, Deployments
Dev/Prod ParityDocker Compose + Minikube

🧭 Lifecycle Overview

Below is a simplified flow of how the Twelve-Factor App lifecycle maps onto modern CI/CD pipelines:

flowchart LR
    A[Build Stage] --> B[Release Stage]
    B --> C[Run Stage]
    C --> D[Monitor & Logs]
    D --> A
    style A fill:#f9f,stroke:#333,stroke-width:1px
    style B fill:#9ff,stroke:#333,stroke-width:1px
    style C fill:#aff,stroke:#333,stroke-width:1px
    style D fill:#ffc,stroke:#333,stroke-width:1px

🧩 Summary

The Twelve Factors aren’t outdated — they’re timeless. In fact, they’ve quietly become the core philosophy of modern DevOps and container-native design.

They remind us to:

  • Isolate code and configuration,

  • Design stateless, disposable processes,

  • Keep environments consistent,

  • And automate everything reproducibly.


🚀 Follow me on norbix.dev for more insights on Go, Python, AI, system design, and engineering wisdom.