Skip to content

Twelve-Factor Examples, Factors I through IV

These examples use the same order-service contract in three ecosystems. Each application exposes web, worker, and admin process types from one versioned source tree. Choose a language in any tab group. Every group on this page follows that selection.

Factor I: Codebase

The deployable application is the unit of identity. Its web, worker, and admin processes come from one revision even though each starts through a different command.

One package.json exposes the web, worker, and admin commands. Every process starts from the same compiled package and locked dependency graph.

"scripts": {
"build": "tsc -p tsconfig.build.json",
"typecheck": "tsc --noEmit",
"start:web": "node dist/web.js",
"start:worker": "node dist/worker.js",
"admin": "node dist/admin.js",
"dev:web": "tsx src/web.ts",
"dev:worker": "tsx src/worker.ts",
"test:unit": "vitest run tests/unit",
"test:integration": "vitest run tests/integration",
"test": "vitest run",
"check:secrets": "node scripts/check-secrets.mjs",
"check:twelve-factor": "node scripts/check-twelve-factor.mjs",
"verify": "npm run check:twelve-factor",
"local:up": "docker compose -p tf_typescript up -d postgres redis",
"local:down": "docker compose -p tf_typescript down",
"local:clean": "node scripts/cleanup.mjs"

Factor I: Codebase TypeScript source

Evidence to collect: Given a running process, recover its release identifier, source revision, and named build target.

Factor II: Dependencies

The dependency declaration is complete only when a clean environment can resolve the same graph without relying on globally installed packages.

package.json declares direct dependencies. package-lock.json records the resolved graph. npm ci rejects drift between them, and npm ls proves the named runtime libraries resolve.

Terminal window
npm ci
npm run verify

Factor II: Dependencies TypeScript source

Evidence to collect: Install and test from an empty dependency cache or clean container, then inspect the runtime artifact for undeclared assumptions.

Factor III: Config

The application owns a typed configuration schema. Each deployment supplies values. Startup rejects missing or invalid input before accepting traffic or work.

Zod parses deploy values once and returns a typed object. Calling code receives config, not unrestricted access to process.env.

const baseSchema = z.object({
APP_HOST: z.string().default("0.0.0.0"),
PORT: z.coerce.number().int().min(1024).max(65535).default(3101),
DATABASE_URL: z.url().refine((value) => usesScheme(value, ["postgres:", "postgresql:"]), {
message: "must use PostgreSQL"
}),
REDIS_URL: z.url().refine((value) => usesScheme(value, ["redis:", "rediss:"]), {
message: "must use Redis"
}).optional(),
RELEASE_ID: z.string().regex(/^[A-Za-z0-9._-]{1,64}$/),
WORKER_CONCURRENCY: z.coerce.number().int().min(1).max(32).default(4),
SHUTDOWN_GRACE_MS: z.coerce.number().int().min(1000).max(60000).default(10000),
TELEMETRY_MODE: z.enum(["console", "memory", "disabled"]).default("console")
});

Factor III: Config TypeScript source

Evidence to collect: Start with a required value missing, a port outside its valid range, and a malformed backing-service URL. Each run must fail safely before readiness.

Factor IV: Backing services

PostgreSQL and the queue are attachments, not host assumptions. The application receives their endpoints and credentials through validated configuration.

The BullMQ client is constructed at startup from a validated Redis endpoint. The queue accepts an injected ioredis connection, and application modules receive the explicit queue boundary.

export class BullOrderQueue implements OrderQueue {
readonly queue: Queue<OrderJob>;
constructor(redisUrl: string) {
this.queue = new Queue<OrderJob>(queueName, { connection: redisConnection(redisUrl) });
}
async ready(): Promise<void> {
await this.queue.waitUntilReady();
}
async publish(job: OrderJob): Promise<void> {
await this.queue.add("complete-order", job, {
jobId: job.orderId,
attempts: 3,
backoff: { type: "exponential", delay: 250 },
removeOnComplete: 50,
removeOnFail: 50
});
}

Factor IV: Backing services TypeScript source

Evidence to collect: Replace an attachment endpoint without editing source, then run contract tests that expose semantic differences instead of assuming providers are interchangeable.

Reference releases

The examples on this page are exact excerpts from independently runnable releases: