---
title: 'What Is System Design'
source: 'https://academia.sh/en/courses/introduction-to-system-design/what-is-system-design'
course: 'Introduction to System Design'
language: en
updated: '2026-08-23T07:01:26+00:00'
license: 'CC BY-SA 4.0'
---

# What Is System Design

Breaking a system design down into three kinds of decisions: component boundaries, the contracts that cross them, and the thresholds placed on qualities; separating the three number classes as the answer to where a threshold comes from, and showing that an unclassified number carries no decision.

The Architectural Styles course compared arrangements of the same shipment library and reduced
every comparison to three parts: quality scenario, threshold, and measured value. The thresholds
were given in that course; a number such as "at most 100 bytes cross the boundary" was an input to
the exercise. The course's closing sentence marked this: a style choice is still defended with the
same three parts here, and what changes is only where the thresholds come from.

This course opens with that question. In a system, a threshold is not arbitrary; it comes from
somewhere, and where it comes from can be written down. This lesson separates the kinds of
decisions a threshold depends on, and it sets the rule that holds through every lesson of the
course: every number in a design text states its class.

## Three Kinds of Decisions

**System design** is the work of determining how software split across multiple components is
divided, and the constraints that division brings. A program's parts run together and stop
together; a system's **components** can scale separately and fail separately. The difference
between these two sentences produces every question system design asks.

A design is made of three kinds of decisions, and each is justified separately.

A **component decision** states which work sits in which unit. Its measure is simple: whether a
component can scale on its own, and how much of the rest stays standing when it stops.

A **contract decision** states what crosses a component boundary: which fields, in which
direction, in what shape. Every field crossing the boundary is a bond between two components, and
changing it later requires changing both together.

A **quality decision** places a threshold on a quality attribute: the tracking query's response
time, the accepted rate of event loss, the hour by which the end-of-day job must finish. Quality
attribute and quality scenario were established in the Quality Attributes lesson of the Quality
and Testing Fundamentals course; they are not redefined here, only used as the carrier of a
threshold.

The first two kinds of decisions are made even on a single machine, and the Architectural Styles
course measured them there. The third is the decision that makes a system a system, because a
threshold comes from outside the software, not from inside it: from load, from expectation, and
from a fault-tolerance decision.

## Shipment Tracking as a System

The course works on a single system throughout: the **shipment tracking and pricing service**. A
user asks about a shipment's status with a tracking number, a carrier sends a state event as a
shipment moves, and a seller requests pricing at the end of the day. The three flows are kept
separate because each wants a separate threshold: the read flow is frequent and small, the write
flow is continuous and small, and the batch flow is infrequent and large.

The first version of the design can be written as a data structure. Once components, contracts,
and flows are listed, countable facts about the design come into view.

```js
// design/system.mjs — shipment tracking and pricing service: components, contracts, and three flows
export const COMPONENT = {
  "tracking-endpoint": "answers the tracking query",
  "event-receiver": "accepts the state event coming from the carrier",
  "shipment-store": "holds the shipment and delivery state",
  "tariff-rule": "calculates a shipment's fee",
  "batch-worker": "produces the seller's end-of-day pricing",
};

export const CONTRACT = {
  "tracking-request": { direction: "client -> tracking-endpoint", field: ["trackingNo"] },
  "tracking-response": { direction: "tracking-endpoint -> client", field: ["trackingNo", "state", "zone", "updatedAt"] },
  "state-event": { direction: "carrier -> event-receiver", field: ["trackingNo", "state", "route", "time"] },
  "state-read": { direction: "tracking-endpoint -> shipment-store", field: ["trackingNo"] },
  "state-write": { direction: "event-receiver -> shipment-store", field: ["trackingNo", "state", "route", "time"] },
  "day-scan": { direction: "batch-worker -> shipment-store", field: ["contractNo", "day"] },
  "fee-request": { direction: "batch-worker -> tariff-rule", field: ["weight", "zone", "rate"] },
};

export const FLOW = {
  read: { name: "tracking query", component: ["tracking-endpoint", "shipment-store"],
    contract: ["tracking-request", "state-read", "tracking-response"] },
  write: { name: "state event", component: ["event-receiver", "shipment-store"],
    contract: ["state-event", "state-write"] },
  batch: { name: "end-of-day pricing", component: ["batch-worker", "shipment-store", "tariff-rule"],
    contract: ["day-scan", "fee-request"] },
};
```

