Master the Twelve-Factor App methodology for building modern cloud-native applications. Learn each factor in detail with practical examples and understand how they enable scalable, maintainable software-as-a-service.
The Twelve-Factor App methodology is a set of best practices for building software-as-a-service applications that are designed to thrive in modern cloud environments. Created by developers at Heroku, these principles have become the de facto standard for cloud-native application development.
These twelve factors address critical aspects of application architecture: from how you manage your codebase and dependencies, to how you handle configuration and backing services, to how you scale and maintain your application. Each factor is designed to maximize portability across execution environments, enable continuous deployment, and minimize divergence between development and production.
Understanding and applying these factors will help you build applications that are easier to deploy, scale horizontally, and maintain over time. Whether you're building microservices, serverless functions, or traditional web applications, these principles provide a solid foundation for cloud-native development.
The first three factors establish the foundation for your application's organization and portability.
Factor I: Codebase - One codebase tracked in version control, many deploys
There should be exactly one codebase per application, tracked in version control like Git. Multiple apps sharing code is a violation - instead, factor shared code into libraries.
1✅ CORRECT: One-to-One Relationship
2App 1 Codebase → Deploy to Dev, Staging, Prod
3App 2 Codebase → Deploy to Dev, Staging, Prod
4Shared Library → npm/PyPI package
5
6❌ WRONG: Multiple apps in one repo
7/monorepo
8 /app1 ← Not twelve-factor compliant
9 /app2 ← Each should be separate repoImplementation Example:
1// ✅ GOOD - Single app with multiple environments
2// package.json
3{
4 "name": "my-service",
5 "version": "1.0.0",
6 "scripts": {
7 "start:dev": "NODE_ENV=development node server.js",
8 "start:staging": "NODE_ENV=staging node server.js",
9 "start:prod": "NODE_ENV=production node server.js"
10 }
11}
12
13// Same codebase, different configs for each environment
14// server.js
15const config = require('./config');
16app.listen(config.port, () => {
17 console.log(`Running in ${process.env.NODE_ENV} on port ${config.port}`);
18});Factor II: Dependencies - Explicitly declare and isolate dependencies
Never rely on system-wide packages. Always declare dependencies explicitly and isolate them from the system.
1// ❌ BAD - Assuming global package
2const express = require('express'); // What if not installed?
3
4// ✅ GOOD - Explicit declaration in package.json
5{
6 "dependencies": {
7 "express": "^4.18.2",
8 "pg": "^8.11.0",
9 "redis": "^4.6.5"
10 },
11 "engines": {
12 "node": ">=18.0.0",
13 "npm": ">=9.0.0"
14 }
15}
16
17// Install with: npm install
18// All dependencies explicitly declared and versionedPython Example:
1# ❌ BAD - No dependency declaration
2import requests
3import pandas
4
5# ✅ GOOD - requirements.txt
6"""
7requests==2.31.0
8pandas==2.0.1
9psycopg2-binary==2.9.6
10"""
11
12# Install with: pip install -r requirements.txt
13# All dependencies explicit and isolated in virtual environmentFactor III: Config - Store config in the environment
Configuration that varies between deployments should be stored in environment variables, not in code.
1// ❌ BAD - Config in code
2const config = {
3 database: 'postgres://localhost:5432/mydb',
4 apiKey: 'sk-1234567890',
5 debug: true
6};
7
8// ✅ GOOD - Config in environment
9const config = {
10 database: process.env.DATABASE_URL,
11 apiKey: process.env.API_KEY,
12 debug: process.env.DEBUG === 'true',
13 port: parseInt(process.env.PORT || '3000')
14};
15
16// .env.development
17DATABASE_URL=postgres://localhost:5432/dev_db
18API_KEY=sk-dev-key
19DEBUG=true
20PORT=3000
21
22// .env.production
23DATABASE_URL=postgres://prod-host:5432/prod_db
24API_KEY=sk-prod-key-secret
25DEBUG=false
26PORT=8080Config Management Best Practices:
1// config.js - Centralized configuration
2const requiredEnvVars = [
3 'DATABASE_URL',
4 'REDIS_URL',
5 'API_KEY'
6];
7
8// Validate all required env vars are present
9requiredEnvVars.forEach(varName => {
10 if (!process.env[varName]) {
11 throw new Error(`Missing required environment variable: ${varName}`);
12 }
13});
14
15module.exports = {
16 database: {
17 url: process.env.DATABASE_URL,
18 maxConnections: parseInt(process.env.DB_MAX_CONNECTIONS || '10')
19 },
20 redis: {
21 url: process.env.REDIS_URL,
22 ttl: parseInt(process.env.CACHE_TTL || '3600')
23 },
24 api: {
25 key: process.env.API_KEY,
26 timeout: parseInt(process.env.API_TIMEOUT || '5000')
27 },
28 env: process.env.NODE_ENV || 'development',
29 port: parseInt(process.env.PORT || '3000')
30};These factors define how your application interacts with external services and how it's deployed and executed.
Factor IV: Backing Services - Treat backing services as attached resources
A backing service is any service the app consumes over the network (databases, caches, message queues, etc.). Treat all backing services as attached resources that can be swapped without code changes.
1// ✅ GOOD - Backing services as attached resources
2class DatabaseService {
3 constructor() {
4 // Connection comes from config, not hardcoded
5 this.client = new PostgresClient({
6 connectionString: process.env.DATABASE_URL
7 });
8 }
9}
10
11class CacheService {
12 constructor() {
13 this.client = new RedisClient({
14 url: process.env.REDIS_URL
15 });
16 }
17}
18
19// Swapping backing services - just change env var
20// Local dev: DATABASE_URL=postgres://localhost:5432/dev
21// Production: DATABASE_URL=postgres://aws-rds.amazon.com/prod
22// No code changes required!Attached Resource Pattern:
1# docker-compose.yml - Local backing services
2services:
3 app:
4 environment:
5 - DATABASE_URL=postgres://db:5432/myapp
6 - REDIS_URL=redis://cache:6379
7 - S3_BUCKET=local-bucket
8
9 db:
10 image: postgres:15
11
12 cache:
13 image: redis:7
14
15# Production - same app, different resources
16# DATABASE_URL=postgres://prod-db.aws.com/myapp
17# REDIS_URL=redis://prod-cache.aws.com:6379
18# S3_BUCKET=prod-bucket-us-east-1Factor V: Build, Release, Run - Strictly separate build and run stages
The twelve-factor app uses strict separation between the build, release, and run stages.
1BUILD STAGE
2 Source Code + Dependencies → Compiled Build Artifact
3 npm install && npm run build
4 ↓
5RELEASE STAGE
6 Build Artifact + Config → Immutable Release
7 v1.2.3 + Production Config → Release #47
8 ↓
9RUN STAGE
10 Release → Running Processes
11 docker run release-47Implementation:
1// package.json
2{
3 "scripts": {
4 // BUILD: Create production bundle
5 "build": "webpack --mode production",
6
7 // RELEASE: Tag and prepare release
8 "release": "npm run build && git tag -a v$npm_package_version",
9
10 // RUN: Start the application
11 "start": "node dist/server.js"
12 }
13}
14
15// CI/CD Pipeline
16// 1. BUILD
17npm ci && npm run build
18docker build -t myapp:$VERSION .
19
20// 2. RELEASE
21docker tag myapp:$VERSION myapp:release-$RELEASE_NUMBER
22kubectl apply -f k8s/deployment.yaml
23
24// 3. RUN
25kubectl rollout status deployment/myappFactor VI: Processes - Execute the app as one or more stateless processes
Twelve-factor processes are stateless and share-nothing. Any data that needs to persist must be stored in a stateful backing service.
1// ❌ BAD - Stateful process
2let userSessions = {}; // Memory state lost when process restarts!
3
4app.post('/login', (req, res) => {
5 const sessionId = generateId();
6 userSessions[sessionId] = req.user;
7 res.json({ sessionId });
8});
9
10// ✅ GOOD - Stateless process
11app.post('/login', async (req, res) => {
12 const sessionId = generateId();
13
14 // Store in backing service (Redis)
15 await redis.setex(`session:${sessionId}`, 3600, JSON.stringify(req.user));
16
17 res.json({ sessionId });
18});
19
20// Benefits:
21// - Can scale horizontally (multiple instances)
22// - Process crashes don't lose data
23// - Can restart anytime without impactStateless Process Pattern:
1class OrderService {
2 constructor(database, cache) {
3 this.db = database; // Backing service
4 this.cache = cache; // Backing service
5 // NO instance variables for request data!
6 }
7
8 async processOrder(orderId) {
9 // All state comes from backing services
10 const order = await this.db.getOrder(orderId);
11 const user = await this.cache.getUser(order.userId);
12
13 // Process and save back to backing service
14 const result = await this.db.saveOrder(processedOrder);
15
16 // No state kept in this process
17 return result;
18 }
19}These factors define how your application exposes itself and scales.
Factor VII: Port Binding - Export services via port binding
The twelve-factor app is completely self-contained and exports HTTP as a service by binding to a port.
1// ✅ Self-contained web server
2const express = require('express');
3const app = express();
4
5// App includes its own web server
6app.get('/', (req, res) => {
7 res.json({ message: 'Self-contained service' });
8});
9
10// Bind to port from environment
11const PORT = process.env.PORT || 3000;
12app.listen(PORT, () => {
13 console.log(`Service listening on port ${PORT}`);
14});
15
16// No external web server (Apache, IIS) required!
17// App is completely self-containedPython Example:
1# Self-contained FastAPI service
2from fastapi import FastAPI
3import uvicorn
4import os
5
6app = FastAPI()
7
8@app.get("/")
9def read_root():
10 return {"message": "Self-contained service"}
11
12if __name__ == "__main__":
13 port = int(os.getenv("PORT", 8000))
14 # Built-in ASGI server, no Apache/Nginx required
15 uvicorn.run(app, host="0.0.0.0", port=port)Factor VIII: Concurrency - Scale out via the process model
Scale your application by running multiple processes, not by making single processes larger.
1VERTICAL SCALING (❌)
2┌─────────────────┐
3│ Single Process │
4│ 32GB RAM │ ← Expensive, has limits
5│ 16 CPU cores │
6└─────────────────┘
7
8HORIZONTAL SCALING (✅)
9┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
10│ Proc │ │ Proc │ │ Proc │ │ Proc │
11│ 2GB │ │ 2GB │ │ 2GB │ │ 2GB │ ← Cheaper, unlimited
12│ 1CPU │ │ 1CPU │ │ 1CPU │ │ 1CPU │
13└──────┘ └──────┘ └──────┘ └──────┘Process Type Scaling:
1# Procfile - Different process types
2web: node server.js
3worker: node worker.js
4scheduler: node scheduler.js
5
6# Scale different process types independently
7# Heroku/Cloud Foundry
8web=4 # 4 web processes for HTTP requests
9worker=10 # 10 worker processes for background jobs
10scheduler=1 # 1 scheduler process for cron jobsNode.js Cluster Example:
1const cluster = require('cluster');
2const os = require('os');
3const express = require('express');
4
5if (cluster.isMaster) {
6 const numCPUs = os.cpus().length;
7 console.log(`Master process starting ${numCPUs} workers`);
8
9 // Fork workers
10 for (let i = 0; i < numCPUs; i++) {
11 cluster.fork();
12 }
13
14 cluster.on('exit', (worker, code, signal) => {
15 console.log(`Worker ${worker.process.pid} died, starting new one`);
16 cluster.fork();
17 });
18} else {
19 // Worker process
20 const app = express();
21
22 app.get('/', (req, res) => {
23 res.json({
24 worker: process.pid,
25 message: 'Handled by worker process'
26 });
27 });
28
29 const PORT = process.env.PORT || 3000;
30 app.listen(PORT, () => {
31 console.log(`Worker ${process.pid} started on port ${PORT}`);
32 });
33}Factor IX: Disposability - Maximize robustness with fast startup and graceful shutdown
The twelve-factor app's processes are disposable - they can be started or stopped at a moment's notice.
1// ✅ GOOD - Fast startup and graceful shutdown
2const express = require('express');
3const app = express();
4
5let server;
6let isShuttingDown = false;
7
8// Fast startup - minimal initialization
9async function startup() {
10 console.log('Starting application...');
11
12 // Connect to backing services
13 await database.connect();
14 await redis.connect();
15
16 server = app.listen(PORT, () => {
17 console.log(`Application ready in ${Date.now() - startTime}ms`);
18 });
19}
20
21// Graceful shutdown
22async function shutdown(signal) {
23 if (isShuttingDown) return;
24 isShuttingDown = true;
25
26 console.log(`Received ${signal}, starting graceful shutdown`);
27
28 // Stop accepting new requests
29 server.close(async () => {
30 console.log('HTTP server closed');
31
32 // Finish existing requests (with timeout)
33 await Promise.race([
34 finishExistingWork(),
35 new Promise(resolve => setTimeout(resolve, 30000))
36 ]);
37
38 // Close backing service connections
39 await database.disconnect();
40 await redis.disconnect();
41
42 console.log('Graceful shutdown complete');
43 process.exit(0);
44 });
45}
46
47// Handle shutdown signals
48process.on('SIGTERM', () => shutdown('SIGTERM'));
49process.on('SIGINT', () => shutdown('SIGINT'));
50
51// Handle crashes
52process.on('uncaughtException', (error) => {
53 console.error('Uncaught exception:', error);
54 shutdown('uncaughtException');
55});
56
57startup();Kubernetes Health Checks:
1// Liveness probe - is app alive?
2app.get('/health/live', (req, res) => {
3 res.status(200).json({ status: 'alive' });
4});
5
6// Readiness probe - is app ready for traffic?
7app.get('/health/ready', async (req, res) => {
8 if (isShuttingDown) {
9 return res.status(503).json({ status: 'shutting down' });
10 }
11
12 const dbOk = await database.ping();
13 const redisOk = await redis.ping();
14
15 if (dbOk && redisOk) {
16 res.status(200).json({ status: 'ready' });
17 } else {
18 res.status(503).json({ status: 'not ready' });
19 }
20});These factors ensure consistency across environments and proper observability.
Factor X: Dev/Prod Parity - Keep development, staging, and production as similar as possible
Minimize the gaps between development and production environments.
1Traditional Development:
2┌──────────────┬─────────────┬─────────────┐
3│ Gap │ Traditional │ Twelve-Factor│
4├──────────────┼─────────────┼─────────────┤
5│ Time │ Weeks │ Hours │
6│ Personnel │ Different │ Same │
7│ Tools │ SQLite/Mock │ PostgreSQL │
8└──────────────┴─────────────┴─────────────┘Implementation with Docker:
1# docker-compose.yml - Same services everywhere
2version: '3.8'
3
4services:
5 app:
6 build: .
7 environment:
8 - DATABASE_URL=postgres://db:5432/myapp
9 - REDIS_URL=redis://cache:6379
10 depends_on:
11 - db
12 - cache
13
14 # Same PostgreSQL version as production
15 db:
16 image: postgres:15-alpine
17 volumes:
18 - postgres_data:/var/lib/postgresql/data
19
20 # Same Redis version as production
21 cache:
22 image: redis:7-alpine
23
24 # Same message queue as production
25 rabbitmq:
26 image: rabbitmq:3.12-management
27
28# Developers run: docker-compose up
29# Production runs: Same containers, different configAvoiding Anti-Patterns:
1// ❌ BAD - Different databases
2// Development
3const db = new SQLite('./dev.db');
4
5// Production
6const db = new PostgreSQL(process.env.DATABASE_URL);
7// Problem: SQLite and PostgreSQL have different features!
8
9// ✅ GOOD - Same database everywhere
10const db = new PostgreSQL(
11 process.env.DATABASE_URL || 'postgres://localhost:5432/dev'
12);
13// Development uses PostgreSQL in Docker
14// Production uses managed PostgreSQL
15// Same database engine, different instanceFactor XI: Logs - Treat logs as event streams
A twelve-factor app never concerns itself with routing or storage of its output stream. It writes all logs to stdout.
1// ❌ BAD - App manages log files
2const fs = require('fs');
3const logFile = fs.createWriteStream('./app.log', { flags: 'a' });
4
5app.use((req, res, next) => {
6 logFile.write(`${new Date()} - ${req.method} ${req.url}\n`);
7 next();
8});
9
10// Problems:
11// - Log files fill up disk
12// - Can't aggregate logs from multiple instances
13// - Need to SSH into server to read logs
14
15// ✅ GOOD - Write to stdout
16app.use((req, res, next) => {
17 console.log(JSON.stringify({
18 timestamp: new Date().toISOString(),
19 method: req.method,
20 url: req.url,
21 userId: req.user?.id
22 }));
23 next();
24});
25
26// Benefits:
27// - Execution environment handles routing (Docker, K8s)
28// - Can send to centralized logging (Datadog, CloudWatch)
29// - Can view with: docker logs, kubectl logsStructured Logging:
1const winston = require('winston');
2
3// Configure logger to write to stdout
4const logger = winston.createLogger({
5 level: process.env.LOG_LEVEL || 'info',
6 format: winston.format.combine(
7 winston.format.timestamp(),
8 winston.format.json()
9 ),
10 transports: [
11 // Only stdout - let environment route logs
12 new winston.transports.Console()
13 ]
14});
15
16// Usage
17logger.info('User logged in', {
18 userId: user.id,
19 ip: req.ip,
20 userAgent: req.get('user-agent')
21});
22
23logger.error('Payment failed', {
24 orderId: order.id,
25 error: error.message,
26 amount: order.total
27});
28
29// Output goes to stdout as JSON
30// Environment routes to:
31// - Development: Terminal
32// - Docker: docker logs
33// - Kubernetes: kubectl logs → Elasticsearch
34// - Cloud: CloudWatch, Stackdriver, etc.Log Aggregation Architecture:
1Application Instances
2┌──────┐ ┌──────┐ ┌──────┐
3│ App1 │ │ App2 │ │ App3 │
4│stdout│ │stdout│ │stdout│
5└───┬──┘ └───┬──┘ └───┬──┘
6 │ │ │
7 └─────────┴─────────┘
8 │
9 ┌───────▼────────┐
10 │ Log Aggregator │
11 │ (Fluentd) │
12 └───────┬────────┘
13 │
14 ┌───────▼────────┐
15 │ Elasticsearch │
16 └───────┬────────┘
17 │
18 ┌───────▼────────┐
19 │ Kibana │
20 └────────────────┘The final factor addresses one-off administrative tasks and how to run them properly.
Factor XII: Admin Processes - Run admin/management tasks as one-off processes
Run one-off administrative tasks (database migrations, console sessions, etc.) in an identical environment to regular app processes.
1// ❌ BAD - Running admin tasks differently
2// SSH into production server
3ssh production-server
4node scripts/migrate-database.js // Different environment!
5
6// ✅ GOOD - Run as one-off process in same environment
7// Using Heroku
8heroku run node scripts/migrate-database.js
9
10// Using Kubernetes
11kubectl run -it migration \
12 --image=myapp:v1.2.3 \
13 --restart=Never \
14 --rm \
15 -- node scripts/migrate-database.js
16
17// Using Docker
18docker run --rm \
19 --env-file .env.production \
20 myapp:latest \
21 node scripts/migrate-database.jsDatabase Migrations:
1// migrations/001-create-users-table.js
2module.exports = {
3 async up(db) {
4 await db.schema.createTable('users', (table) => {
5 table.uuid('id').primary();
6 table.string('email').unique().notNullable();
7 table.timestamp('created_at').defaultTo(db.fn.now());
8 });
9 },
10
11 async down(db) {
12 await db.schema.dropTable('users');
13 }
14};
15
16// Run as one-off process
17// package.json
18{
19 "scripts": {
20 "migrate": "node scripts/migrate.js",
21 "migrate:rollback": "node scripts/migrate.js rollback"
22 }
23}
24
25// In production (same environment as app)
26heroku run npm run migrate
27kubectl exec deployment/myapp -- npm run migrateAdmin Console:
1// scripts/console.js - Interactive REPL
2const repl = require('repl');
3const db = require('../lib/database');
4const models = require('../models');
5
6async function startConsole() {
7 // Connect using same config as app
8 await db.connect();
9
10 const replServer = repl.start({
11 prompt: 'app > '
12 });
13
14 // Make models available
15 replServer.context.db = db;
16 replServer.context.User = models.User;
17 replServer.context.Order = models.Order;
18
19 console.log('Admin console ready. Try: User.findAll()');
20}
21
22startConsole();
23
24// Run in production
25// heroku run node scripts/console.js
26// > const users = await User.findAll()
27// > console.log(users.length)Complete Twelve-Factor Application Example:
1// server.js - Twelve-factor compliant application
2const express = require('express');
3const config = require('./config'); // III: Config from env
4const db = require('./lib/database'); // IV: Backing service
5
6const app = express();
7
8// Validate environment
9if (!config.isValid()) {
10 console.error('Invalid configuration');
11 process.exit(1);
12}
13
14// VII: Port binding
15const PORT = config.port;
16
17// Startup
18async function start() {
19 try {
20 // Connect to backing services
21 await db.connect();
22
23 // VI: Stateless processes
24 // No in-memory session storage
25
26 app.listen(PORT, () => {
27 console.log(`Service started on port ${PORT}`);
28 // XI: Logs to stdout
29 });
30 } catch (error) {
31 console.error('Startup failed:', error);
32 process.exit(1);
33 }
34}
35
36// IX: Graceful shutdown
37process.on('SIGTERM', async () => {
38 console.log('Shutting down gracefully');
39 await db.disconnect();
40 process.exit(0);
41});
42
43start();
44
45// I: One codebase - single repo
46// II: Dependencies - package.json
47// V: Build/release/run - npm build, docker build
48// VIII: Concurrency - horizontal scaling
49// X: Dev/prod parity - same PostgreSQL version
50// XII: Admin processes - npm run migrateChecklist for Twelve-Factor Compliance:
1✓ I. Codebase
2 - One repo per app
3 - Tracked in Git
4 - Shared code as libraries
5
6✓ II. Dependencies
7 - package.json/requirements.txt
8 - No system-wide dependencies
9 - Explicit versioning
10
11✓ III. Config
12 - Environment variables
13 - No hardcoded secrets
14 - Different configs per environment
15
16✓ IV. Backing Services
17 - Databases as attached resources
18 - Can swap without code changes
19 - URL-based configuration
20
21✓ V. Build/Release/Run
22 - Separate stages
23 - Immutable releases
24 - Rollback capability
25
26✓ VI. Processes
27 - Stateless
28 - Share-nothing
29 - State in backing services
30
31✓ VII. Port Binding
32 - Self-contained web server
33 - Export via port binding
34 - No external server required
35
36✓ VIII. Concurrency
37 - Scale via process model
38 - Different process types
39 - Horizontal scaling
40
41✓ IX. Disposability
42 - Fast startup (<10s)
43 - Graceful shutdown
44 - Handle SIGTERM properly
45
46✓ X. Dev/Prod Parity
47 - Same backing services
48 - Same versions
49 - Docker/containers
50
51✓ XI. Logs
52 - Stdout only
53 - Structured logging
54 - Environment handles routing
55
56✓ XII. Admin Processes
57 - One-off processes
58 - Same environment
59 - Same codebase/releaseYour progress is saved in this browser, on this device only.