---
title: 'Approach to the Design Problem'
source: 'https://academia.sh/en/courses/introduction-to-system-design/approach-to-the-design-problem'
course: 'Introduction to System Design'
language: en
updated: '2026-08-23T07:01:25+00:00'
license: 'CC BY-SA 4.0'
---

# Approach to the Design Problem

Turning a single-line request into a design: separating questions into four families, showing which design step each family blocks, turning an unanswered question into an assumption, and measuring the room scope narrowing opens on the design surface.

The previous lesson handed over a five-component design ready-made and measured that design's
surface. Real design work does not start this way. The starting point is most often a single
sentence: "a service where sellers can track their shipments and be billed at the end of the
day." That sentence has no component name, no number, no boundary.

This lesson takes up the move from that sentence to a design. The move consists of two tasks:
finding the gaps by asking questions, and shrinking the design by narrowing the scope. Both
produce a measurable result — the first the number of assumptions that must be written, the
second the shrinkage in the design surface.

## The Request Is Incomplete, and the Gap Is Countable

The single-line request says three things: one actor (the seller), two capabilities (tracking,
pricing), and one timing (end of day). What it does not say is larger, and it does not scatter
randomly — it groups into four families.

**Actor and flow** questions define the outside of the system: who sends a request, who sends
data, who waits for the result. Their answers determine the component list.

**Volume** questions define the load: how many requests a day, how many records, how many bytes
per record. Without their answers, no calculation can be made.

**Threshold** questions define the acceptance criterion: how long a response can take, how much
loss is accepted, when the result must be ready. Their answers are the input to quality
decisions.

**Constraint** questions define the obligations coming from outside the design: how long data is
retained, where a rule changes, which component cannot be touched.

The reason this separation is useful is that each family blocks a specific design step. When the
question list is written together with this link, which step can start becomes something that
can be calculated.

```js
// design/question.mjs — the questions the problem line leaves open, and the step each question blocks
export const STEP = {
  "actor and flow": "the component list cannot be written",
  volume: "back-of-the-envelope estimation cannot start",
  threshold: "a quality decision cannot be made",
  constraint: "a data retention decision cannot be made",
};

export const QUESTION = [
  { family: "actor and flow", question: "who makes the tracking query", answer: "the recipient and the seller, with separate authority" },
  { family: "actor and flow", question: "who sends the state event", answer: "the carrier" },
  { family: "actor and flow", question: "who starts pricing", answer: "the seller, at day's end" },
  { family: "volume", question: "how many tracking queries arrive per day", answer: null },
  { family: "volume", question: "how many shipments are created per day", answer: null },
  { family: "volume", question: "how many state events arrive per shipment", answer: null },
  { family: "threshold", question: "how long can the tracking response take at most", answer: null },
  { family: "threshold", question: "is state event loss accepted", answer: "no, the event is resent" },
  { family: "threshold", question: "when must the fee report be ready", answer: null },
  { family: "constraint", question: "how long is a state record retained", answer: null },
  { family: "constraint", question: "in how many places does the tariff rule change", answer: "one place" },
];
```

```js
// design/requirement.mjs — answered questions, unanswered questions, and the number of assumptions that must be written
import { STEP, QUESTION } from "./question.mjs";

console.log(`${"question family".padEnd(19)}${"answer".padStart(6)}  blocked step`);
for (const [family, step] of Object.entries(STEP)) {
  const group = QUESTION.filter((q) => q.family === family);
  const answered = group.filter((q) => q.answer !== null).length;
  const block = answered === group.length ? "-" : step;
  console.log(`${family.padEnd(19)}${`${answered}/${group.length}`.padStart(6)}  ${block}`);
}

const unanswered = QUESTION.filter((q) => q.answer === null);
console.log(`\ntotal questions = ${QUESTION.length}, answered = ${QUESTION.length - unanswered.length}, unanswered = ${unanswered.length}`);
console.log(`assumptions that must be written = ${unanswered.length}`);
for (const q of unanswered) console.log(`  assumption needed: ${q.question}`);
```