The first measure that comes out of this list is the **design surface**: how many components, how
many contracts, and how many fields the contracts carry.

```js
// design/surface.mjs — the components each flow touches, the contracts it crosses, and the fields it carries
import { COMPONENT, CONTRACT, FLOW } from "./system.mjs";

console.log(`component = ${Object.keys(COMPONENT).length}, contract = ${Object.keys(CONTRACT).length}`);
console.log(`${"flow".padEnd(23)}${"component".padStart(10)}${"contract".padStart(9)}${"field".padStart(6)}`);
for (const f of Object.values(FLOW)) {
  const field = f.contract.reduce((t, c) => t + CONTRACT[c].field.length, 0);
  console.log(`${f.name.padEnd(23)}${String(f.component.length).padStart(10)}${String(f.contract.length).padStart(9)}${String(field).padStart(6)}`);
}

for (const b of Object.keys(COMPONENT)) {
  const flows = Object.values(FLOW).filter((f) => f.component.includes(b)).map((f) => f.name);
  console.log(`${b.padEnd(17)} flows touching = ${flows.length}${flows.length > 1 ? ` (${flows.join(", ")})` : ""}`);
}
```

```
component = 5, contract = 7
flow                    component contract field
tracking query                  2        3     6
state event                     2        2     8
end-of-day pricing              3        2     5
tracking-endpoint flows touching = 1
event-receiver    flows touching = 1
shipment-store    flows touching = 3 (tracking query, state event, end-of-day pricing)
tariff-rule       flows touching = 1
batch-worker      flows touching = 1
```

The last lines give the design's most important fact: four of the five components serve a single
flow, while `shipment-store` serves all three. This component is where three separate thresholds
meet, and in the course's later topics every new constraint shows up here first. A design's most
fragile point is the component the most flows touch.

## Where the Threshold Comes From

A quality decision carries a number, and that number has a source. The source falls into one of
three classes; until the text states which one, the decision cannot be checked.

An **assumption** is a number chosen as an input to the design: daily active users, tracking
queries per user, record size. Its accuracy cannot be shown from inside the design, so its
rationale and what happens if it doubles are written down.

A **computed value** is a number that comes out of assumptions through arithmetic: requests per
second, daily data growth, the bandwidth required. A computed value is only as valid as the
assumptions it rests on.

A **measurement** is a number that comes out of an apparatus: how long a function takes on this
machine, the peak size of a buffer. A measurement is tied to its environment, and that dependence
is marked in the text.

The script below checks four decisions against these three classes. The fourth decision is left
without a class on purpose.

```js
// design/decision.mjs — the class of the number next to a decision: assumption, computed value, or measurement
const CLASS = ["assumption", "computed value", "measurement"];
const FEE_BUDGET_US = 50;                     // budget chosen for the tariff call (microseconds)

// Two assumptions; the full assumption table is built in the Back-of-the-Envelope Estimation lesson.
const V = { dailyUsers: 2_000_000, queriesPerUser: 6, dailyShipments: 400_000, eventsPerShipment: 7 };
const query = V.dailyUsers * V.queriesPerUser;
const event = V.dailyShipments * V.eventsPerShipment;

const TIER = [[1, 3000], [5, 4800], [20, 9600]];
const ZONE = { "34": 100, "06": 115, "35": 125 };
function tariff(s) {
  const tier = TIER.find(([k]) => s.weight <= k) ?? [0, 9600];
  const base = Math.max(2500, Math.round((tier[1] * ZONE[s.zone]) / 100));
  return base - Math.round(base * Math.min(s.rate, 0.4));
}

function medianMicroseconds(runs = 9) {
  const durations = [];
  for (let i = 0; i < runs; i += 1) {
    const start = process.hrtime.bigint();
    tariff({ weight: 4, zone: "35", rate: 0.15 });
    durations.push(Number(process.hrtime.bigint() - start) / 1000);
  }
  durations.sort((a, b) => a - b);
  return durations[Math.floor(runs / 2)];
}

const DECISION = [
  { name: "read scales independently of write", measure: "read/write request ratio",
    value: (query / event).toFixed(2), class: "computed value" },
  { name: "tracking response served from cache", measure: "cache hit ratio",
    value: "0.90", class: "assumption" },
  { name: "tariff rule called inside the process", measure: `${FEE_BUDGET_US} us budget`,
    value: medianMicroseconds() < FEE_BUDGET_US ? "passed" : "missed", class: "measurement" },
  { name: "batch job runs in the night window", measure: "window hours",
    value: "4", class: null },
];

console.log(`${"decision".padEnd(40)}${"measure".padEnd(27)}${"value".padStart(8)}  class`);
for (const k of DECISION) {
  console.log(`${k.name.padEnd(40)}${k.measure.padEnd(27)}${String(k.value).padStart(8)}  ${k.class ?? "-"}`);
}
const unclassified = DECISION.filter((k) => CLASS.includes(k.class) === false);
console.log(`\ndecisions carrying an unclassified number = ${unclassified.length}: ${unclassified.map((k) => k.name).join("; ")}`);
console.log(`daily tracking queries = ${query}, daily state events = ${event}`);
console.log(`if the assumption doubles: queries per user 12 -> ratio ${((query * 2) / event).toFixed(2)}`);
```

