Chapter

Draw the Smallest Complete System

Connect clients, application logic, and durable storage first, then add components only for measured pressure or failure requirements.

80 min 4 objectives
Learning objectives
  • Draw a complete read and write path before optimizing either
  • Explain the responsibilities of DNS, load balancing, services, caches, and databases
  • Scale the design by responding to a specific bottleneck or failure
  • Review an architecture against requirements, estimates, and failure behavior

Start with a system that can actually complete the work

We now have requirements, workload estimates, API contracts, and a data model. The first architecture should be the smallest arrangement that performs both core workflows correctly:

Code
Client → URL service → links database

The service validates creation requests, generates codes, reads mappings, and produces redirect responses. The database makes accepted mappings durable.

This design is intentionally plain. It is valuable because every component has a necessary responsibility. It also gives us a baseline: when a requirement or estimate exceeds it, we can identify exactly what to change.

Trace the write path

For POST /links:

Code
1. Client sends destination, expiration, and an idempotency key.
2. Service validates the destination and applies rate limits.
3. Service generates a candidate short code.
4. Database inserts the mapping with a uniqueness constraint on code.
5. If the code collides, the service generates another and retries.
6. After durable insertion succeeds, the service returns the short URL.

The acknowledgment boundary matters. If the service returns success before durable storage, an application crash can erase a link the user was told exists.

The database uniqueness constraint is the final authority on collisions. Checking whether a code exists and then inserting it as two unrelated operations creates a race:

Code
service A checks code X → absent
service B checks code X → absent
service A inserts X
service B inserts X

An atomic unique insert allows only one to succeed.

Trace the read path

For GET /{code}:

Code
1. Service validates the shape of the code.
2. Service reads the mapping by primary key.
3. Service verifies that it has not expired or been disabled.
4. Service returns a redirect with the destination in the Location header.

Unknown, expired, and disabled codes follow explicit error behavior. Even this tiny design is complete because it explains both success and failure paths.

Add multiple service instances for capacity and availability

The peak estimate is about 12,000 redirects per second. One service process may not safely handle that load, and one process is also a single failure point.

Run several instances and place a load balancer in front:

Code
                         ┌→ URL service A ┐
Client → load balancer ──┼→ URL service B ├→ links database
                         └→ URL service C ┘

A load balancer accepts traffic at one address and distributes requests among healthy service instances. Health checks help it stop sending work to an instance that cannot serve requests.

The service instances should be stateless with respect to link mappings: any healthy instance can handle the next request because durable shared state lives outside its process. An instance can restart without losing accepted links.

This does not mean an application holds no temporary memory. It means correctness does not depend on a client returning to the same instance.

Where DNS fits

Users type a domain such as sho.rt, while networks route to numeric addresses. The Domain Name System, or DNS, resolves the domain to the public endpoint serving the application, often a load balancer or edge service.

Code
sho.rt → DNS lookup → public endpoint → load balancer → service

DNS responses are cached, so changing them is not an instantaneous per-request routing mechanism. It provides naming and coarse routing; the load balancer handles live request distribution among service instances.

Add a cache for the read-heavy path

Redirects outnumber creations about 100 to 1, and popular links may be requested repeatedly. A cache can keep frequently used mappings in faster storage:

Code
Client → load balancer → URL service → cache
                                     ├─ hit  → return mapping
                                     └─ miss → database → fill cache

A cache hit finds the mapping in the cache. A cache miss falls back to the database. The service, not the cache, remains responsible for applying expiration and disabled-link rules correctly.

Cache-aside flow

For a redirect:

  1. Read the code from cache.
  2. On a hit, validate the cached record and redirect.
  3. On a miss, read the database.
  4. If a valid mapping exists, place it in cache with an expiration time.
  5. Redirect.

Only successful, active mappings may be cached initially. Caching “not found” results can protect the database from repeated invalid codes, but a short negative-cache lifetime is needed so a newly created code does not remain falsely absent.

