---
title: 'Pattern Concept'
source: 'https://academia.sh/en/courses/design-patterns/pattern-concept'
course: 'Design Patterns'
language: en
updated: '2026-08-23T07:01:14+00:00'
license: 'CC BY-SA 4.0'
---

# Pattern Concept

Defining a pattern as the triad of problem, solution, and consequences: applying a registry solution already familiar from two separate lessons to carrier selection in the shipping library, measuring the two versions that produce the same output by file count, import edge count, the number of spots where a type name appears, and the longest import path, and counting the gain and the cost separately.

The Design Principles course closed with a set of criteria: keep the boundary narrow, let
dependency point in the stable direction, keep what changes together in one place. None of
these sentences hands over a solution. A module failing the open–closed principle is
measurable, but the principle does not say what to write in that module's place. What decides
the answer is that the same problem has already been solved before.

When the same problem is solved the same way, repeatedly, across different code bases, that
solution has a name, a known form, and known consequences. A **pattern** is exactly this: not a
piece of code, but the named solution to a recurring problem, together with the consequences
that solution brings. This lesson defines the parts of that triad, applies a solution already
found twice before, in the shipping fee library, to carrier selection, and counts its
consequences.

## Finding the Same Solution Twice

In the Choosing the Right Structure lesson of the Clean Code course, a long conditional chain
had been moved into a table; the table was a **registry** that mapped a type name to a
behavior. In the Open–Closed Principle lesson of the Design Principles course, the same
structure showed up again: instead of resolving the tariff type with a `switch`, tariffs wrote
themselves into a registry, and the fee module never learned the type names at all.

The two lessons did not share a problem. The first wanted to lower one body's decision-point
count; the second wanted to stop a new type from forcing an edit to an existing file. Even so,
the solution they landed on had the same shape, letter for letter: a mapping table, a function
that writes to it, a function that reads from it, and a single composition root that fills it.
A solution found a second time, under a separate pressure, is the sign it deserves a name.

## The Three Parts of the Triad

A pattern's narrative has three parts, and each part answers a different question.

| Part | Its question | Counterpart in the registry solution |
|---|---|---|
| Problem | Under what pressure does it show up | The number of types is growing, and each new type forces an edit to existing files |
| Solution | Which pieces stand in which relationship | A name-behavior table, a function that writes, a function that reads, a composition root that fills it |
| Consequences | What it improves, what it worsens | The number of spots the type name appears in falls; the file count and the import edge count rise |

A **context** sentence sits in front of the triad: the condition under which the pattern
applies. The registry solution's context is that types are selected by name at run time. If
types are known at compile time and no selection ever happens, there is no problem, and
therefore no pattern.

Of the three parts, consequences are skipped most often. "Applying" a pattern is not building
its structure but accepting its consequences; a pattern cannot be chosen without counting them
first.

## Two Versions of Carrier Selection

The library has a new problem: for one shipment, several carrier offers need to be produced.
Each carrier has its own fee and its own transit time. In the first version, the carrier name
is resolved with two separate `switch` statements.

```js
// switch/carrier.mjs — carrier name resolved with two separate switches
export function fee(name, shipment) {
  switch (name) {
    case "domestic":
      return 3900 + Math.ceil(shipment.weight) * 900;
    case "express":
      return 6400 + Math.ceil(shipment.weight) * 1500;
    default:
      throw new RangeError(`unknown carrier: ${name}`);
  }
}

export function transitDays(name) {
  switch (name) {
    case "domestic":
      return 3;
    case "express":
      return 1;
    default:
      throw new RangeError(`unknown carrier: ${name}`);
  }
}
```

```js
// switch/offer.mjs — offer line; also counts the carrier names itself
import { fee, transitDays } from "./carrier.mjs";

export const NAMES = ["domestic", "express"];
export const line = (name, s) => `${name}: ${fee(name, s)} cents / ${transitDays(name)} day(s)`;
```

```js
// switch/main.mjs — writes every carrier offer for one example shipment
import { NAMES, line } from "./offer.mjs";

export const SHIPMENT = { weight: 2.4, address: "34100" };
for (const name of NAMES) console.log(line(name, SHIPMENT));
```

```sh
node switch/main.mjs
```

```
domestic: 6600 cents / 3 day(s)
express: 10900 cents / 1 day(s)
```

The second version applies the registry solution. Each carrier stands as an object in its own
file and writes itself into the registry.

```js
// registry/registry.mjs — carrier registry: every carrier writes itself into this table
const CARRIERS = new Map();

export function register(carrier) {
  CARRIERS.set(carrier.name, carrier);
}

export const names = () => [...CARRIERS.keys()];
export function carrier(name) {
  const c = CARRIERS.get(name);
  if (c === undefined) throw new RangeError(`unknown carrier: ${name}`);
  return c;
}
```

```js
// registry/carriers/domestic.mjs — domestic carrier
import { register } from "../registry.mjs";

register({
  name: "domestic",
  fee: (s) => 3900 + Math.ceil(s.weight) * 900,
  transitDays: () => 3,
});
```

```js
// registry/carriers/express.mjs — express carrier
import { register } from "../registry.mjs";

register({
  name: "express",
  fee: (s) => 6400 + Math.ceil(s.weight) * 1500,
  transitDays: () => 1,
});
```

```js
// registry/offer.mjs — offer line; no carrier name passes through it
import { carrier } from "./registry.mjs";

export const line = (name, s) => {
  const c = carrier(name);
  return `${name}: ${c.fee(s)} cents / ${c.transitDays()} day(s)`;
};
```