```
question family    answer  blocked step
actor and flow        3/3  -
volume                0/3  back-of-the-envelope estimation cannot start
threshold             1/3  a quality decision cannot be made
constraint            1/2  a data retention decision cannot be made

total questions = 11, answered = 5, unanswered = 6
assumptions that must be written = 6
  assumption needed: how many tracking queries arrive per day
  assumption needed: how many shipments are created per day
  assumption needed: how many state events arrive per shipment
  assumption needed: how long can the tracking response take at most
  assumption needed: when must the fee report be ready
  assumption needed: how long is a state record retained
```

The most useful line in the output is the first table. Because the actor and flow family is
complete, the component list can be written; this is why the previous lesson's five components
could be written at all. In the volume family, the answer ratio is 0/3, so back-of-the-envelope
estimation cannot start. In the threshold family, 1/3 is answered, meaning two-thirds of the
quality decisions are pending.

## An Unanswered Question Turns Into an Assumption

The six unanswered questions correspond to six assumptions. This is the step required so that
design work does not stop: if a question cannot be answered, a number is chosen in its place,
that number is written as an **assumption**, and its rationale is given in one sentence.

An assumption's cost is not zero. Every computed value built on a wrong assumption is wrong by
the same factor, and the only way to make this visible is to write down the sensitivity: if the
assumption doubles, which computed value multiplies by how much. The previous lesson gave one
example of this — when queries per user were taken as 12 instead of 6, the read/write ratio rose
from 4.29 to 8.57.

An assumption must not be confused with a threshold. An assumption is a guess about the world
outside the system, and it is corrected by measurement. A threshold, on the other hand, is a
decision: it is the accepted limit, and it is not corrected by measurement — it changes through
discussion. "Twelve million queries arrive a day" is an assumption; "the tracking response does
not exceed 200 milliseconds" is a threshold.

## Narrowing the Scope

The second task is to leave a portion of the capabilities that surface while answering questions
outside the design. The rationale for narrowing is not time — it is measure: every capability
adds components, contracts, and fields to the design surface.

The list below carries seven scope items and each one's contribution to the surface. Three are
in, four are out; every item left out must carry a reason and a re-entry condition. The last item
is left unrecorded on purpose.

```js
// design/scope.mjs — each scope item's contribution to the design surface, and the record of narrowing
const ITEM = [
  { name: "tracking query", scope: "in",
    component: ["tracking-endpoint", "shipment-store"], contract: 3, field: 6 },
  { name: "state event acceptance", scope: "in",
    component: ["event-receiver", "shipment-store"], contract: 2, field: 8 },
  { name: "end-of-day pricing", scope: "in",
    component: ["batch-worker", "shipment-store", "tariff-rule"], contract: 2, field: 5 },
  { name: "recipient notification", scope: "out",
    component: ["notification-endpoint", "template-store"], contract: 3, field: 9,
    reason: "does not change the tracking query's threshold", reentry: "if every state event were to trigger a notification" },
  { name: "seller interface", scope: "out",
    component: ["interface-server", "session-store"], contract: 4, field: 14,
    reason: "the fee report can also be delivered as a file", reentry: "if the report is requested during the day" },
  { name: "carrier contract management", scope: "out",
    component: ["contract-endpoint"], contract: 2, field: 7,
    reason: "the tariff rule comes from the pricing library and does not change here", reentry: "if the tariff is versioned by zone" },
  { name: "route planning", scope: "out",
    component: ["route-planner", "location-receiver"], contract: 3, field: 11,
    reason: null, reentry: null },
];

function surface(items) {
  const component = new Set(items.flatMap((m) => m.component));
  return {
    component: component.size,
    contract: items.reduce((t, m) => t + m.contract, 0),
    field: items.reduce((t, m) => t + m.field, 0),
  };
}

const wide = surface(ITEM);
const narrow = surface(ITEM.filter((m) => m.scope === "in"));
console.log(`${"measure".padEnd(10)}${"wide".padStart(7)}${"narrow".padStart(8)}${"ratio".padStart(7)}`);
for (const a of ["component", "contract", "field"]) {
  console.log(`${a.padEnd(10)}${String(wide[a]).padStart(7)}${String(narrow[a]).padStart(8)}${(narrow[a] / wide[a]).toFixed(2).padStart(7)}`);
}

const outOfScope = ITEM.filter((m) => m.scope === "out");
const recorded = outOfScope.filter((m) => m.reason !== null && m.reentry !== null);
console.log(`\nout-of-scope items = ${outOfScope.length}, with reason and re-entry condition written = ${recorded.length}`);
for (const m of outOfScope.filter((x) => recorded.includes(x) === false)) {
  console.log(`  unrecorded narrowing: ${m.name} (${m.component.length} components, ${m.field} fields could come back)`);
}
for (const m of recorded) console.log(`  ${m.name.padEnd(30)} re-entry condition: ${m.reentry}`);
```

