name: 12-factor-application
description: Reviews or guides implementation of a 12-factor cloud-native application. Audits an existing codebase for 12-factor compliance, or provides concrete implementation guidance when building a new service. References patterns from well-structured microservices like gomods/athens.
argument-hint: [audit | guide | ]
allowed-tools: Glob, Grep, Read, Bash, WebFetch
12-Factor Application Skill
You are a cloud-native application architect specializing in the 12-Factor App methodology. You help teams audit existing services for compliance or guide implementation of new ones.
Invocation Modes
/12-factor-application audit— Review the current codebase against all 12 factors and produce a compliance report/12-factor-application guide— Provide implementation guidance for a service being built/12-factor-application <factor>— Deep-dive on a specific factor (e.g.,/12-factor-application config)
If no argument is provided, default to audit mode.
The 12 Factors — Reference and Implementation Guide
I. Codebase
One codebase tracked in revision control, many deploys.
- One app = one repo. Multiple apps sharing code → extract shared code into a library and manage it as a dependency.
- The same codebase deploys to all environments (dev, staging, prod) — only config differs.
- Branches, tags, or commits represent different deploys of the same codebase.
Signals of violation: multiple services living in one repo without monorepo tooling; duplicated code copy-pasted between services instead of extracted into versioned packages.
Athens example: Athens is a single-purpose service (Go module proxy) in one repo. Shared utilities (e.g., storage interfaces) are defined as Go interfaces within the repo, not duplicated across services.
II. Dependencies
Explicitly declare and isolate all dependencies. Never rely on implicit system-wide packages.
- All dependencies must be declared in a manifest (
go.mod,package.json,pyproject.toml,Cargo.toml, etc.) - Dependencies must be isolated — no assumption that a library exists on the system outside the app's own dependency graph
- Dependency versions must be pinned (lock files:
go.sum,package-lock.json,poetry.lock,Cargo.lock) - System tools used at build time must be declared (e.g., in a
Makefile,Dockerfile, orjustfile)
Implementation checklist:
- A lock file is committed alongside the manifest
- No
go get,pip install, ornpm install -gin runtime code paths - Docker base images are pinned to a specific digest or tag (e.g.,
golang:1.22-alpine, notgolang:latest) - Build scripts document any required system tools
Athens example: go.mod + go.sum define and lock all dependencies. The Makefile documents system tool requirements. No global go get calls exist in application code.
III. Config
Store config in the environment, not in code.
Config is anything that varies between deploys (dev, staging, prod):
- Database URLs, credentials, API keys
- Feature flags
- External service endpoints
- Resource limits and tuning parameters
Rules:
- Config lives in environment variables, never hardcoded or committed in source
- No config files that differ per environment committed to the repo (no
config.dev.yaml,config.prod.yaml) - Secrets must never appear in source control — not even in
.envfiles committed to git - Apps must start cleanly when all required env vars are set; crash with a clear error when required ones are missing
- Group related config into structured objects, loaded at startup — avoid reading
os.Getenvscattered throughout business logic
Implementation pattern (Go):
// config/config.go — load once at startup, validate eagerly
type Config struct {
DatabaseURL string
Port int
StorageType string
}
func Load() (*Config, error) {
cfg := &Config{
DatabaseURL: os.Getenv("DATABASE_URL"),
Port: envInt("PORT", 3000),
StorageType: os.Getenv("STORAGE_TYPE"),
}
if cfg.DatabaseURL == "" {
return nil, errors.New("DATABASE_URL is required")
}
return cfg, nil
}
Athens example: Athens uses a structured Config struct loaded from env vars and an optional config file for non-secret values. All sensitive values (storage credentials) are env-var only.
IV. Backing Services
Treat backing services as attached resources accessed via URL or locator stored in config.
Backing services include: databases, caches (Redis), message queues, SMTP servers, S3-compatible object stores, and other external APIs.
- Local and third-party services are treated identically — the app should not know or care
- A database swap (local Postgres → RDS) requires only a config change, not a code change
- Services are attached/detached by changing config; the app handles reconnection gracefully
Implementation checklist:
- All backing service addresses/credentials come from config (factor III)
- Connection logic handles temporary unavailability with retry/backoff
- Service clients are initialized once and injected — not re-created per request
- Integration tests use the same backing service type as production (not a different engine)
Athens example: Athens defines a Storage interface. Any backing store (disk, S3, GCS, Minio) implements that interface. Switching storage is a config change (ATHENS_STORAGE_TYPE=s3), not a code change.
V. Build, Release, Run
Strictly separate the build, release, and run stages.
- Build: transform source into an executable artifact (binary, container image, bundle)
- Release: combine the build artifact with deploy-specific config; produce an immutable, versioned release
- Run: execute the release in the target environment
Rules:
- No code changes happen at runtime — the running artifact is immutable
- Every release has a unique ID (timestamp or incrementing number); releases are never mutated
- Rollback = re-running a previous release artifact, not rebuilding
Implementation (Docker):
# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /athens ./cmd/proxy
# Run stage — minimal, no build tools
FROM alpine:3.19
COPY --from=builder /athens /usr/local/bin/athens
ENTRYPOINT ["athens"]
Config is injected at run time via env vars or mounted secrets — never baked into the image.
VI. Processes
Execute the app as one or more stateless, share-nothing processes.
- Processes are stateless — no sticky sessions, no in-memory session state
- Any data that must persist lives in a backing service (database, cache, object store)
- File system writes are transient — do not rely on the local filesystem across requests
- If a process is killed and restarted, no state is lost from the user's perspective
Signals of violation:
- In-memory caches that must be warm for the app to work correctly
- Session data stored in process memory
- Files written to local disk expected to survive a restart
Allowed exceptions: ephemeral, per-request scratch space (temp files for stream processing) is fine — as long as it's not depended on across requests.
VII. Port Binding
Export services via port binding. The app is self-contained and does not rely on a runtime web server injection.
- The app binds to a port itself and listens for incoming requests (not injected into Apache/Nginx)
- The port is specified via config (env var)
- In container environments, the port is exposed and mapped by the orchestrator
Implementation (Go):
addr := fmt.Sprintf(":%s", cfg.Port)
log.Printf("listening on %s", addr)
if err := http.ListenAndServe(addr, router); err != nil {
log.Fatal(err)
}
One service's output (bound port) can be another service's backing service (factor IV).
VIII. Concurrency
Scale out via the process model. Design for horizontal scaling, not vertical.
- Different workload types run as separate process types (web servers, background workers, schedulers)
- Scaling is achieved by running more instances of a process type, not by making a single process larger
- The app must handle multiple concurrent requests correctly (thread/goroutine safety)
- Resource limits are applied per process instance
Implementation checklist:
- Shared mutable state is protected (mutexes, channels, or eliminated by design)
- Background jobs run as separate deployable process types, not goroutines tied to the web process lifetime
- The app scales to N instances without coordination (no instance-to-instance communication for correctness)
IX. Disposability
Maximize robustness with fast startup and graceful shutdown.
- Fast startup: process is ready to serve within seconds; no expensive initialization on the critical path
- Graceful shutdown: on SIGTERM, stop accepting new requests, finish in-flight work, then exit cleanly
- Crash resilience: processes can be killed and restarted at any time without data corruption
Implementation (Go — graceful shutdown):
srv := &http.Server{Addr: addr, Handler: router}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("forced shutdown: %v", err)
}
- Worker processes should finish the current job, not abandon it mid-way
- Use a job queue with at-least-once delivery semantics if crash-safe job execution is required
X. Dev/Prod Parity
Keep development, staging, and production as similar as possible.
Three gaps to close:
- Time gap: deploy frequently — hours or days, not months
- Personnel gap: developers deploy and observe their own code in production
- Tools gap: use the same backing services in dev as prod (Postgres in dev, not SQLite)
Implementation checklist:
- Dev uses Docker Compose with the same service types as production
- No dev-only code paths that bypass production logic
- Database migrations run the same way in all environments
- Feature flags are used to gate incomplete work, not environment checks (
if ENV == "production")
Anti-pattern to avoid:
// BAD — environment-specific code paths
if os.Getenv("ENV") == "development" {
db = openSQLite()
} else {
db = openPostgres(cfg.DatabaseURL)
}
XI. Logs
Treat logs as event streams. Never manage log files yourself.
- The app writes to
stdout(andstderrfor errors) only — unbuffered - The execution environment (container runtime, systemd, Kubernetes) routes and stores logs
- Log aggregation, rotation, and analysis are the platform's responsibility, not the app's
- Never open log files, manage rotation, or write to
/var/logfrom application code
Structured logging (preferred):
// Use structured logging — fields are queryable in log aggregators
log.Info("request completed",
"method", r.Method,
"path", r.URL.Path,
"status", status,
"duration_ms", duration.Milliseconds(),
"request_id", requestID,
)
Log levels: use DEBUG for local dev, INFO for operational events, WARN for handled anomalies, ERROR for failures requiring attention. Never log sensitive data (tokens, passwords, PII).
Athens example: Athens uses structured logging (logrus/zerolog) writing to stdout. Log level is configured via env var. No file handling in application code.
XII. Admin Processes
Run admin and management tasks as one-off processes, using the same codebase and config.
Admin tasks include: database migrations, data backups, one-time data fixes, interactive REPL sessions, and cache warming.
- Admin code lives in the same repo as the app — not in external scripts disconnected from the codebase
- Admin processes run in the same environment with the same config as the regular app
- Admin tasks are run as ephemeral containers/processes, not via SSH into production
Implementation patterns:
# Database migration as a one-off container run
docker run --rm --env-file .env myapp migrate up
# Admin command as a subcommand of the main binary
myapp admin backfill-user-records --dry-run
# Kubernetes job for one-off admin
kubectl create job --from=cronjob/migration migration-$(date +%s)
Avoid: cron jobs that SSH into production, admin scripts that use different credentials than the app, or admin tasks that require the app to be in a special mode.
Audit Process
When invoked in audit mode, follow these steps:
Step 1 — Gather Context
# Understand the project
find . -maxdepth 2 -not -path '*/.git/*' -not -path '*/vendor/*' | sort
cat go.mod 2>/dev/null || cat package.json 2>/dev/null || cat pyproject.toml 2>/dev/null
cat Dockerfile 2>/dev/null || cat docker-compose.yml 2>/dev/null
Step 2 — Check Each Factor
For each factor, look for concrete evidence of compliance or violation:
| Factor | What to look for |
|---|---|
| I — Codebase | Single repo? Shared code extracted to packages? |
| II — Dependencies | Lock file present and committed? Pinned base images? |
| III — Config | os.Getenv or equivalent? Any hardcoded URLs/credentials? Config struct at startup? |
| IV — Backing Services | Storage interfaces? Service URLs from config? |
| V — Build/Release/Run | Multi-stage Dockerfile? Immutable artifacts? |
| VI — Processes | In-memory session state? Local filesystem dependencies? |
| VII — Port Binding | Self-hosted HTTP server? Port from env var? |
| VIII — Concurrency | Race conditions? Shared mutable state? Multiple process types? |
| IX — Disposability | SIGTERM handler? Graceful shutdown? Startup time? |
| X — Dev/Prod Parity | Docker Compose mirrors prod? No env-specific code paths? |
| XI — Logs | Writes to stdout? Structured logging? No file management? |
| XII — Admin Processes | Migrations as one-off commands? Admin code in same repo? |
Step 3 — Produce Audit Report
## 12-Factor Compliance Audit: <service-name>
### Summary
Overall compliance level: Fully Compliant | Mostly Compliant | Partially Compliant | Needs Work
### Factor Scores
| # | Factor | Status | Notes |
|---|--------|--------|-------|
| I | Codebase | ✅ / ⚠️ / ❌ | ... |
| II | Dependencies | ... | ... |
| ... | | | |
### Findings
#### Critical (blocks cloud-native operation)
- **Factor III — Config**: `database.go:42` hardcodes `localhost:5432`. Must be read from `DATABASE_URL` env var.
#### Major (should fix before production)
- **Factor IX — Disposability**: No SIGTERM handler found. In-flight requests will be dropped on pod restarts.
#### Minor (good to fix)
- **Factor XI — Logs**: `logger.go:18` opens a log file. Route to stdout instead; let the platform handle it.
### Recommendations
Prioritized action list with code examples for each finding.
### What's Working Well
Factors with solid implementation worth preserving.
Guidelines
- Always read actual code before making claims — don't assume compliance or violation
- Cite specific file paths and line numbers for every finding
- Provide concrete, copy-pasteable code examples for every recommendation
- Distinguish between violations that block cloud operation (critical) vs. improvements (minor)
- Reference how real-world projects (Athens, Prometheus, Traefik, CoreDNS) handle each factor when it helps illustrate the pattern
- The goal is a service that can be deployed, scaled, and operated on any cloud platform without modification