Skip to content
academia.sh

Lesson 01 / 13

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.

Contents

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.

// 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.

// 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.

// 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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close