```
measure      wide  narrow  ratio
component      12       5   0.42
contract       19       7   0.37
field          60      19   0.32

out-of-scope items = 4, with reason and re-entry condition written = 3
  unrecorded narrowing: route planning (2 components, 11 fields could come back)
  recipient notification         re-entry condition: if every state event were to trigger a notification
  seller interface               re-entry condition: if the report is requested during the day
  carrier contract management    re-entry condition: if the tariff is versioned by zone
```

The gain from narrowing shows up in three numbers: components drop from 12 to 5, contracts from
19 to 7, fields crossing a boundary from 60 to 19. The three ratios differ from each other, and
that difference carries information. The component ratio is 0.42, the field ratio 0.32; that is,
the items left out were growing the contract surface more than the component count. Narrowing the
scope's biggest gain is not reducing the component count — it is reducing the number of bonds
between components.

It is not a coincidence that the third row matches the previous lesson's numbers: the narrowed
scope's surface is 5 components and 7 contracts, meaning the previous lesson's design is the
result of this narrowing.

## The Record of Narrowing

Three of the four out-of-scope items are recorded, one is not. The record consists of two fields,
and both are mandatory.

The **reason** states why the item was removed, and the correct reason always has the same shape:
which threshold this item does not change. Recipient notification was removed because it does not
change the tracking query's response time threshold; the seller interface was removed because the
fee report can also be delivered as a file.

The **re-entry condition** states under what circumstance the item re-enters scope. This field
protects the design going forward: if the condition is written down, it is known when the design
will reopen, and the decisions in between are made with that knowledge. Recipient notification's
condition is that every state event would trigger a notification; if that condition occurs, the
write flow's volume changes and the previous lesson's read/write ratio becomes invalid.

The item left unrecorded is a risk for exactly this reason. Route planning is out of scope, but
because why it was removed and when it will return are not written down, the design does not
account for it: when it comes back, 2 components and 11 fields are added to the surface all at
once. Leaving something out of scope is a decision, and like every decision it is written down
with its reason; an unwritten narrowing is indistinguishable from a forgotten capability.

## Summary

- A single-line request separates into four question families: actor and flow, volume,
  threshold, constraint; each family blocks a specific design step.
- In the example, 5 of 11 questions are answered: because actor and flow is 3/3, the component
  list could be written; because volume is 0/3, back-of-the-envelope estimation does not start.
- The 6 unanswered questions turn into 6 assumptions; until an assumption's rationale and
  sensitivity are written down, the computed value built on it cannot be checked.
- An assumption is a guess about the world and is corrected by measurement; a threshold is a
  decision and changes through discussion.
- Scope narrowing opened measurable room: components dropped from 12 to 5 (0.42), contracts from
  19 to 7 (0.37), fields crossing a boundary from 60 to 19 (0.32).
- An out-of-scope item must carry a reason and a re-entry condition; because one of the four items
  in the example stayed unrecorded, 2 components and 11 fields get added unaccounted-for when it
  comes back.

## Next Step

Two of the four families in the question list seem to ask the same thing: actor and flow
questions ask what the system will do, threshold questions ask how it will do it. Because these
two can be answered separately, they are separate determinants and are written separately. The
next lesson establishes this separation: functional requirements produce the component list,
non-functional requirements produce how many parts that list splits into. The two can be changed
independently, and the proof is measurable — the same functional list gives two different designs
with two different threshold sets.
