---
title: 'Indentation and Code Formatting'
source: 'https://academia.sh/en/courses/clean-code/indentation-and-code-formatting'
course: 'Clean Code'
language: en
updated: '2026-08-23T07:01:11+00:00'
license: 'CC BY-SA 4.0'
---

# Indentation and Code Formatting

Taking code format out of the realm of style and measuring it by diff cost: the diff lines produced by the same content in two indentation schemes, the diff line counts the same revision produces in an aligned and a plain form of the tariff table, and separating out the diff lines that carry no content.

The previous lesson brought every function down to a single level of abstraction, but
nothing was yet said about the file itself: where lines end, which declarations sit next
to each other, how much indentation there should be. These decisions are usually treated
as style, and the argument about them usually gets settled by taste.

Format has a measurable consequence. Every change to a codebase produces a **diff**, and
someone else reads that diff and approves it. The same content change produces a
different number of diff lines in different formats, and some of those lines carry no
information at all. This lesson ties format to two numbers: the number of diff lines a
change produces, and how many of those carry no content.

## Indentation Shows the Structure Itself

Indentation is the only signal that carries the depth of nested blocks to the eye. When
structure and indentation part ways, whoever is reading has to find where a block ends by
counting curly braces by hand. The two files below carry the same sequence of words; only
their indentation differs.

```sh
mkdir -p tidy messy aligned plain
```

```js
// tidy/fee.mjs — indentation matches structural depth
const MINIMUM_FEE = 52;
const WEIGHT_TIERS = [
  { upperLimitKg: 5, ratePerKg: 26 },
  { upperLimitKg: 30, ratePerKg: 17 },
];

export function tierFee(weightKg, zoneCoefficient) {
  const tier = WEIGHT_TIERS.find((t) => weightKg <= t.upperLimitKg);
  if (tier === undefined) {
    throw new RangeError("weight outside tier range");
  }
  return Math.max(tier.ratePerKg * weightKg * zoneCoefficient, MINIMUM_FEE);
}
```

```js
// messy/fee.mjs — same content, indentation does not match structure
const MINIMUM_FEE = 52;
const WEIGHT_TIERS = [
    { upperLimitKg: 5, ratePerKg: 26 },
  { upperLimitKg: 30, ratePerKg: 17 },
];

export function tierFee(weightKg, zoneCoefficient) {
    const tier = WEIGHT_TIERS.find((t) => weightKg <= t.upperLimitKg);
      if (tier === undefined) {
    throw new RangeError("weight outside tier range");
      }
  return Math.max(tier.ratePerKg * weightKg * zoneCoefficient, MINIMUM_FEE);
}
```

In the second file, the `throw` line is indented less than the condition that holds it.
A reader looking at that line cannot tell from the indentation alone whether it sits
inside the condition or outside it. The same ambiguity is in the first entry of the
`WEIGHT_TIERS` array: the two entries sit at the same structural depth but in different
columns.

The difference between the two files is measurable. The first line of each file is the
marker carrying the filename, so it does not enter the comparison.

```sh
diff <(tail -n +2 tidy/fee.mjs) <(tail -n +2 messy/fee.mjs)
```

```
3c3
<   { upperLimitKg: 5, ratePerKg: 26 },
---
>     { upperLimitKg: 5, ratePerKg: 26 },
8,9c8,9
<   const tier = WEIGHT_TIERS.find((t) => weightKg <= t.upperLimitKg);
<   if (tier === undefined) {
---
>     const tier = WEIGHT_TIERS.find((t) => weightKg <= t.upperLimitKg);
>       if (tier === undefined) {
11c11
<   }
---
>       }
```

Eight diff lines, and none of them describe a behavior change. This is the first cost of
format disagreement: whoever fixes the indentation produces an eight-line diff without
changing any content, and someone else has to read that diff.

## The Tariff Table's Two Formats

The second measurement answers a more concrete question: how many diff lines does the
same **content** change produce in two different formats? The tariff module was written
in two formats. In the first, the equals signs are aligned and the tier table is packed
two entries per line; in the second, declarations carry a single space and every tier
sits on its own line.

```js
// aligned/tariff.mjs — aligned declarations, packed tier table
export const VOLUMETRIC_DIVISOR           = 3000;
export const MINIMUM_FEE                  = 52;
export const CONTRACTED_CUSTOMER_DISCOUNT = 0.15;
export const CURRENCY                     = "TL";

export const WEIGHT_TIERS = [
  { upperLimitKg:  1, ratePerKg: 38 }, { upperLimitKg:  5, ratePerKg: 26 },
  { upperLimitKg: 10, ratePerKg: 21 }, { upperLimitKg: 30, ratePerKg: 17 },
];
```

```js
// plain/tariff.mjs — single-space declarations, one tier per line
export const VOLUMETRIC_DIVISOR = 3000;
export const MINIMUM_FEE = 52;
export const CONTRACTED_CUSTOMER_DISCOUNT = 0.15;
export const CURRENCY = "TL";

export const WEIGHT_TIERS = [
  { upperLimitKg: 1, ratePerKg: 38 },
  { upperLimitKg: 5, ratePerKg: 26 },
  { upperLimitKg: 10, ratePerKg: 21 },
  { upperLimitKg: 30, ratePerKg: 17 },
];
```

The carrier revisited the tariff and three things changed: the rate for the five
kilogram tier dropped from 26 to 25, a new tier opened up to fifty kilograms, and the
`CONTRACTED_CUSTOMER_DISCOUNT` constant was renamed to `CONTRACT_DISCOUNT`. The same
three changes are applied to both files.

