The Twelve-Factor App
The Twelve-Factor App methodology, created by developers at Heroku, provides a set of best practices for building modern web applications that are portable, scalable, and suitable for cloud deployment. These principles apply to applications written in any programming language and using any combination of backing services.
The 12 Factors at a Glance
| # | Factor | Principle |
|---|---|---|
| I | Codebase | One codebase tracked in version control, many deploys |
| II | Dependencies | Explicitly declare and isolate dependencies |
| III | Config | Store config in the environment |
| IV | Backing Services | Treat backing services as attached resources |
| V | Build, Release, Run | Strictly separate build and run stages |
| VI | Processes | Execute the app as one or more stateless processes |
| VII | Port Binding | Export services via port binding |
| VIII | Concurrency | Scale out via the process model |
| IX | Disposability | Maximize robustness with fast startup and graceful shutdown |
| X | Dev/Prod Parity | Keep development, staging, and production as similar as possible |
| XI | Logs | Treat logs as event streams |
| XII | Admin Processes | Run admin/management tasks as one-off processes |
Factor III: Config — Store Config in the Environment
// BAD: Hardcoded config
const dbUrl = "postgres://user:pass@localhost:5432/mydb";
// GOOD: Environment-based config with validation
import { z } from "zod";
const envSchema = z.object({
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url(),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(["development", "staging", "production"]),
JWT_SECRET: z.string().min(32),
STRIPE_API_KEY: z.string().startsWith("sk_"),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
CORS_ORIGINS: z.string().transform(s => s.split(",")),
});
export type Config = z.infer;
export function loadConfig(): Config {
const result = envSchema.safeParse(process.env);
if (!result.success) {
console.error("Invalid environment configuration:");
for (const error of result.error.errors) {
console.error(` ${error.path.join(".")}: ${error.message}`);
}
process.exit(1);
}
return result.data;
}
Factor VI: Processes — Stateless Applications
// BAD: Storing state in process memory
let sessions: Record = {}; // Lost on restart!
app.post("/login", (req, res) => {
sessions[sessionId] = { userId: user.id }; // Memory only
});
// GOOD: Use external backing services for state
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
app.post("/login", async (req, res) => {
const session = { userId: user.id, createdAt: Date.now() };
await redis.setex(`session:${sessionId}`, 3600, JSON.stringify(session));
res.cookie("sessionId", sessionId, { httpOnly: true, secure: true });
});
Factor IX: Disposability — Fast Startup and Graceful Shutdown
// Graceful shutdown handling
const server = app.listen(config.PORT, () => {
console.log(`Server started on port ${config.PORT}`);
});
async function gracefulShutdown(signal: string): Promise {
console.log(`${signal} received. Starting graceful shutdown...`);
// Stop accepting new connections
server.close(() => {
console.log("HTTP server closed");
});
// Wait for in-flight requests to complete (with timeout)
const shutdownTimeout = setTimeout(() => {
console.error("Forced shutdown — timeout exceeded");
process.exit(1);
}, 30000); // 30 second timeout
try {
// Close database connections
await pool.end();
console.log("Database connections closed");
// Close Redis connections
await redis.quit();
console.log("Redis connections closed");
// Close message queue connections
await consumer.disconnect();
console.log("Message queue disconnected");
clearTimeout(shutdownTimeout);
console.log("Graceful shutdown complete");
process.exit(0);
} catch (error) {
console.error("Error during shutdown:", error);
process.exit(1);
}
}
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
Factor XI: Logs — Treat Logs as Event Streams
// BAD: Writing to log files
import fs from "fs";
fs.appendFileSync("app.log", `${new Date()} - Error occurred\n`);
// GOOD: Write structured JSON to stdout
const logger = {
info(message: string, meta?: Record): void {
console.log(JSON.stringify({
level: "info",
message,
timestamp: new Date().toISOString(),
service: process.env.SERVICE_NAME,
...meta,
}));
},
error(message: string, error?: Error, meta?: Record): void {
console.error(JSON.stringify({
level: "error",
message,
timestamp: new Date().toISOString(),
service: process.env.SERVICE_NAME,
error: error ? { message: error.message, stack: error.stack } : undefined,
...meta,
}));
},
};
// The execution environment (Docker, Kubernetes) captures stdout
// and routes to a log aggregation service (ELK, Datadog, CloudWatch)
Modern Extensions (Beyond 12 Factors)
- API First: Design APIs before implementation — use OpenAPI specs
- Telemetry: Build in observability from day one — metrics, traces, and structured logs
- Security: Shift left on security — scan dependencies, use secrets management
- Feature Flags: Decouple deployment from release using feature flags