Module 1: How to Approach a System Design Problem

6. Enlace in a single box

Description

The moment we've been deliberately postponing for five lessons has arrived: drawing. With clear requirements (lessons 2–4) and the estimation run (lesson 5), we finally execute step 3 of the framework —the high-level design— and give Enlace its first shape. And that shape is, deliberately, the humblest possible: a single box. An application server and a database, and nothing else. No cache, no replicas, no load balancer, no microservices. Not because we don't know Enlace will end up needing all of that —the next seven modules are going to add it—, but because the correct design starts simple and grows only when a number forces it. Starting with the most complex architecture you can think of is the novice mistake; starting with the simplest one that meets the requirements, and complicating it under numerical pressure, is the craft.

In this lesson you'll draw the two paths Enlace travels —the write one (shorten: a long URL arrives, a short code comes out) and the read one (resolve: a code arrives, a redirect comes out)— over the single box. You'll see, with order-of-magnitude numbers, what a single box can handle and what it can't: that the ~40 writes/s are plenty for it, that the ~4000 reads/s fit but without much margin, that the disk fills up in a few months, and that —most important— if that single box goes down, all of Enlace disappears. Those cracks aren't a design flaw: they're the map of what modules 4 to 7 fix. Lesson 7 will name them one by one; this one makes them visible by drawing the box and seeing where it creaks.

Connection to the module: this is the lesson where the method finally produces a design. It's step 3 applied to Enlace, resting on the requirements (step 1) and the estimation (step 2) of the previous lessons. And it's the antechamber of lesson 7, which takes this single box and points out exactly where it breaks —each crack mapped to a future module—. The mini-project (lesson 8) will ask you to draw your own single box for a new system. Here you learn to do it well: simple first, with the two paths clear, and honest about their limits.

The corner store

Think of it this way. You open a neighborhood store. You don't start by building a supermarket with twenty cash registers, three warehouses, and a fleet of trucks —you'd go broke before selling your first piece of gum—. You start with the minimum that works: a counter where you serve people and a back room where you keep the merchandise. You, behind the counter, do both things: when someone wants to buy, you look for the product in the back room and hand it to them (that's reading); when new merchandise arrives, you arrange it in the back room (that's writing). One counter, one back room, one person. And it works —it really works— as long as the neighborhood is small and the line short.

Notice why it's the correct decision to start: with few customers, that store serves them all with no problem, it cost almost nothing to set up, and it's very easy to understand and operate. It would be absurd to set up the twenty-register supermarket to sell to the ten people who pass by a day. Simplicity isn't a limitation here; it's the response appropriate to the size of the problem. The mistake would be the opposite: over-sizing from day one "in case the neighborhood grows".

But the one-person store has limits you can already anticipate, and they're exactly the ones Enlace's single box will have. If the neighborhood grows and a hundred customers arrive at once, a single person at a single counter can't keep up: the line becomes endless (the counter saturates). If the back room fills up, no more merchandise fits (the storage runs out). And —the most serious— if you, the only person, get sick one day, the store doesn't open: there's no one else to serve (a single point of failure). None of those problems exists when the neighborhood is small; they all appear on growth. And for each one there's a solution the store will adopt: hire more cashiers (more servers + load balancer), open a second back room (sharding), have a backup cashier (redundancy).

Enlace in a single box is that one-person store. A server (the counter, which serves requests) and a database (the back room, which stores the Links). It does the two operations —write when creating a link, read when resolving it— and works perfectly as long as the scale is modest. We start here on purpose, because it's the right thing to start with; and we draw its limits clearly, because they're the map of the rest of the guide.

It's worth spelling it out in full:

The high-level design starts with the simplest thing that meets the requirements —for Enlace, a single box: a server and a database—. It gets complicated only when a number forces it. Over-sizing from day one is as much a mistake as falling short.

The single-box architecture

Here is Enlace in its simplest form. Two components, and the client that uses them:

graph LR
    C[Client / Browser] -->|HTTP| S[Enlace server<br/>shorten / resolve]
    S -->|SQL| DB[(Database<br/>links table)]