```
decision                                measure                       value  class
read scales independently of write      read/write request ratio       4.29  computed value
tracking response served from cache     cache hit ratio                0.90  assumption
tariff rule called inside the process   50 us budget                 passed  measurement
batch job runs in the night window      window hours                      4  -

decisions carrying an unclassified number = 1: batch job runs in the night window
daily tracking queries = 12000000, daily state events = 2800000
if the assumption doubles: queries per user 12 -> ratio 8.57
```

Four rows require four separate readings. The read/write request ratio of 4.29 is a **computed
value**: it is the quotient of two assumptions, and the last line shows its sensitivity — if
queries per user were 12 instead of 6, the ratio would be 8.57. The cache hit ratio of 0.90 is an
**assumption**; its rationale is that the same tracking number is asked several times before
delivery, and its accuracy can only be tested once the system is running. The tariff call staying
under budget is a **measurement**, and the output records a verdict rather than a raw duration,
because a raw duration is tied to this machine and this runtime version.

The fourth row has no class. "The batch job runs in a four-hour night window" reads as reasonable
in a design text, but it cannot be checked: it is unclear whether four hours is an assumption, a
computed value that follows from the seller's expectation, or a duration measured in some run.
An unclassified number carries no decision, in the same way an unmeasured axis carries no weight
in an argument.

## Component Decisions Depend on Quality Decisions

The three kinds of decisions are not made in sequence; they determine each other. The first
version of the component list comes from functional expectation: an endpoint that answers the
tracking query, a receiver that accepts the event, a store that holds the state. This list is not
an arrangement, only how the work is divided.

The component count grows past this list because of quality decisions. If the read flow's
threshold is to stay independent of the write flow's threshold, the two flows go into separate,
independently scalable components, and the component count rises. If the batch job's end-of-day
window must not disturb the read flow's response time, the batch job moves into its own unit.
Every such separation grows the contract count and the number of fields crossing a boundary; that
is the cost the Architectural Styles course measured.

The direction of the argument therefore reads backward: a component count cannot be defended
before the thresholds are known. Describing a design as "five components" is not a decision unless
it says which threshold forced which separation. The next two lessons open the source of a
threshold in order: first how requirements are gathered, then how the functional is treated as a
determinant separate from the non-functional.

## Summary

- System design is made of three kinds of decisions: component boundaries, the contracts that
  cross them, and the thresholds placed on qualities; the first two are made even on a single
  machine, the third comes from load and expectation.
- A design surface is countable: the example system has 5 components and 7 contracts; the tracking
  query carries 3 contracts and 6 fields, the state event 2 contracts and 8 fields, the end-of-day
  pricing 2 contracts and 5 fields.
- The component the most flows touch is the most fragile point: `shipment-store` sits in all three
  flows, and the other four components sit in one flow each.
- The number next to a decision falls into one of three classes: an assumption is chosen and its
  sensitivity is written down, a computed value comes out of assumptions, and a measurement comes
  out of an apparatus and marks its dependence on the environment.
- An unclassified number carries no decision; in the example, one of four decisions stayed
  unclassified and was counted as unverifiable.
- The component count grows from quality thresholds, not from the functional split; a component
  count cannot be defended before the thresholds are known.

## Next Step

This lesson said a threshold has a source, but it did not go looking for that source. The five
components and seven contracts in the example were given ready-made; a real design task instead
starts with a single-line request: "a service where sellers can track their shipments and be
billed at the end of the day." That line contains no component name, no number, no boundary. The
next lesson takes up how a design is reached from that line: which questions get asked, why every
question left unanswered turns into an assumption, and how much room scope narrowing opens on the
design surface.
