---
title: 'Twelve-Factor App Principles'
source: 'https://academia.sh/en/courses/server-fundamentals/twelve-factor-app-principles'
course: 'Server-Side Fundamentals'
language: en
updated: '2026-08-19T05:19:37+00:00'
license: 'CC BY-SA 4.0'
---

# Twelve-Factor App Principles

The decisions made in this section are mapped to twelve principles named as a portability contract; a single build running unchanged across two environments is measured with a file hash, and fast startup and graceful shutdown are measured through the completion of an in-flight request.

The decisions made in this section look independent of one another: splitting directories into
layers, reading configuration from the environment, masking secrets, writing the log to standard
output, passing errors through a single layer. There is a connection between them, and it
converges on a single goal: making the application **independent of the machine it runs on**.

There is a method that turns this goal into a named set of rules: the **twelve-factor app**
principles. The principles are not a framework or a tool; they are a contract that defines the
interface between the party that writes the application and the party that runs it. This lesson
counts the principles, matches which ones this section satisfies against its measurements, and
points out the ones that are not satisfied.

## The Subject of the Contract

The principles share a common assumption: the party running the application has to be able to
install it, configure it, replicate it, monitor it, and stop it without knowing its internals.
Once that holds, the application is bound not to a machine but to a contract.

For the library loan service, this has a concrete counterpart. The developer who runs the service
on a laptop and the operator who runs it on a server use the same file; the only difference
between them is which environment variables they supply. The code does not know which machine it
is on.

The twelve principles and their counterparts in this section are as follows.

| Principle | Summary | This section's counterpart |
|---|---|---|
| Codebase | One codebase, many deploys | Introduction to Version Control course |
| Dependencies | Explicitly declared and isolated | Modules, Tooling and the Ecosystem course |
| Config | Stored in the environment | Configuration Management lesson |
| Backing services | Treated as an attached resource | The Local Development Environment lesson |
| Build, release, run | Stages strictly separated | Rendering Strategies and Infrastructure course |
| Processes | Stateless and share-nothing | The Application Runtime lesson |
| Port binding | Service binds its own port | The Web Server Concept lesson |
| Concurrency | Scales via the process model | The Application Runtime lesson |
| Disposability | Fast startup, graceful shutdown | This lesson |
| Dev/prod parity | Environments kept close to each other | This lesson |
| Logs | Treated as an event stream | Logging Setup lesson |
| Admin processes | Run as one-off tasks | Backend Development curriculum's advanced courses |

Four of the principles fall outside this course; two are this lesson's measurements.

## Measurement: One Build, Two Environments

The **dev/prod parity** principle asks for the same build to run unchanged across different
environments. Its testable form is this: the file's hash stays unchanged while its behavior
changes through environment variables.

```js
// src/http/server.mjs — the same file in every environment; binds its own port,
// writes the log to standard output, finishes the in-flight request and exits on SIGTERM.
import { createServer } from "node:http";
import { setTimeout as wait } from "node:timers/promises";

const PORT = Number(process.env.LIBRARY_PORT ?? 8440);
const ENV = process.env.LIBRARY_ENV ?? "local";
const LOAN_DAYS = Number(process.env.LIBRARY_LOAN_DAYS ?? 14);

const log = (record) => console.log(JSON.stringify({ at: new Date().toISOString(), ...record }));

let openRequests = 0;

const server = createServer(async (req, res) => {
  res.sendDate = false;
  openRequests++;
  const path = new URL(req.url, "http://local").pathname;
  if (path === "/loans") await wait(1500);            // a loan operation that takes time
  const text = JSON.stringify({ env: ENV, loanDays: LOAN_DAYS, path });
  res.writeHead(200, { "content-type": "application/json; charset=utf-8",
    "content-length": Buffer.byteLength(text) });
  res.end(text);
  openRequests--;
});

server.listen(PORT, "127.0.0.1", () => log({
  event: "started", port: PORT, env: ENV, loanDays: LOAN_DAYS,
  startupMs: Number(performance.now().toFixed(1)),      // since process start
}));

process.on("SIGTERM", () => {
  log({ event: "sigterm_received", openRequests });
  server.close(() => {                                // no new connections, open ones finish
    log({ event: "closed", openRequests });
    process.exit(0);
  });
  server.closeIdleConnections();                      // idle connections close immediately
  setTimeout(() => { log({ event: "force_closed" }); process.exit(1); }, 5000).unref();
});
```

```bash
#!/usr/bin/env bash
# Runs the same file with two environments; shows behavior changing while the file's hash stays unchanged.
echo "file hash: $(shasum -a 256 src/http/server.mjs | cut -c1-16)…"

try() {  # $1 = env label, $2 = loan days, $3 = port
  LIBRARY_ENV="$1" LIBRARY_LOAN_DAYS="$2" LIBRARY_PORT="$3" \
    node src/http/server.mjs > "log-$1.jsonl" 2>&1 & s=$!
  sleep 0.7
  printf '%-11s -> ' "$1"
  curl -sS "http://127.0.0.1:$3/books"; echo
  kill -TERM "$s"; wait "$s" 2>/dev/null
  grep -o '"startupMs":[0-9.]*' "log-$1.jsonl"
  rm -f "log-$1.jsonl"
}

try local 14 8441
try production 21 8442
```

```
file hash: cc28586133fd7c6a…
local       -> {"env":"local","loanDays":14,"path":"/books"}
"startupMs":28.5
production  -> {"env":"production","loanDays":21,"path":"/books"}
"startupMs":28.2
```