```js
// aligned/tariff-new.mjs — same revision, alignment preserved
export const VOLUMETRIC_DIVISOR = 3000;
export const MINIMUM_FEE        = 52;
export const CONTRACT_DISCOUNT  = 0.15;
export const CURRENCY           = "TL";

export const WEIGHT_TIERS = [
  { upperLimitKg:  1, ratePerKg: 38 }, { upperLimitKg:  5, ratePerKg: 25 },
  { upperLimitKg: 10, ratePerKg: 21 }, { upperLimitKg: 30, ratePerKg: 17 },
  { upperLimitKg: 50, ratePerKg: 14 },
];
```

```js
// plain/tariff-new.mjs — tariff revision applied
export const VOLUMETRIC_DIVISOR = 3000;
export const MINIMUM_FEE = 52;
export const CONTRACT_DISCOUNT = 0.15;
export const CURRENCY = "TL";

export const WEIGHT_TIERS = [
  { upperLimitKg: 1, ratePerKg: 38 },
  { upperLimitKg: 5, ratePerKg: 25 },
  { upperLimitKg: 10, ratePerKg: 21 },
  { upperLimitKg: 30, ratePerKg: 17 },
  { upperLimitKg: 50, ratePerKg: 14 },
];
```

In the aligned version, the shortened name moved the alignment column, so all four
declarations were rewritten. In the plain version, only the declaration whose name
actually changed was rewritten.

## The Diff Measurer

The tool below diffs each file pair and splits the diff lines in two. If a removed
line, once its whitespace is collapsed, also shows up among the added lines, that pair is
a **format-only** change; it carries no content. What remains is a content change.

```js
// diff-measure.mjs — how many of the diff lines between two files carry content
import { execSync } from "node:child_process";

// The first line is the filename marker; it does not enter the comparison.
const diff = (before, after) =>
  execSync(`diff <(tail -n +2 ${before}) <(tail -n +2 ${after}) || true`, { shell: "/bin/bash" })
    .toString().split("\n");
const content = (s) => s.slice(1).replace(/\s+/g, " ").trim();

const PAIRS = [
  ["tidy/fee.mjs", "messy/fee.mjs"],
  ["aligned/tariff.mjs", "aligned/tariff-new.mjs"],
  ["plain/tariff.mjs", "plain/tariff-new.mjs"],
];

for (const [before, after] of PAIRS) {
  const lines = diff(before, after);
  const removed = lines.filter((s) => s.startsWith("< ")).map(content);
  const added = lines.filter((s) => s.startsWith("> ")).map(content);
  const matched = removed.filter((s) => added.includes(s)).length;
  console.log(`${before.padEnd(18)} -> ${after.padEnd(22)} changed=${removed.length + added.length}` +
    `  format only=${matched * 2}  content=${removed.length + added.length - matched * 2}`);
}
```

```
tidy/fee.mjs       -> messy/fee.mjs          changed=8  format only=8  content=0
aligned/tariff.mjs -> aligned/tariff-new.mjs changed=11  format only=6  content=5
plain/tariff.mjs   -> plain/tariff-new.mjs   changed=5  format only=0  content=5
```

The three rows say the following. The indentation fix produces eight diff lines and all
eight are format; the content diff is zero. The tariff revision's content diff is the
same in both formats: five lines. In the aligned format, six more lines are added next to
those five.

## Whoever Reads the Diff

What the numbers mean is the job of whoever reads the diff. In the eleven-line diff, five
lines carry a decision and six do not; the reader does not know beforehand which is
which and has to compare all of them. In the five-line diff, there is no such sorting to
do.

The packed table has a second effect too. In the aligned version's diff output, the
changed table row carries two tiers at once: one changed, one did not. The reader has to
search character by character for where in the row the change sits. In the plain
version, the changed row carries a single tier, so the comparison ends with the row
itself.

This is where the format decision's measure comes from: **a line should carry one piece
of information, and one piece of information's format should not depend on its
neighbor.** Alignment breaks the second condition; when one name's length sets the
whitespace count on neighboring lines, a declaration whose name changes rewrites its
neighbors too. Packing more than one item per line breaks the first condition.

## The Format Decision Gets Made Once

The measurement does not say exactly which format to choose; whether indentation should
be two or four spaces does not show up in these numbers. What it says is that the
decision should be made **once, at the project level**, and not reopened file by file.
When two indentation habits live in the same project side by side, every file turns into
whoever last worked on it, and every such turn produces a diff line that carries no
content.

For this reason the format decision is a matter for a configuration file, not a
discussion. A tool from the formatter class applies the rules to the source
mechanically; the decision gets made once, and applying it is not left to the individual.
The format the tool picks may not line up with anyone's preference exactly; the measured
cost is larger than the cost of a preference mismatch.

## Summary

- The measure of a format decision is the number of diff lines it produces: every change
  produces a diff, and someone else reads that diff.
- A file with only its indentation broken produced eight diff lines and all eight were
  content-free; a format mismatch has a cost even when nothing changes.
- The same tariff revision produced five diff lines in the plain format and eleven in the
  aligned format; the content diff was five lines in both, the remaining six came only
  from alignment.
- A line should carry one piece of information, and a piece of information's format
  should not depend on the neighboring lines; alignment breaks the second condition,
  packing several items per line breaks the first.
- The format decision gets made once at the project level and applied mechanically; a
  decision remade file by file produces a content-free diff every time.

## Next Step

Format determines how code looks to the eye, not the structure itself. A properly
indented function can still be hard to read: nested conditions, multiple exit points, and
compound logical expressions do not get fixed by indentation. This difficulty has a
number too. The next lesson writes a measurer that counts a function's decision points,
compares cyclomatic complexity and the number of linearly independent paths across two
versions of the fee calculation, and then applies the transformations that lower the
number.