Our MVP mappings are immutable, which simplifies caching. Expiration can be enforced with a cache TTL no later than the link’s own expiration. If editing is added later, writes must invalidate or update cached copies.

Protect the database from becoming the next single point

Multiple service instances do not help if every request depends on one database process that can fail. Storage replication keeps additional copies of data.

A common arrangement has one primary accepting writes and replicas serving as recovery targets or, when consistency requirements permit, reads:

Code
service → database primary → replica
                           → replica

Replication introduces questions:

  • Is replication synchronous or asynchronous?
  • Can a redirect read from a replica before a new mapping arrives there?
  • How is a new primary selected after failure?
  • What happens to writes during that transition?
  • How are backups tested independently of replicas?

Replicas improve availability and durability only when failure detection and recovery are designed and exercised. “Three replicas” is not itself a recovery plan.

Keep analytics off the redirect’s critical path

Analytics was outside the MVP, but it illustrates asynchronous work. If every redirect waits for an analytics database write, an analytics slowdown increases redirect latency and may make redirects unavailable.

A future design could publish a small click event to a durable queue:

Code
redirect service → return redirect
        └────────→ event queue → analytics workers → analytics store

A queue holds work until consumers can process it. This separates redirect latency from analytics processing speed, but it introduces delivery, duplication, retention, and backlog-monitoring questions. Add it when the feature exists, not merely because queues are common in diagrams.

Scale by naming the pressure

Pressure or requirementTargeted response
One service cannot handle peak requestsAdd stateless instances behind a load balancer
One service failure must not stop redirectsHealth-check multiple instances
Repeated reads overload storageCache popular immutable mappings
Storage process failure loses availabilityReplicate data and define failover
Data no longer fits or one write leader saturatesPartition by a stable key after measuring the bottleneck
Analytics slows redirectsMove analytics to an asynchronous event path

This is the central discipline of system design: every added component should answer a named pressure, failure, or product requirement.

Guided design: a URL shortener

Close the chapter and reproduce the design without copying its boxes. Use this order:

1. Restate the problem

Write the two core workflows, exclusions, scale assumptions, and top three quality priorities. If you change an assumption, note how it may alter the design.

2. Recalculate the scale

Estimate average and peak read/write QPS, five-year storage, and the size of a seven-character base-62 namespace. Keep units visible.

3. Define the contract and record

Sketch create and redirect APIs, important errors, an idempotency strategy, and the minimal link record. Identify the unique key and dominant lookup.

4. Draw the baseline

Show the complete creation and redirect paths through one service and durable storage. Mark where success is acknowledged.

5. Add only justified components

Use the estimated peak and failure requirements to decide whether you need multiple service instances, load balancing, cache, replication, or partitioning. Write one sentence beside each added box explaining why it exists.

6. Walk through failures

Explain behavior when the cache is unavailable, the database is slow, a generated code collides, a service instance fails, and a link expires while cached.

Self-review rubric

Score each dimension from 0 to 2:

Dimension012
Requirementsvague or missingbehaviors listedscope, scale, and priorities explicit
Estimatesabsentnumbers without consequencesunits, peaks, and design consequences shown
Data and APIsgenerichappy path onlyaccess patterns, errors, uniqueness, and retries covered
Architecturetechnology listpaths mostly connectedevery component has a requirement-backed responsibility
Tradeoffsone “best” answeralternatives namedrejected alternatives explained against priorities
Failuresignoredfailures listedbehavior and recovery described

A score of 9 or more indicates a coherent first pass. A low score is not failure; it tells you which part of the reasoning needs another iteration.

Continue with authoritative material

Use production architecture frameworks as review lenses rather than vendor-service shopping lists:

These sources go beyond the current MVP. Later parts of this curriculum should introduce their ideas only when a concrete system creates the need.

Part checkpoint

You can now move from an ambiguous product request to a defensible first architecture:

Code
requirements → estimates → API and data → baseline paths → targeted scaling → failure review

The next part should deepen individual building blocks—caching, storage, replication, partitioning, asynchronous work, and reliability—without losing this end-to-end sequence.