Chapter
Draw the Smallest Complete System
Connect clients, application logic, and durable storage first, then add components only for measured pressure or failure requirements.
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:
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:
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:
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}:
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:
┌→ 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.
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:
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:
- Read the code from cache.
- On a hit, validate the cached record and redirect.
- On a miss, read the database.
- If a valid mapping exists, place it in cache with an expiration time.
- 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:
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:
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 requirement | Targeted response |
|---|---|
| One service cannot handle peak requests | Add stateless instances behind a load balancer |
| One service failure must not stop redirects | Health-check multiple instances |
| Repeated reads overload storage | Cache popular immutable mappings |
| Storage process failure loses availability | Replicate data and define failover |
| Data no longer fits or one write leader saturates | Partition by a stable key after measuring the bottleneck |
| Analytics slows redirects | Move 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:
| Dimension | 0 | 1 | 2 |
|---|---|---|---|
| Requirements | vague or missing | behaviors listed | scope, scale, and priorities explicit |
| Estimates | absent | numbers without consequences | units, peaks, and design consequences shown |
| Data and APIs | generic | happy path only | access patterns, errors, uniqueness, and retries covered |
| Architecture | technology list | paths mostly connected | every component has a requirement-backed responsibility |
| Tradeoffs | one “best” answer | alternatives named | rejected alternatives explained against priorities |
| Failures | ignored | failures listed | behavior 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:
- Google Cloud Well-Architected Framework organizes guidance around operational excellence, security, reliability, cost, performance, and sustainability.
- AWS Well-Architected Framework provides questions for evaluating architecture tradeoffs across similar qualities.
- Google’s Site Reliability Engineering books connect design to reliability objectives, monitoring, incident response, and secure operation.
- MIT 6.5840 Distributed Systems is optional deeper study for replication, consistency, and fault tolerance after the foundation is comfortable.
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:
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.