The file hash and startup times change on every machine and after every edit; the hash is here to
show that the file did not change between the two runs.

The same file produced two different responses in two environments: the loan period is fourteen
days in one, twenty-one in the other. There is no branch in the code; no condition carries the
environment's name, no `if (ENV === "production")` line exists. The moment such a line is added,
the principle breaks, because the application now knows where it is running, and the path tested
locally diverges from the path that runs in production.

The same output confirms another principle too. The **port binding** principle asks the service to
open its own port without needing an external component. The `curl` call went straight to the
process; no component sat in between. The reverse proxy set up in an earlier section is an option,
not a requirement — and that is exactly why local development and production run the same code.

## Measurement: Disposability

The **disposability** principle asks processes to start up fast and shut down cleanly. Both rest
on the same reasoning: processes get started and stopped often. Releases, scaling changes, machine
changes, and restarts after a crash all make this necessary.

The startup time is visible in the output above: under thirty milliseconds from process start to
listening. What actually needs testing is how the process comes down. The measurement below sends
`SIGTERM` to the process while a request is in flight.

```bash
#!/usr/bin/env bash
# Sends SIGTERM while a request is in flight; measures whether it finishes and whether a new connection is rejected.
node src/http/server.mjs > log.jsonl 2>&1 & pid=$!
sleep 0.7

curl -sS -o /dev/null -w 'in-flight request : %{http_code}  %{time_total} s\n' \
  http://127.0.0.1:8440/loans > inflight.txt & req_pid=$!
sleep 0.4
echo "SIGTERM sent"
kill -TERM "$pid"
sleep 0.4

printf 'new request during shutdown : '
if curl -s -o /dev/null --max-time 2 http://127.0.0.1:8440/books; then
  echo "response received"
else
  echo "connection could not be established (curl exit code=$?)"
fi

wait "$req_pid"; cat inflight.txt
wait "$pid"; echo "process exit code=$?"
echo "--- log ---"
cat log.jsonl
rm -f log.jsonl inflight.txt
```

```
SIGTERM sent
new request during shutdown : connection could not be established (curl exit code=7)
in-flight request : 200  1.505712 s
process exit code=0
--- log ---
{"at":"2026-07-28T20:20:44.905Z","event":"started","port":8440,"env":"local","loanDays":14,"startupMs":25.9}
{"at":"2026-07-28T20:20:45.986Z","event":"sigterm_received","openRequests":1}
{"at":"2026-07-28T20:20:47.101Z","event":"closed","openRequests":0}
```

Timestamps and durations change on every run.

Three observations hold steady. **The in-flight request finished**: the signal arrived in the
middle of the request, and even so the response came back as `200` and the one-and-a-half-second
job was not cut short. The log's `sigterm_received` line records that one request was open at that
moment, and the `closed` line records that zero requests were open once shutdown completed.

**The new connection was rejected.** The request made after the signal could not even establish a
connection; the `server.close` call closed the listening socket but did not cut off open
connections. This is the correct behavior for a process that is shutting down: take no new work,
finish the work you already took.

**The exit code is zero.** A supervisor seeing this code knows the process went down deliberately;
a nonzero code would report an unexpected termination and call for a different response.

The force-shutdown timer is the unavoidable complement to this arrangement. A request that never
finishes could keep the process waiting forever; once the five-second limit is up, the process
goes down with a nonzero code. The `unref` call keeps this timer from extending the process's life
once shutdown has already finished early.

## What Is Not Satisfied

Reading the list of principles honestly means also counting the ones that are not satisfied.

The **backing services** principle has not been applied yet. This section's stores lived in
process memory; treating a database or an external service as a swappable attached resource
identified by nothing more than an address is set up in this course's The Local Development
Environment lesson.

The **admin processes** principle is also open. One-off tasks like data migration, bulk fixes, and
report generation have to run with the same code and the same environment as the application
itself; done with a separate script, two different realities are created.

The **build, release, run** separation was not measured in this section; keeping the build output
separate from environment variables and giving every release a revertible identity was covered in
the Rendering Strategies and Infrastructure course, and its server-side counterpart sits in more
advanced courses.

Not satisfying a principle is not a defect; not knowing that you have not satisfied it is. That is
what the list is for — it ties each decision, whether made or deferred, to a name.

## Summary

- The twelve-factor principles are not a tool but a contract between the party that writes the
  application and the party that runs it; their shared goal is making the application independent
  of the machine it runs on.
- Dev/prod parity was measured by the file's hash staying unchanged while behavior changed through
  environment variables; the code has no branch that looks at the environment's name.
- Under the port binding principle, the service opens its own socket; a reverse proxy is an option,
  not a precondition.
- In the disposability measurement, after `SIGTERM` the in-flight request finished with `200`, the
  new connection was rejected, and the process went down with a zero exit code; the force-shutdown
  timer guards the limit against a request that never finishes.
- Backing services, admin processes, and the build-release-run separation were not satisfied in
  this section; the list's job is to name the decisions that were deferred.

## Next Step

The scaffold is now standing: configuration is read from the environment, secrets are kept
separate, the log is written to standard output, errors turn into a shared response shape, and the
process starts up and shuts down cleanly. Even so, the application still gives nothing real to the
outside world; the only thing it hands back is its own state. The next section fills that gap
starting from the simplest case: serving a file sitting on disk over the network. The next lesson
sets up under what condition that file is allowed to be read, how the recipient will interpret its
content, what happens when the same file is requested a second time, and whether that load stays
on the server.