```js
// registry/main.mjs — composition root: only this file knows which carriers load
import "./carriers/domestic.mjs";
import "./carriers/express.mjs";
import { names } from "./registry.mjs";
import { line } from "./offer.mjs";

export const SHIPMENT = { weight: 2.4, address: "34100" };
for (const name of names()) console.log(line(name, SHIPMENT));
```

```sh
node registry/main.mjs
```

```
domestic: 6600 cents / 3 day(s)
express: 10900 cents / 1 day(s)
```

The outputs match. The only difference is in the consequences, and consequences can be counted.

## Counting the Consequences

The following measurer reads the modules under a directory and yields four numbers: file count,
import edge count, the number of spots where the given type names appear as string literals,
and the length of the longest path in the import graph. The first two numbers measure the
pattern's cost, the third its gain, the fourth its level of indirection.

```js
// measure.mjs — file, import edge, type-name and indirection measures of a design
import { readdirSync, readFileSync } from "node:fs";
import { dirname, join, relative, resolve } from "node:path";

const root = process.argv[2];
const TYPES = process.argv.slice(3);

const collect = (d) =>
  readdirSync(d, { withFileTypes: true }).flatMap((e) =>
    e.isDirectory() ? collect(join(d, e.name)) : e.name.endsWith(".mjs") ? [join(d, e.name)] : []);

const text = new Map(collect(root).sort().map((y) => [relative(root, y), readFileSync(y, "utf8")]));

const edge = new Map();
for (const [name, m] of text) {
  edge.set(name, [...m.matchAll(/(?:from|import)\s*["'](\.[^"']+)["']/g)]
    .map((x) => relative(root, resolve(dirname(join(root, name)), x[1]))));
}
const edgeCount = [...edge.values()].reduce((t, h) => t + h.length, 0);

let typeSpot = 0;
for (const [, m] of text)
  for (const t of TYPES) typeSpot += [...m.matchAll(new RegExp(`"${t}"`, "g"))].length;

const depth = (name, seen = new Set()) => {
  const next = (edge.get(name) ?? []).filter((h) => !seen.has(h));
  return next.length === 0 ? 0 : 1 + Math.max(...next.map((h) => depth(h, new Set([...seen, name]))));
};
const longest = Math.max(...[...edge.keys()].map((a) => depth(a)));

console.log(`${root.padEnd(9)} files=${text.size} import-edges=${edgeCount} ` +
  `type-name-spots=${typeSpot} longest-import-path=${longest}`);
```

```sh
node measure.mjs switch domestic express
node measure.mjs registry domestic express
```

```
switch    files=3 import-edges=2 type-name-spots=6 longest-import-path=2
registry  files=5 import-edges=7 type-name-spots=2 longest-import-path=2
```

The gain is in the third number. In the first version, the two carrier names appear in six
spots: each once in the two `switch` statements and once in the `NAMES` array. This is the
knowledge duplication defined in the Clean Code course; the carrier list is held separately in
three places, and nothing keeps the three in agreement. In the second version, each name
appears once, only in its own file.

The cost is in the first two numbers. File count went from three to five, import edge count
from two to seven. The second number is the same dependency count measured in the Component
Coupling Principles lesson: five edges were added to the graph, four of them into a single
file, the composition root.

The fourth number shows where the cost does not show up: the longest import path is 2 in both
versions. So the indirection grows not in path length between files but in resolution that
happens at run time. In the first version, which body a `fee` call reaches is found by reading
the file; in the second, the call's target depends on which files wrote into the registry, and
that information lives only in the composition root. A single measure cannot give a pattern's
full cost.

## Splitting by Problem Class

Patterns are separated by the problem type they solve, and this course takes its sections from
that split. **Creational patterns** solve who builds an object and how. **Structural patterns**
arrange how existing objects are combined. **Behavioral patterns** handle the distribution of
responsibility and communication among objects. **Enterprise application patterns** belong to
the boundary between business logic and persistence; some were already built in the Data
Access Layer and Business Logic course of the Backend Development curriculum, and here they
are taken up again at the pattern-catalog level.

The registry solution falls on the creational side, because what it solves is not behavior
itself but where the object carrying it comes from. The next two lessons ask this question more
sharply.

## Summary

- A pattern is not a piece of code but the named solution to a recurring problem; its narrative
  is the triad of problem, solution, and consequences, with a context condition in front of it.
- A solution counts as a pattern when the same structure is rediscovered under separate
  pressures; the registry solution came out identical for two separate problems in the Clean
  Code and Design Principles courses.
- Across two versions producing the same offer line, the spots where the type name appears fell
  from 6 to 2; this is the pattern's gain.
- In the same comparison, file count rose from 3 to 5 and import edge count from 2 to 7; this is
  the pattern's cost, read from the same measure set as the gain.
- The longest import path stayed at 2 in both versions: a pattern's indirection cost does not
  show up in a single measure, and run-time resolution of the call target must be accounted for
  separately.

## Next Step

In the registry version, the offer module does not know the carrier names, but it still asks,
through a string, which name corresponds to which object, and the registry itself builds the
object. When building the object does not fit in a single call — if a carrier has a fee
calculation, a route planner, and a label format — the thing written to the registry turns into
a construction procedure of its own. The next lesson pulls this construction into a method: it
defines the factory method, where the subtype decides which object to produce, adds the
abstract factory, which produces together the objects that must fit each other, and counts the
file and line count edited in both versions when a new carrier family is added.
