---
title: 'Hollywood Principle'
source: 'https://academia.sh/en/courses/design-principles/hollywood-principle'
course: 'Design Principles'
language: en
updated: '2026-08-23T07:01:17+00:00'
license: 'CC BY-SA 4.0'
---

# Hollywood Principle

Reversing the direction between caller and callee: comparing an arrangement where three separate callers write the step order against one where the steps register themselves with a skeleton, counting how many files know the order, and measuring the files edited and lines touched when a new step is added.

In the previous lesson's abstract layout, the composition root decided which price list to
use, but it still called the clients itself. The order and count of the steps lived in the
caller's body. Adding a new step to this flow required editing the caller's body every time.

**The Hollywood principle** reverses the direction: "do not call us, we will call you." Parts do
not call the high-level flow; the high-level flow gives the parts a point to be called from.
**Inversion of control**, introduced in the Asynchronous JavaScript and the Runtime course, is
the general name for this principle; what is measured here is a specific form of it — which
module the step order is written in.

## Arrangement Where the Caller Writes the Order

The fee calculation consists of three steps: the base fee, the zone factor, the contracted
customer discount. Three separate callers write this order in their own body.

```sh
mkdir -p caller skeleton
```

```js
// records.mjs — sample shipments used by both arrangements
export const RECORDS = [
  { code: "GN-1", weight: 0.8, address: "34100", contracted: false },
  { code: "GN-2", weight: 3.0, address: "06500", contracted: true },
  { code: "GN-3", weight: 12.0, address: "65200", contracted: true },
];
```

```js
// caller/steps.mjs — steps; they do not know the order, they do not know the caller either
const TIER = [[1, 3900], [5, 6400], [20, 11800]];
const ZONE = { "34": 100, "06": 115, "65": 140 };

export const base = (cents, s) => TIER.find(([max]) => s.weight <= max)?.[1] ?? 15000;
export const zone = (cents, s) => Math.round((cents * (ZONE[s.address.slice(0, 2)] ?? 160)) / 100);
export const discount = (cents, s) => Math.round((cents * (s.contracted ? 90 : 100)) / 100);
```

```js
// caller/invoice.mjs — the caller writes the order
import { base, zone, discount } from "./steps.mjs";

export function invoice(shipment) {
  let cents = base(0, shipment);
  cents = zone(cents, shipment);
  cents = discount(cents, shipment);
  return `${shipment.code} ${cents} cents`;
}
```

```js
// caller/preview.mjs — writes the same order a second time
import { base, zone, discount } from "./steps.mjs";

export function preview(shipment) {
  let cents = base(0, shipment);
  cents = zone(cents, shipment);
  cents = discount(cents, shipment);
  return `~${cents}`;
}
```

```js
// caller/batch.mjs — writes the same order a third time
import { base, zone, discount } from "./steps.mjs";

export function total(shipments) {
  let grandTotal = 0;
  for (const s of shipments) {
    let cents = base(0, s);
    cents = zone(cents, s);
    cents = discount(cents, s);
    grandTotal += cents;
  }
  return `TOTAL ${grandTotal} cents`;
}
```

```js
// caller/setup.mjs — composition root
import { RECORDS } from "../records.mjs";
import { invoice } from "./invoice.mjs";
import { preview } from "./preview.mjs";
import { total } from "./batch.mjs";

console.log(invoice(RECORDS[1]), preview(RECORDS[1]), total(RECORDS));
```

## Arrangement Where the Skeleton Knows the Order

In the second arrangement, the order lives in a skeleton. The steps do not call the skeleton;
they register themselves with it and are called when their turn comes.

```js
// skeleton/pipeline.mjs — skeleton: calls the steps in order, does not know their names in advance
const STEPS = [];

export const addStep = (name, fn) => STEPS.push({ name, fn });
export const stepNames = () => STEPS.map((s) => s.name);

export function run(shipment) {
  let cents = 0;
  for (const s of STEPS) cents = s.fn(cents, shipment);
  return cents;
}
```

```js
// skeleton/steps.mjs — steps register themselves with the skeleton; they do not call it
import { addStep } from "./pipeline.mjs";

const TIER = [[1, 3900], [5, 6400], [20, 11800]];
const ZONE = { "34": 100, "06": 115, "65": 140 };

addStep("base", (cents, s) => TIER.find(([max]) => s.weight <= max)?.[1] ?? 15000);
addStep("zone", (cents, s) => Math.round((cents * (ZONE[s.address.slice(0, 2)] ?? 160)) / 100));
addStep("discount", (cents, s) => Math.round((cents * (s.contracted ? 90 : 100)) / 100));
```

```js
// skeleton/invoice.mjs — does not know the order, calls the skeleton
import { run } from "./pipeline.mjs";

export const invoice = (shipment) => `${shipment.code} ${run(shipment)} cents`;
```

```js
// skeleton/preview.mjs — does not know the order
import { run } from "./pipeline.mjs";

export const preview = (shipment) => `~${run(shipment)}`;
```

```js
// skeleton/batch.mjs — does not know the order
import { run } from "./pipeline.mjs";

export const total = (shipments) =>
  `TOTAL ${shipments.reduce((t, s) => t + run(s), 0)} cents`;
```

