TechLead
Lesson 23 of 27
5 min read
Software Architecture

Twelve-Factor App

Master all 12 factors for building modern, cloud-native applications with practical examples and modern context

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
ICodebaseOne codebase tracked in version control, many deploys
IIDependenciesExplicitly declare and isolate dependencies
IIIConfigStore config in the environment
IVBacking ServicesTreat backing services as attached resources
VBuild, Release, RunStrictly separate build and run stages
VIProcessesExecute the app as one or more stateless processes
VIIPort BindingExport services via port binding
VIIIConcurrencyScale out via the process model
IXDisposabilityMaximize robustness with fast startup and graceful shutdown
XDev/Prod ParityKeep development, staging, and production as similar as possible
XILogsTreat logs as event streams
XIIAdmin ProcessesRun 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

Continue Learning