That's all. The Enlace server receives the HTTP requests and runs the logic (shorten and resolve). The database stores the Link records in a table. The client —a browser, or whoever calls the API— talks to the server; the server talks to the database. Three boxes, two arrows. An anxious designer would want to put a cache, a load balancer, replicas in here; resist the temptation. The step-2 numbers don't demand any of that yet, and adding it now would be like setting up the supermarket to sell gum.

The table in the database is dead simple, a reflection of the Link record from lesson 1:

-- the links table: the heart of Enlace in a single box
CREATE TABLE links (
    short_code  VARCHAR(7)   PRIMARY KEY,   -- the code, indexed by being PK
    long_url    TEXT         NOT NULL,       -- the original URL (~500 bytes)
    created_at  TIMESTAMP    NOT NULL,
    expires_at  TIMESTAMP,                   -- NULL = doesn't expire
    clicks      BIGINT       DEFAULT 0       -- deferred in v1, but the field exists
);

Notice short_code VARCHAR(7) PRIMARY KEY. Being the primary key means the database maintains an index on it automatically. And that index is what makes resolve fast: looking up a row by its primary key doesn't scan the 6 billion records one by one, it jumps straight to the correct row in a few steps (under the hood, a B-tree the DB navigates in logarithmic time). Without that index, each resolution would be a disaster; with it, it's a near-instant lookup. This detail —the primary key gives the index, the index gives the read speed— is what lets a single box handle the 4000 reads/s, as we'll see.

The write path: shorten

When someone shortens a URL, the server travels this path:

sequenceDiagram
    participant C as Client
    participant S as Enlace server
    participant DB as Database
    C->>S: POST /shorten { long_url }
    S->>S: generate short_code (base62, 7 chars)
    S->>DB: INSERT INTO links (short_code, long_url, ...)
    DB-->>S: OK
    S-->>C: 201 { short_code: "aX9kR2q" }

Step by step: (1) the client sends the long URL; (2) the server generates a unique short_codehow it's generated without colliding is all of module 3, here "produces 7 base62 characters" is enough—; (3) the server inserts the Link record into the table; (4) it returns the code to the client. A single write to the database per shortening. At ~40 writes/s, this is a stroll for any database: it doesn't even break a sweat.

The read path: resolve

When someone visits a short link —the path that happens ~100 times more often—:

sequenceDiagram
    participant U as Browser
    participant S as Enlace server
    participant DB as Database
    U->>S: GET /aX9kR2q
    S->>DB: SELECT long_url FROM links WHERE short_code = 'aX9kR2q'
    DB-->>S: long_url (primary-key index lookup)
    S-->>U: 302 Location: https://...long-url
    U->>U: the browser follows the redirect

Step by step: (1) the browser requests the short code; (2) the server looks up the row by short_code —primary-key index lookup, very fast—; (3) it obtains the long_url; (4) it responds with a redirect (302) pointing at the long URL; (5) the browser follows it. A single read to the database per resolution. This is Enlace's hot path —~4000 times per second— and it's the one that, on growth, will scream for a cache (module 4). For now, with the index, the single box sustains it.

