Drawing the First Diagram

Client, load balancer, one stateless service, one datastore — then grow it only when a requirement points at the next box.

Syntaxclient -> load balancer -> stateless service -> datastore

The first diagram is four boxes: client, load balancer, one stateless service, one datastore. Draw those, label the arrows, and walk a single request from the client to the datastore and back before adding anything else.

Why starting small reads as senior

A design that starts simple and grows under questioning shows that each addition was caused by a requirement. The cache arrives because reads outnumber writes 50 to 1 and the p99 target is 150ms. The queue arrives because the upload has to return before the thumbnail exists. Object storage arrives because the payload is several megabytes and does not belong in a row. A diagram that lands complete at minute eight — twelve services, a broker, a search cluster — reads as memorised, and it invites the only question that matters: why is that there? You have to defend every box you draw, so draw the ones you can defend.

Label the arrows

An unlabelled arrow is where interviews go wrong quietly. Each one needs a direction, a protocol, and whether the caller waits for the answer. "The service writes to the database" and "the service enqueues a job a worker will pick up later" are different systems with different failure modes, and on a whiteboard they are the same pencil stroke.

Say that the service tier is stateless, and say it early. That one property is what lets you answer the scaling question later by adding instances rather than by redesigning.

Example

Example · javascript
// The first diagram, written down so no arrow can stay unlabelled.
const nodes = [
  { id: 'client', kind: 'client' },
  { id: 'lb',     kind: 'load-balancer', healthCheck: 'GET /healthz' },
  // Stateless is the load-bearing word: scaling later means raising `instances`.
  { id: 'api',    kind: 'service', stateless: true, instances: 3 },
  { id: 'db',     kind: 'datastore', engine: 'relational', role: 'primary' },
];

// Every edge carries direction, protocol, and whether the caller blocks.
const edges = [
  { from: 'client', to: 'lb',  protocol: 'HTTPS',    sync: true },
  { from: 'lb',     to: 'api', protocol: 'HTTP',     sync: true },
  { from: 'api',    to: 'db',  protocol: 'SQL/TCP',  sync: true, timeoutMs: 250 },
];

// Longhand: edges.filter(function (e) { return e.sync === true; })
// Shorthand: the arrow form. Same function. These are the edges whose failure
// fails the user's request — the list you will be asked about in the deep dive.
const onCriticalPath = edges.filter((e) => e.sync);

When to use it

  • Opening the high-level design phase without over-drawing
  • Justifying each component as the interviewer adds a requirement
  • Reviewing a teammate's architecture diagram for unlabelled or unowned arrows
  • Deciding whether a cache belongs in the first diagram or the third

More examples

Growing the diagram one requirement at a time

The `because` and `onFailure` fields are the two things interviewers probe on every added component. Writing them as you draw means the follow-up question is already answered.

Example · javascript
// Each push names the requirement that forced it. If you cannot write the
// `because`, do not draw the box.
nodes.push({ id: 'cache', kind: 'cache', because: 'read:write is 50:1, p99 target 150ms' });
edges.push({ from: 'api', to: 'cache', protocol: 'RESP', sync: true, timeoutMs: 50,
             onFailure: 'treat as a miss and read the database — never fail the request' });

nodes.push({ id: 'blobs', kind: 'object-storage', because: 'originals are 2-8MB each' });
// The upload bytes never pass through the API, so the edge starts at the client.
edges.push({ from: 'client', to: 'blobs', protocol: 'HTTPS', sync: true,
             note: 'client uploads directly with a pre-signed URL; the API only issues it' });

nodes.push({ id: 'queue', kind: 'queue', because: 'the resize must happen outside the request' });
nodes.push({ id: 'thumbs', kind: 'worker', because: 'upload must return before resizing finishes' });
edges.push({ from: 'api', to: 'queue', protocol: 'HTTPS', sync: false });
edges.push({ from: 'queue', to: 'thumbs', protocol: 'HTTPS', sync: false,
             note: 'at-least-once: the worker keys output by object id so a retry is harmless' });

Catching the box you cannot justify

Most over-engineered answers come from components added by reflex rather than by requirement. Run this check over a photo of your practice whiteboard; the boxes with no `because` are the ones that will be attacked.

Example · javascript
// A self-check for your own practice diagrams.
const components = [
  { id: 'cache',       because: 'read:write is 50:1 and p99 must stay under 150ms' },
  { id: 'read-replica',because: 'feed may be 30s stale, so reads can leave the primary' },
  { id: 'event-bus',   because: null }, // drawn out of habit
  { id: 'service-mesh',because: null }, // drawn to look thorough
];

const unjustified = components.filter((c) => !c.because).map((c) => c.id);
// ['event-bus', 'service-mesh'] -> erase them, or find the requirement first

Discussion

  • Be the first to comment on this lesson.