Module 1: How to Approach a System Design Problem

1. Module introduction: understand the problem before you draw

Description

By the end of this lesson you'll hold an idea that completely changes how you approach a design problem, and that almost nobody has at the start: your first job is not to design, it's to understand. When someone drops "design a URL shortener" on you, the temptation is to open the diagram editor and start placing boxes —a server, a database, an arrow—. That's the wrong temptation. That prompt is full of holes: it doesn't say how many people will use it, or what "fast" means, or whether you need to store who clicked, or how long the links live. Designing on top of those holes is building on sand. A good designer does the opposite: before drawing anything, they turn the vague prompt into concrete requirements —what the system has to do and how well— and only then start designing. This entire module is the training of that reflex.

You'll also meet Enlace, the system we work on across all eight modules: a URL shortener, the canonical problem of the craft, because it's so simple to state that it fits in one sentence and yet, once you scale it, it touches every fundamental —identifier generation, data model, read-heavy load, caching, replicas, sharding, load balancing, and the consistency tradeoffs—. You'll meet its central record, Link, its two operations, shorten and resolve, and —most important— its anchor numbers: the exact results (100 million a month, ~40 writes/s, ~4000 reads/s, ~6 TB, 62⁷ ≈ 3.52 × 10¹²) that we'll reproduce over and over and that work as the guide's "checksum". You'll see them computed with Python, not quoted from memory: in this guide, every number is reproduced with arithmetic. If in module 5 your count comes out to 4000 reads/s and here we said 4000, you're on the right track.

Connection to the module: this lesson is the map, not the territory. Here you install the mindset —understand before you draw— and meet the case; the next seven lessons give you the tools to turn that mindset into a method. Lesson 2 separates the two kinds of requirement (functional vs. non-functional), the distinction everything else rests on. Lesson 3 teaches you what to ask to fill the holes in the prompt. Lesson 4, how to scope —choose what goes into the first design and what is deferred—. Lesson 5 gives you the 4-step framework (requirements → estimation → high-level design → deep dive), the backbone of the rest of the guide. Lesson 6 finally draws: Enlace in a single box. Lesson 7 installs the tradeoffs mindset and points out where that box breaks —which is exactly what modules 2 through 7 fix—. And lesson 8, the project, puts you to work producing the complete starter package with your own hands.

A note on scope, because it sets the tone: in this module we don't do deep back-of-the-envelope math, we don't pick the database, and we don't add a cache. All of that arrives —numeric estimation is module 2, the data model module 3, the cache module 4—. Here we stay at the layer of how you approach the problem: understanding it, scoping it, and sketching the first design. It's the step almost everyone skips, and that's why almost everyone gets stuck.

The architect who doesn't draw until they ask

Think of it this way. You hire an architect and tell them: "I want you to design me a house". A bad architect pulls out the pencil and starts drawing rooms. A good architect puts the pencil away and pulls out a notebook of questions: For how many people? What's your budget? Is the lot flat or sloped? Do you need a home office? Are you planning to have kids? Cold climate or hot? Do you care more about natural light or energy savings? None of those questions is about walls or windows yet. They're about understanding the problem. Because "a house" for a childless couple on a tight budget and "a house" for a family of six on a steep lot are two different buildings, and drawing before knowing which is which guarantees redoing the plan.