What a single box can handle (and what it can't)

This is where the design gets honest. A single box doesn't handle infinity, and a good designer knows exactly where it starts to creak. Let's compute the three limits with back-of-the-envelope arithmetic:

# how far does a single box go for Enlace?
GB = 1e9
TB = 1e12
seconds_per_month = 30 * 24 * 3600

# --- 1. Writes: ~40/s. Can a DB handle them? Plenty. ---
qps_write = 100_000_000 / seconds_per_month     # ~38.6/s

# --- 2. Reads: ~4000/s. Can a server with an index handle them? ---
qps_read = qps_write * 100                       # ~3858/s
# an indexed point-lookup costs tenths of a ms; at 0.5 ms/lookup:
lookups_per_core = 1000 / 0.5                     # 2000/s per core
cores_needed = qps_read / lookups_per_core

# --- 3. Storage: how much does it grow and when does a disk fill? ---
growth_per_month = 100_000_000 * 1024            # bytes/month
for disk_tb in [1, 4, 8]:
    months_to_fill = disk_tb * TB / growth_per_month
    print(f"{disk_tb} TB disk fills in {months_to_fill:4.1f} months "
          f"({months_to_fill/12:.1f} years)")

print(f"\nwrites/s = {qps_write:.0f}  -> trivial for a DB")
print(f"reads/s  = {qps_read:.0f}  -> ~{cores_needed:.0f} cores with an index: fits, without much margin")
print(f"growth   = {growth_per_month/GB:.0f} GB/month = {growth_per_month*12/TB:.2f} TB/year")

What to expect. Running this with Python 3.14.0:

1 TB disk fills in  9.8 months (0.8 years)
4 TB disk fills in 39.1 months (3.3 years)
8 TB disk fills in 78.1 months (6.5 years)

writes/s = 39  -> trivial for a DB
reads/s  = 3858  -> ~2 cores with an index: fits, without much margin
growth   = 102 GB/month = 1.23 TB/year

Let's read the three limits calmly, because they're the verdict on the single box:

  • Writes: plenty. ~40/s is ridiculously little for a modern database, which handles thousands of writes/s without sweating. Enlace's write path will never be the problem in a single box. Good: no need to touch it.
  • Reads: fit, but tight. ~4000 reads/s, with index lookup, are sustained by a server of a few cores. They fit. But notice "without much margin": there's no slack for spikes (a link that goes viral), or for growth. And all those reads hit the same DB. This is where, on growth, the cache comes in (module 4): serving 90% of the reads from memory frees the DB almost entirely.
  • Storage: it fills up, and soon. Enlace grows ~102 GB a month, 1.23 TB a year. A 1 TB disk fills in 10 months; an 8 TB one, in 6.5 years. That is: a single box with a large disk lasts a couple of years, but the 6 TB at 5 years no longer fit comfortably on a single machine forever. This is where replicas and sharding come in (module 5).

And there's a missing limit that no capacity calculation shows but that is the most serious of all: the single box is a single point of failure. If that server reboots, runs out of memory, or its disk fails, all of Enlace disappears —there aren't 40 writes/s or 0, there's service or no service—. With a target of 99.9% availability (remember: at most 8.76 hours of downtime a year), a single box doesn't guarantee it: a single machine fails more than that. This is where redundancy and failover come in (module 7).

graph TD
    Box[Enlace in a single box] --> W[Writes ~40/s<br/>PLENTY -> untouched]
    Box --> R[Reads ~4000/s<br/>FITS tight -> cache M4]
    Box --> St[Storage 6 TB<br/>FILLS UP -> replicas/sharding M5]
    Box --> F[Single point of failure<br/>DOESN'T meet 99.9% -> redundancy M7]

Notice the elegance of this: the single box, drawn honestly, becomes the index of the next four modules. Each limit we find is a module that resolves it. That's the journey of the guide, and it starts here, in the simplest possible design seen with critical eyes.

Why we don't start with the complex design

You could object: "if we know Enlace is going to need cache, replicas, sharding, and balancing, why not draw them now and save the steps?". It's a real temptation and worth answering, because the answer is one of the most important lessons of the craft.

First, because the complex design might be mis-sized. Without having gone through the limits of the single box, you wouldn't know how much cache, how many replicas, how many shards. You'd end up copying a generic eight-node architecture that maybe is too much or too little for the real numbers. Complexity is added by measuring where the simple box creaks, not in advance.

Second, because each piece of complexity has a cost. A cache can serve stale data (invalidation). Replicas introduce propagation delay (lag). Sharding complicates queries. Balancing adds a layer that can fail. Each one solves a problem and creates a new one. Adding them all from day one is carrying all those new problems before having the old problems that justify them. The single box has none of those costs —and that's why it's the correct starting point—.

Third, because simple is easier to understand, operate, and debug. When something fails in the single box, there's a server and a DB to check. When something fails in a twenty-component system, the failure can be in any of them or in how they interact. Simplicity isn't just elegant; it's operationally cheaper. You pay complexity when the scale demands it, not before.

The rule, then, is the discipline that runs through all of systems engineering: add complexity only when a number proves that simplicity is no longer enough. Enlace's single box is the embodiment of that rule. It meets all the functional requirements today, sustains the current scale (tight but real), and its cracks are drawn with names and surnames. When module 4 adds the cache, it'll be because the 4000 reads/s demand it —not "because big systems have caches"—.

Common mistakes

Starting with the most complex architecture "because it looks more professional". What happens: someone draws microservices, queues, multiple caches, and sharding for Enlace right away, without having estimated anything. The design impresses but isn't justified: they don't know whether those eight shards are too many or too few. Why it happens: complexity is confused with competence; a single box "looks like little". How to detect it: if your first diagram has more than three or four boxes and you can't justify each one with a number, you started too high. How to fix it: always start with the single box (or the minimal equivalent), verify it meets the requirements, and add complexity only when a step-2 number demands it. Simple first, always.

Drawing the single box but not knowing its limits. What happens: someone draws the server and the DB, says "done, here's Enlace", and can't answer "up to how many operations does it hold? when does the disk fill? what happens if the server goes down?". The design is incomplete not because of what's left to draw, but because of what they don't know about what they drew. Why it happens: "I drew the diagram" is confused with "I understand the system". How to detect it: if you can't name the three or four points where your design breaks, you don't fully understand it. How to fix it: for each box, ask yourself "what saturates it, what fills it, what happens if it dies?". Drawing the box is the easy step; knowing its cracks is the real design —and it's what lesson 7 does—.

Forgetting the index (or not knowing why the read is fast). What happens: someone draws the single box and assumes resolve is fast "just because", without realizing the speed depends on short_code being indexed. If by carelessness the search doesn't use the index, each resolve scans the entire table —6 billion rows— and the system crawls, even with little load. Why it happens: the index is invisible in the box diagram; it's taken for granted. How to detect it: if you can't explain why looking up a short_code is fast, you're missing understanding the mechanism. How to fix it: remember that PRIMARY KEY creates the index, and that without an index a search is a full scan. Module 3 goes into this in depth; for now, be clear that the single box's read speed depends on the index, it's not free.

Exercises

Exercise 1 — Draw the missing path. The read path (resolve) above responds with a 302. Draw (in ASCII or by describing the steps) how the sequence diagram would change if the link doesn't exist (nonexistent code). What does the server return to the client, and does it touch the database or not?

See solution

The path of a nonexistent code:

Browser  -> Server:   GET /doesnotexist
Server   -> DB:       SELECT long_url FROM links WHERE short_code = 'doesnotexist'
DB       -> Server:   (0 rows: none with that code)
Server   -> Browser:  404 Not Found

Yes, it touches the database: the server has to ask to know the code doesn't exist (the DB responds "zero rows"). Only then does it return a 404. Notice an interesting design detail that will reappear with the cache: looking up a nonexistent code also costs a DB query, even though it returns nothing. If someone bombarded Enlace with made-up codes, each one would spend a read —a real design problem (sometimes even the "doesn't exist" responses are cached to avoid it), but that's material for module 4—. For now, what matters: the 404 is part of the resolve contract (functional requirement F3), and yes, it queries the DB.

Exercise 2 — Which limit breaks first? Imagine Enlace, in its single box, grows faster than expected. For each scenario, say which of the four limits (writes, reads, storage, single point of failure) breaks first and which future module it sends you to: (a) A link goes viral and receives 50,000 visits/s for an hour. (b) Enlace has been running for three years and the 4 TB disk is almost full. (c) The server reboots for an update and Enlace is inaccessible for 5 minutes. (d) The company doubles its marketing and now 200 million new URLs/month come in.

See solution
  • (a) Viral, 50,000 visits/s. The reads limit breaks: 50,000/s is well above the ~4000/s the single box sustains. The DB saturates. → Module 4 (cache): serving the viral link from memory offloads the DB almost entirely.
  • (b) 4 TB disk almost full at 3 years. The storage breaks (matches our calculation: 4 TB fills in ~3.3 years). → Module 5 (replicas/sharding): spread the data across several machines.
  • (c) Reboot, 5 minutes inaccessible. It's the single point of failure: without redundancy, any stop of the only server brings Enlace down. Five minutes per reboot, repeated, eat the 99.9% budget. → Module 7 (redundancy and failover).
  • (d) 200M URLs/month. It raises the writes to ~80/s (still trivial) but doubles the reads to ~8000/s and doubles the disk growth to ~2.5 TB/year. The reads break first (and then the storage faster). → Modules 4 and 5.

The lesson: each way of growing stresses a different limit, and each limit has its module. The single box, seen honestly, already tells you where you'll have to go depending on how the system grows. Designing is anticipating these cracks, not pretending they don't exist.

Exercise 3 — Design another system's single box. Apply step 3 (high-level design in a single box) to this prompt: "Design a polls service: people create a poll with options and share a link; voters open the link and vote for an option; anyone can see the results." Draw (in ASCII or by describing) the single box —what components, what table(s)—, and identify which of its paths (create, vote, see results) will be the hottest and why.

See solution

The polls service's single box, with the same shape as Enlace (server + DB):

Client  --HTTP-->  Polls server       --SQL-->  Database
                   (create / vote / results)     tables: polls, options, votes

A reasonable data model:

CREATE TABLE polls   ( poll_id TEXT PRIMARY KEY, question TEXT, created_at TIMESTAMP );
CREATE TABLE options ( option_id TEXT PRIMARY KEY, poll_id TEXT, label TEXT );
CREATE TABLE votes   ( vote_id TEXT PRIMARY KEY, option_id TEXT, voted_at TIMESTAMP );

The three paths:

  • Create poll (write): rare, a few per user. Like shorten in Enlace: calm.
  • Vote (write): more frequent —many voters per poll— but bounded (each person votes once per poll).
  • See results (read): the hottest, almost certainly. People refresh the results many more times than they vote (you check how the poll is going over and over). It's a read-heavy pattern, just like Enlace.

The hottest path is see results, and for the same reason as in Enlace: it's read much more than it's written. This anticipates that, on growth, "see results" will call for a cache —and that counting votes in real time, if added, would be this system's "hiking boots" (like click analytics in Enlace), because it would add load on every view—. Notice how the method transfers: different domain, same single-box structure, same analysis of which path is the hot one.

Summary and next step

In this lesson you executed step 3 of the framework and gave Enlace its first shape: a single box —a server and a database, like the one-person neighborhood store—. You drew its two paths: the write one (shorten: generate code, insert record) and the read one (resolve: look up by index, redirect with 302), and you saw that the read speed depends on the primary-key index on short_code, it's not free.

And —most important— you drew the box honestly, measuring its four limits: the ~40 writes/s are plenty (untouched), the ~4000 reads/s fit but tight (cache, M4), the storage grows 1.23 TB/year and fills up soon (replicas/sharding, M5), and it's a single point of failure that doesn't meet 99.9% (redundancy, M7). Those cracks aren't defects: they're the index of the rest of the guide. And you saw why you start simple —the complex design might be mis-sized, each piece has a cost, and simple is easier to operate—: you add complexity only when a number demands it.

Before moving on you should be able to: draw Enlace's single box with its two paths; explain why resolve is fast (the index); name the four limits of the single box and which module each sends you to; and defend why you start simple instead of with the complex architecture.

What comes next is sharpening the mindset that turns these limits into decisions. Lesson 7 installs the central idea of the craft —there's no correct answer, there are tradeoffs— and takes the single box to point out, one by one, where it breaks and what you gain and pay when you fix it. It's the bridge between "I understood and drew the problem" and "I know how to reason about its decisions", and the direct antechamber of the mini-project.

Resources

  • System Design Primer — "Step 2: Create a high level design" — the step of drawing the high-level architecture with the main components, exactly what we did with the single box. The Primer insists on starting simple and justifying each component, the same discipline as this lesson.
  • PostgreSQL — "Indexes" — why a primary-key lookup is fast and one without an index is a full table scan. It's the mechanism that makes the single box sustain the 4000 reads/s; module 3 revisits it in depth.
  • Designing Data-Intensive Applications (DDIA), Chapter 1 — official site — Kleppmann's discussion of why simplicity and evolvability are first-class requirements. It's the theoretical foundation of "start with one box and complicate only when a number demands it".
  • MDN — "Redirections in HTTP" — the complete reference for how HTTP redirects work (301, 302, the Location header), which is what Enlace's read path returns. Useful for understanding exactly what the browser does when it receives the 302.