```js
// skeleton/setup.mjs — composition root: loads the steps, then calls the clients
import "./steps.mjs";
import { RECORDS } from "../records.mjs";
import { stepNames } from "./pipeline.mjs";
import { invoice } from "./invoice.mjs";
import { preview } from "./preview.mjs";
import { total } from "./batch.mjs";

console.log(invoice(RECORDS[1]), preview(RECORDS[1]), total(RECORDS));
console.log("steps:", stepNames().join(" -> "));
```

```sh
node caller/setup.mjs
node skeleton/setup.mjs
```

```
GN-2 6624 cents ~6624 TOTAL 25392 cents
GN-2 6624 cents ~6624 TOTAL 25392 cents
steps: base -> zone -> discount
```

The same result. The difference is what shows up in the `stepNames()` line: in the second
arrangement, the step list is data that can be queried at run time; in the first, it is a
control flow scattered across three separate bodies.

## The Cost of a New Step

An insurance premium step is being added: the amount rises by two percent after the discount.
In the first arrangement, a new function is written and added to three callers' bodies; in
the second, a single registration line is enough. A backup extension is given for the
in-place edit; GNU and BSD `sed` behave the same way in this form.

```sh
cp -r caller caller-new
cp -r skeleton skeleton-new

cat >> caller-new/steps.mjs <<'EOF'
export const insurance = (cents) => Math.round((cents * 102) / 100);
EOF
sed -i.y 's/base, zone, discount/base, zone, discount, insurance/' caller-new/*.mjs
sed -i.y 's/^  cents = discount(cents, shipment);/&\n  cents = insurance(cents);/' caller-new/*.mjs
sed -i.y 's/^    cents = discount(cents, s);/&\n    cents = insurance(cents);/' caller-new/*.mjs
rm -f caller-new/*.y

cat >> skeleton-new/steps.mjs <<'EOF'
addStep("insurance", (cents) => Math.round((cents * 102) / 100));
EOF

node caller-new/setup.mjs
node skeleton-new/setup.mjs
for k in caller skeleton; do
  echo "$k: files edited = $(diff -rq $k $k-new | grep -c '^Files')" \
    " lines touched = $(diff -rU0 $k $k-new | grep -cE '^[+-][^+-]')" \
    " files that know the order = $(grep -l 'cents = ' $k/*.mjs | wc -l | tr -d ' ')"
done
```

```
GN-2 6756 cents ~6756 TOTAL 25899 cents
GN-2 6756 cents ~6756 TOTAL 25899 cents
steps: base -> zone -> discount -> insurance
caller: files edited = 4  lines touched = 10  files that know the order = 3
skeleton: files edited = 1  lines touched = 1  files that know the order = 1
```

Both arrangements produce the same new result. The cost is four files and ten lines against
one file and one line. The last column gives the reason: the first arrangement had three
files that knew the order, the second had one.

The measure says something different from what the open–closed principle measures. There,
the cost of adding a new **type** was counted; here, what is counted is the cost of adding a
new **step** to an existing flow. Both point in the same direction: the change lands on data,
not on text.

## Reversing the Direction

The difference between the two arrangements also shows up in the import graph. In the first
arrangement, `invoice.mjs` imports the steps and calls them; control flows top-down. In the
second, `steps.mjs` imports the skeleton and registers with it; the call direction is built
bottom-up, while the execution direction stays top-down. The principle carries this name
because the relationship between caller and callee is reversed.

This reversal is not the same thing as dependency inversion. There, the question was which
side the abstraction is **defined** on; here, the question is which side the control flow is
**written** on. The two principles are used together: the skeleton defines the steps'
contract (dependency inversion) and also holds the order in which the steps are called
(Hollywood principle).

## The Cost of the Skeleton

The reversal's cost is paid in readability. In the first arrangement, reading a single body
is enough to see the flow; in the second, the order depends on the run-time arrangement of the
registration calls, and that arrangement comes from the composition root's import order. A
query like `stepNames()` is therefore necessary: once the order becomes data, that data has to
be readable.

The second cost is in debugging. When a step produces a wrong result, the call chain shows up
directly in the stack trace in the first arrangement; in the second, the skeleton's loop gets
in the way. The criterion is again a number: if the number of files that know the order is
greater than one, and requests to add a step to the flow keep recurring, the skeleton wins; if
the flow is fixed and there is a single caller, the skeleton is only a layer of indirection.

## Summary

- The Hollywood principle reverses the direction between caller and callee: parts do not call
  the flow, the flow gives parts a point to be called from.
- The measure is how many files the step order is written in: 3 files knew the order in the
  caller arrangement, 1 in the skeleton arrangement.
- When a new step was added to the flow, the caller arrangement had 4 files and 10 lines
  edited, the skeleton arrangement had 1 file and 1 line.
- In the skeleton arrangement, the step list is data that can be queried at run time; in the
  caller arrangement, it is a control flow scattered across three separate bodies.
- The cost is in readability and debugging; with a single caller and a fixed flow, the
  skeleton only adds a layer of indirection.

## Next Step

Every measure up to this point has been about **pairs of modules**: the strength of the bond
between two modules, how related the names inside one module are, how many objects a call
passes through, the file set a change touches. Once a codebase grows, the real question
changes shape — it is no longer which module binds to which, but where the boundary **gets
drawn**. The answer to that question is hidden in how often decisions change — which decision
is a stable policy that stands for years, and which is a detail that moves every time the
price list is renewed. The next topic builds this distinction and measures both sides of the
boundary by how often they change.