Notice something crucial about that notebook of questions: there's no correct house. There's a correct house for these requirements. High ceilings are wonderful —and expensive to heat—. Huge windows bring light —and lose privacy—. Every decision is a tradeoff: you gain something and you pay something. The architect's job isn't to find the perfect house (it doesn't exist); it's to understand what matters to the client so they can choose what to pay and what to gain. A software design is exactly the same. There's no "correct design for the URL shortener". There's the correct design for 100 million URLs a month with reads a hundred times heavier than writes, which is a different system from the one that's correct for a thousand URLs a month on a company intranet.

A system design problem is that client who tells you "I want a house" and expects you to ask the rest. The prompt —"design a URL shortener"— is intentionally poor. Whoever poses it didn't forget the details: they omitted them on purpose to see whether you demand them. The worst possible response is to start drawing boxes right away, because it reveals that you design without understanding. The best is to pull out the notebook: How many users? How many new URLs a month? Do people visit them a lot or a little? How long do they live? Do we need analytics? Each answer trims the space of possible designs until one defensible option remains.

It's worth spelling it out in full, because it's the idea that runs through all eight modules:

Designing a system starts by understanding it. The prompt is always vague on purpose; your first job is to turn it into concrete requirements —what it does and how well— before drawing a single box. And there's no correct design: there are correct tradeoffs for concrete requirements.

Worked example: two sentences, two operations, one redirect

Let's bring the idea down to something concrete with the poorest possible prompt and see how much we can extract just by understanding it, without designing yet. The prompt is: "Design Enlace, a URL shortener."

The first thing a designer does isn't draw; it's ask what this has to do, in its minimal form. A URL shortener does two things, and only two, at its core:

  1. Shorten: you give it a long URL and it returns a short code. In Enlace, shorten("https://a-very-long-blog.com/2026/07/article-about-system-design") returns something like "aX9kR2q", and the public URL is enla.ce/aX9kR2q.
  2. Resolve: you give it the short code and it returns the long URL, to redirect the browser. resolve("aX9kR2q") returns "https://a-very-long-blog.com/2026/07/article-about-system-design".

That's the whole core. Notice that already, without drawing a single box, we've done real design work: we've named the two operations and we've chosen their names in English —shorten and resolve— because that's how the tech market is and that's how you'll read them in any documentation. Written as function signatures, the system's contract is this:

# enlace/api.py — Enlace's minimal contract (signatures, no implementation)

def shorten(long_url: str) -> str:
    """Takes a long URL and returns a 7-character base62 short_code."""

def resolve(short_code: str) -> str:
    """Takes a short_code and returns the original long_url (or raises NotFound)."""

What happens when someone visits enla.ce/aX9kR2q? The browser requests that URL, Enlace resolves it, and responds with a redirect: it tells the browser "this is actually somewhere else, go there". Let's draw that flow —not the internal design yet, just what the user sees—:

sequenceDiagram
    participant U as Browser
    participant E as Enlace
    U->>E: GET enla.ce/aX9kR2q
    E->>E: resolve("aX9kR2q") -> long_url
    E-->>U: 301/302 Location: https://...long-article
    U->>U: the browser follows the redirect

What to expect. That diagram is the entire observable behavior of Enlace on the read side: a request arrives with a short code, Enlace resolves it, and responds with a redirect HTTP status code (301 or 302) and a Location header pointing at the long URL. The browser, upon receiving a 3xx with Location, makes a second request to the long URL on its own. The difference between 301 (a permanent redirect) and 302 (a temporary redirect) is not cosmetic and you'll decide it deliberately later: a 301 is cached by the browser, so next time it won't even ask again —blazing fast, but you lose the click count because the browser stops going through Enlace—; a 302 is not cached, so every visit goes through Enlace —a little slower, but you can count every click—. That's your first tradeoff in the guide, and notice that it already appeared without having drawn the internal architecture: it's born from understanding the problem, not from choosing technology.

Pause on what we just did, because it's the method in miniature. We started from an empty sentence —"a URL shortener"— and, just by asking ourselves what it has to do, we pulled out: the two operations, their names, their signatures, the redirect flow, and even a tradeoff (301 vs. 302). None of this came from drawing boxes. It came from understanding. The boxes come later, in lesson 6, and by then you'll know exactly what they have to do.

The guide's case study: Enlace

The whole guide works on the same system, for the same reason a chess apprentice always studies the same openings: if every module debuted a new problem, you'd spend half your energy understanding the context instead of practicing the technique. With a single case, by module 3 you already know Enlace by heart and can concentrate on what each lesson adds —the data model, the cache, sharding— without asking yourself again "wait, what was the short_code?".

What Enlace is and why it's the perfect case

Enlace is a URL shortener: it takes a long web address and returns a short, memorable one (enla.ce/aX9kR2q); when someone visits the short one, it redirects to the long one. It's what TinyURL or a social network's t.co does. You state it in one sentence, and that's why it's deceptive: it looks like a toy exercise. It isn't. It's the canonical system design problem precisely because its core is trivial —store a (code, URL) pair and look it up— but every non-functional requirement you add activates a different fundamental:

  • 100 million URLs a month? → You need to estimate capacity (module 2) and generate identifiers that don't collide (module 3).
  • People read a hundred times more than they write? → You need a cache for the read path (module 4).
  • 6 TB of data? → A single database isn't enough: replicas and sharding (module 5).
  • Thousands of reads per second? → A single server isn't enough: load balancing and stateless services (module 6).
  • It can't go down? → Redundancy, failover, and the consistency tradeoffs (module 7).

That's the guide's journey: we start with Enlace in a single box —the simplest thing that meets the core— and scale it module by module, always driven by a number that forces the next step. We never add complexity "just because"; we add it when the arithmetic proves the previous box no longer suffices. That discipline —complicate only when the numbers demand it— is itself one of the most important lessons of the craft.

An honesty upfront, in the spirit of the guide: Enlace is a lab case. A real industrial shortener would have authentication, per-user rate limits, spam and phishing detection, an analytics dashboard, billing. Enlace keeps the heart —shorten, resolve, redirect at scale— and defers the rest on purpose, because the heart is what teaches the fundamentals. How to decide what stays in the heart and what gets deferred is, precisely, the topic of lesson 4.

The central record: Link

At the center of Enlace there's a single thing to store: the association between a short code and a long URL, plus a bit of metadata. We call it Link. The identifiers are in English —Link, short_code, long_url— because that's how the real tech market is: the documentation, the libraries, and the team you'll work with speak that language. The prose you read is in Spanish; the names inside the code, in English.

# enlace/models.py
from dataclasses import dataclass
from datetime import datetime


@dataclass
class Link:
    """The record of a shortened URL in Enlace."""
    short_code: str            # the short code, base62, 7 chars: "aX9kR2q"
    long_url: str              # the original URL, ~500 bytes on average
    created_at: datetime       # when the link was created
    expires_at: datetime | None  # when it expires (None = never)
    clicks: int                # visit counter (analytics, optional)

Pause on each field, because each one hides a design decision we'll develop later:

  • short_code — the heart of the problem. It's a short, unique string that identifies the link. In Enlace we use base62 (the 0-9a-zA-Z alphabet) with 7 characters. Why base62 and not, say, the digits 0 through 9, and why exactly 7 characters, is the topic of module 3. For now the intuition is enough: 7 base62 characters give a gigantic space of possible codes —we'll compute it in a moment— and they're short to type.
  • long_url — the original URL. On average it weighs about 500 bytes; some are short, others enormously long with tracking parameters. This number matters when we estimate storage in module 2.
  • created_at and expires_at — lifecycle metadata. expires_at can be None (the link never expires) or a date (it expires). Expiration is an optional feature that we'll decide whether to include when we scope in lesson 4.
  • clicks — the visit counter, the seed of analytics. It's also optional, and watch out for this one: incrementing a counter on every read has an enormous write cost when there are 4000 reads per second. That detail —analytics = a hundred times more writes— is a star example of how a scoping decision changes the entire system, and you'll see it with numbers in lesson 4.

Notice something: this Link record is tiny —five fields—, and yet all of Enlace revolves around it. That's another truth of the craft: large systems are almost never complicated in their data model; they're complicated in how they store, look up, and scale a simple data model. Enlace demonstrates it by the book.

The anchor numbers (computed, not quoted)

Here is the guide's numeric heart. These exact results are the "checksum": if your calculations reproduce them, you're doing fine; if not, there's an error and you'll notice it. And a hard rule of this guide: I don't quote them from memory, I compute them. I actually ran them with Python 3.14.0 before writing them. Module 2 develops this back-of-the-envelope math in depth; here we just present it so the number keeps you company from the start.

The starting assumptions —the data that in a real problem you'd pull out of the client with questions— are these: 100 million new URLs a month, people read (visit) 100 times more than they write (create), each record weighs ~1 KB, and we keep 5 years. Everything else follows from that:

# enlace/capacity.py — Enlace's back-of-the-envelope math (M2 develops it)

writes_per_month = 100_000_000              # assumption: 100M new URLs/month
seconds_per_month = 30 * 24 * 3600          # 30 days * 24 h * 3600 s
qps_write = writes_per_month / seconds_per_month
qps_read = qps_write * 100                  # assumption: read:write ratio = 100:1

records_5y = writes_per_month * 12 * 5      # 5 years of accumulation
bytes_per_record = 1024                     # ~1 KB per record
storage_bytes = records_5y * bytes_per_record

code_space = 62 ** 7                        # base62, 7 characters

print(f"seconds per month  = {seconds_per_month:,}")
print(f"writes/second      = {qps_write:.1f}  (~40)")
print(f"reads/second       = {qps_read:.0f}  (~4000)")
print(f"records at 5 years = {records_5y:,}")
print(f"storage            = {storage_bytes / 1e12:.2f} TB  (~6 TB)")
print(f"code space         = {code_space:,}  (62^7 ~ 3.52e12)")

What to expect. With Python 3.14.0, running python enlace/capacity.py, you get exactly this:

seconds per month  = 2,592,000
writes/second      = 38.6  (~40)
reads/second       = 3858  (~4000)
records at 5 years = 6,000,000,000
storage            = 6.14 TB  (~6 TB)
code space         = 3,521,614,606,208  (62^7 ~ 3.52e12)

Read it slowly, because these six numbers are the entire guide compressed:

  • ~40 writes/second. 100 million a month, spread across the ~2.6 million seconds a month has, give 38.6 writes per second. We round to ~40. It's tiny —a single database handles it without breaking a sweat—. This is the first lesson the number gives: the write side of Enlace is calm.
  • ~4000 reads/second. Here's the drama. Since people read 100 times more than they write, reads are 40 × 100 = 3858, ~4000/s. A hundred times more than the writes. This tells you, from minute one, that Enlace is a read-heavy system, and that almost all the effort of scaling it will go to the read path (which is why the cache is a whole module).
  • 6 billion records ≈ 6 TB. Five years accumulating 100M/month are 6 billion records; at ~1 KB each, 6.14 TB. That's too much to keep comfortably on a single machine forever —hence replicas and sharding in module 5—.
  • 62⁷ ≈ 3.52 × 10¹² possible codes. The 7-character base62 code space is 3.5 trillion (short-scale trillion: millions of millions). Compare it to the 6 billion records at 5 years: we'd use less than 0.2% of the space. Plenty to spare. That calculation —do 7 characters suffice?— is the heart of module 3.

Keep these numbers. They'll reappear, computed, in every module. When in module 4 we measure how much memory the cache needs, we'll start from the ~4000 reads/s. When in module 5 we decide whether to shard, we'll start from the 6 TB. They're the thread that sews the guide together.

The guide's map

Eight modules, and each one scales Enlace one step further, always pushed by a number. They're not in any order: they go from understanding the problem to estimating it to modeling its data to making it fast to making it big to distributing it to making it reliable, and they end by designing all of Enlace from start to finish.

ModuleWhat it installsWhat you walk away with
1. How to approach a system design problemUnderstand before you draw: functional/non-functional requirements, clarifying questions, scoping, the 4-step framework, and Enlace in a single boxTake a vague prompt and produce requirements + scope + a first design
2. Back-of-the-envelope estimationThe capacity math in depth: QPS, storage over 5 years, bandwidth, working-set memory. Powers of ten, seconds/day, unitsEstimate the scale of any system with back-of-the-envelope arithmetic
3. Data model and ID generationThe Link record; SQL vs. NoSQL for this case; generating the short_code (hash vs. counter+base62 vs. random); why 7 characters suffice (62⁷)Choose the data model and generate unique, short identifiers
4. Caching the read-heavy pathWhy 100:1 calls for a cache; cache-aside; eviction (LRU); hit ratio and average latency (L = h·L_cache + (1−h)·L_db); the 80/20 ruleAdd a cache with judgment and calculate how much it speeds up the system
5. Scaling the databaseWhen one DB isn't enough: replication (primary/replica, lag), sharding (why and by which key), consistent hashing (remapping ~K/N)Scale storage beyond a single machine
6. Load balancing and statelessnessBalancing (round-robin, least-connections, by hash); stateless services to scale horizontally; where the state lives; health checksScale compute horizontally behind a load balancer
7. Reliability and the consistency tradeoffRedundancy, single points of failure, failover; CAP and PACELC; strong vs. eventual consistency and which one Enlace gets; SLA/SLO as a numberReason about reliability and choose the consistency model with judgment
8. Project: design Enlace end to endThe complete process: requirements, estimation, high-level design, read and write paths, cache, replicas/sharding, load balancing, tradeoffsDesign and defend a complete distributed system from scratch

Notice the shape of the arc. Module 1 gives you the mindset and the method. Module 2, the numbers. Module 3, the data model. Module 4 makes it fast (cache). Module 5 makes it big (scaling the DB). Module 6 makes it horizontal (balancing). Module 7 makes it reliable (redundancy and consistency). And module 8 puts it all together by designing all of Enlace. Each module solves a concrete crack in the design of a single box that you'll meet in lesson 7 of this module.

The boundary: what's taught here and what's next door

This guide lives in an ecosystem of sibling guides about architecture and system design, and it has a clear rule about what belongs to it. It's the guide of the design and scaling fundamentals: how to approach the problem, estimate, model data, cache, scale, balance, and reason about consistency. It's worth knowing from the start where to look for each thing, so you don't expect from here something that belongs to another guide:

TopicWhat's covered hereWhere the full development lives
Styles and boundaries (monolith vs. microservices, DDD, bounded contexts)Nothing: the single box is a monolith by default, without entering the debate on stylesarchitectural-styles-and-boundaries-guide
Event-driven architecture (queues, event sourcing, CQRS, streaming)At most a pointer: click analytics could go through a queue, but we don't design itevent-driven-architecture-guide
Resilience patterns (circuit breaker, bulkhead, retry/backoff, idempotency in depth)Only the basic failover and the concept of a single point of failure (module 7)resilience-and-reliability-patterns-guide
Organization-level decisions (ADRs, build-vs-buy)We name tradeoffs, but not the formal process of deciding and documenting themarchitecture-decisions-and-tradeoffs-guide
API design in depth (versioning, pagination, REST/gRPC contracts)Only Enlace's minimal contract (shorten, resolve)api-design-and-integration-guide

The mechanical rule to remember it: if the question is "how do I design and scale this system with numbers and tradeoffs?", it's this guide. If it's "monolith or microservices?", it's the styles guide. If it's "how do I keep a cascading failure from bringing everything down?", it's the resilience guide. Keeping that boundary clear is what lets you learn the fundamentals without drowning in topics you don't need yet —and without reinventing what another guide already explains better—.

Common mistakes

Starting to draw boxes before understanding the problem. What happens: you get "design a URL shortener" and thirty seconds later you already have a server, a database, and three arrows on the whiteboard. Five minutes later you get stuck, because you don't know whether you designed for a thousand users or a hundred million, and those are different architectures. Why it happens: drawing feels productive; asking feels slow. It's an illusion: asking for five minutes saves you half an hour of redesigning. How to detect it: if you drew a box and couldn't say what number justifies it, you drew too soon. How to fix it: keep the pencil away until you have requirements. This entire module is that habit.

Believing there's a "correct" design. What happens: someone looks for the answer to the URL shortener, memorizes it, and regurgitates it identically regardless of the prompt. When the problem changes —"now it's a company intranet, a thousand URLs a month"—, they apply the massive-scale design and over-build by a factor of a thousand. Why it happens: it's easier to memorize an answer than to learn a method. How to detect it: if your design wouldn't change when you change the numbers in the prompt, you're not designing, you're reciting. How to fix it: think in tradeoffs for concrete requirements, not in answers. The cache is wonderful at 4000 reads/s and a waste at 4. The number decides.

Confusing "shorten URLs" with "the problem is easy". What happens: the prompt fits in one sentence, so someone concludes the design does too, keeps a dictionary in memory, and calls it done. Then the 6 TB show up, the 4000 reads/s, and the need for it not to go down, and the in-memory dictionary doesn't survive the first non-functional requirement. Why it happens: the simplicity of the prompt is confused with the simplicity of the system at scale. How to detect it: if your design doesn't mention a single capacity number, you're still solving the toy, not the system. How to fix it: the core is simple (and in lesson 6 we draw it that way, on purpose); the difficulty lives in the non-functional requirements, and those are the rest of the guide.

Exercises

Exercise 1 — Pull the operations out of a vague prompt. Without designing anything, take this prompt: "Design a pastebin service: people paste some text and get a short link to share it; whoever opens the link sees the text." Write, as we did with Enlace, the two core operations with English names and their function signatures (input and output). Don't add features yet: just the heart.

See solution

The pastebin, at its core, does the same as Enlace but storing text instead of a URL:

def create_paste(content: str) -> str:
    """Takes some text and returns a short paste_id to share."""

def get_paste(paste_id: str) -> str:
    """Takes a paste_id and returns the original text (or raises NotFound)."""

Two operations, just like Enlace: one that writes (create_paste) and one that reads (get_paste). Notice the parallel with shorten/resolve: the pattern "store something under a short identifier and retrieve it" is the same, and that's why the pastebin and the shortener are almost the same design problem (the big difference is in the size of what you store: 500 bytes of URL vs. potentially megabytes of text, which changes the storage and bandwidth estimation). Naming it this way, with two clean operations, is already design: you've just defined the system's contract without drawing a single box.

Exercise 2 — Functional or non-functional? For each of these statements about Enlace, decide whether it describes what the system does (functional) or how well it does it (non-functional), and explain it in one sentence. (a) "When you visit a short code, it redirects to the long URL." (b) "It handles 4000 reads per second." (c) "If a link doesn't exist, it returns a 404." (d) "The redirect responds in under 100 milliseconds." (e) "It's available 99.9% of the time."

See solution
  • (a) Functional. It describes a capability: the redirect is something the system does. It's the resolve operation.
  • (b) Non-functional. It's not a new capability, it's how much load the capability it already has can handle. It's a scale/throughput requirement.
  • (c) Functional. It's behavior: what it does when given a nonexistent code. It's part of the resolve contract.
  • (d) Non-functional. It's not what it does, it's how fast. It's a latency requirement.
  • (e) Non-functional. It's how often it's on. It's an availability requirement.

The lesson: functional ones usually start with a verb ("redirects", "returns", "shortens"); non-functional ones usually carry a number or a quality adjective ("fast", "available", "handles X"). This distinction is so central that lesson 2 is devoted entirely to it.

Exercise 3 — Reproduce an anchor number by hand. Without running the script, compute with pencil and paper the writes per second of Enlace from the assumptions (100 million URLs a month, a month ≈ 30 days). Show the calculation. Then, using the 100:1 ratio, compute the reads per second. Round to a friendly power of ten at the end.

See solution

Writes per second: you have to spread 100 million across the seconds in a month.

  • Seconds in a month = 30 days × 24 hours × 3600 seconds = 2,592,000 s (about 2.6 million).
  • Writes/s = 100,000,000 ÷ 2,592,000 ≈ 38.6, which we round to ~40 writes/s.

Reads per second, with the 100:1 ratio:

  • Reads/s = 40 × 100 = ~4000 reads/s.

It matches what Python computed (38.6 and 3858). Notice the back-of-the-envelope trick: you don't need the exact calculator; with "100M over ~2.6M ≈ 40" you already have the order of magnitude, which is what matters for deciding. Module 2 turns this mental rounding into a method. And keep the qualitative conclusion, which is worth more than the digits: writes are few (~40/s) and reads are many (~4000/s) —Enlace is read-heavy—.

Summary and next step

In this lesson you installed the mindset that holds up all eight modules: understand before you draw. A design problem always arrives vague on purpose, and your first job isn't to put boxes on a whiteboard, it's to turn that vagueness into concrete requirements —what the system does and how well—. And you saw it framed by the truth that makes the craft professional: there's no correct design, there are correct tradeoffs for concrete requirements, like the architect who doesn't draw until they ask.

You met Enlace, the URL shortener we work on throughout the guide: its two operations (shorten, resolve), its redirect flow (and the first tradeoff, 301 vs. 302), its Link record with five fields, and —computed with Python, not quoted— its anchor numbers: ~40 writes/s, ~4000 reads/s (Enlace is read-heavy), 6 billion records ≈ 6 TB at five years, and 62⁷ ≈ 3.52 × 10¹² possible codes. Those numbers are the guide's checksum.

Before moving on you should be able to: explain why you understand before you draw; name Enlace's two operations and its redirect flow; recite the five fields of Link and why clicks is dangerous at scale; and reproduce at least one anchor number with back-of-the-envelope arithmetic (~40 writes/s → ~4000 reads/s).

What comes next is sharpening the distinction the whole method rests on. In lesson 2 we separate with precision the functional requirements (what it does) from the non-functional ones (how well, with numbers), and you'll see that confusing them is the number-one cause of designs that collapse. It's the foundation; without it, the clarifying questions of lesson 3 would have nowhere to stand.

Resources

  • System Design Primer — GitHub repository — the most widely used open compendium for learning system design. Start with its "How to approach a system design interview question" section: it's exactly the mindset of this lesson —understand, scope, estimate, design— written as a checklist.
  • Designing Data-Intensive Applications, by Martin Kleppmann — official site — the reference book of the craft (we'll cite it as "DDIA" throughout the guide). The Preface and Chapter 1 ("Reliable, Scalable, and Maintainable Applications") define the three non-functional pillars —reliability, scalability, maintainability— that are the vocabulary of lesson 2.
  • MDN — 301 Moved Permanently and 302 Found — the two redirects Enlace chooses between. Read them together: the cacheability difference is what creates the "speed vs. counting clicks" tradeoff we mentioned, and which we'll decide with judgment later.
  • Python — dataclasses — the @dataclass decorator the Link record is written with. You don't need to master it to design Enlace, but knowing that it generates the __init__ for you explains why we can write Link(short_code="aX9kR2q", ...) without defining a constructor.