System Design · Module 7 · Lesson 4 of 4 · 50 min
Capstone Design a Short Link Service End to End
Assemble requirements APIs schema capacity routing and caching into one defended design, then review retries freshness and failure recovery.
01 The idea
The problem this lesson solves
You now have the ingredients for a short-link service, but a complete design is more than assembling the ingredients into a diagram. Its API, schema, capacity budget, cache policy and failure behavior must agree. This capstone provides a worked design with explicit assumptions, then asks you to change it and defend the consequences.
Use the API, database, capacity and cache concepts from earlier modules. These lessons add distributed ownership and failure trade-offs.
02 Worked example
Defend a complete short-link service
03 The method
Understand the mechanism
Fix the product contract and workload
The service creates links for authenticated owners, redirects public readers, lists an owner’s links and permits owner-authorized destination changes. Analytics, custom domains and billing are outside this first design. Codes are unique, and an acknowledged creation survives an app restart.
Use 100,000 DAU, ten redirects per active user/day, 10,000 creations/day and a 5× peak factor. That gives about 57.87 peak redirects/s and 0.579 peak creations/s. At 500 modeled bytes per mapping and one year of retention, logical mapping data is 1.825 GB before physical overhead and recovery copies.
These assumptions justify starting with a relational source of truth and measured app capacity. They do not justify sharding simply because the course has taught sharding. Availability and durability still need a deployment and recovery contract independent of the modest QPS.
For mutable destinations, define public redirect freshness and a strict owner verification path separately. Do not claim immediate global freshness while quietly serving arbitrary cached copies. The initial strict path reads the authority; any public caching must fit a declared policy and tested race behavior.
Define identities and operations together
POST /v1/links accepts a destination and an idempotency key. The authenticated caller supplies the owner identity through trusted context, not a body field granting authority. A successful response identifies the saved code and resource location.
For this capstone, the replayed creation result identifies the immutable code and resource location; it does not promise to replay the destination’s current or original mutable fields. That makes the stored result_code sufficient for the demonstrated result. An API promising an exact original response body would need to retain that body or equivalent immutable outcome data too.
GET /r/{code} performs a public lookup and returns a temporary redirect with an explicit HTTP caching policy when the record is active. An authoritative missing result and an unavailable lookup have different responses. Management reads and edits check ownership.
The links table stores code, owner_id, destination, version and creation time. Code is the primary key. An owner-and-time index supports stable listings. A create_operations table uniquely identifies caller plus operation key and stores the request fingerprint and result code.
The illustrative SQL below demonstrates one successful transaction with fixed ids. A real handler must validate inputs, allocate collision-resistant or otherwise controlled codes, bind parameters, handle concurrent key conflicts and return the stored outcome. The schema is an executable foundation, not a complete web server.
Make creation retry-safe
On the first request with k1, begin a transaction. Claim the scoped operation identity under a unique constraint, create the mapping and store the replayable outcome in the same transaction. Commit before reporting success.
If a competing attempt uses k1 with the same fingerprint, recover the recorded outcome or follow the documented in-progress policy. If it uses a different fingerprint, reject the conflict. A crash after commit but before response must leave enough information to return the original code a7.
Code allocation has a separate uniqueness problem. Random generation can collide, so the database constraint remains necessary and the handler needs bounded collision retries. Sequential allocation has different enumeration and distribution properties. Choose the scheme according to the product rather than assuming a large alphabet removes all concerns.
Keep operation-key retention explicit. If a client can retry after the retained record expires, define how it discovers the original result or whether a new effect is possible. This is part of the API contract, not an invisible housekeeping detail.
Trace redirect and destination update
A redirect request reaches an eligible app instance, validates the code form and consults an allowed read path. An eligible cache hit can supply the destination. A miss reads the authoritative mapping, distinguishes absent from unavailable and fills only under the chosen freshness policy.
An owner update checks permission and an expected version, changes the destination and advances the version atomically. A stale version should produce a conflict/precondition outcome rather than overwrite a newer edit.
After updating, the owner’s verification reads a sufficiently fresh authority path. Public cached reads require the separate invalidation or version-coordination design. Reproduce the late-reader fill race before claiming a freshness bound; a new TTL on old data is not enough.
A redirect returns a Location for the browser to follow. It does not require the service to fetch arbitrary destinations. Adding a preview crawler would introduce server-side fetching, additional security controls and a different capacity model, so it remains outside this initial design.
Size normal operation and recovery
At 90% eligible cache hits, the modeled peak origin redirect rate is about 5.79/s. At zero hits it is 57.87/s. Keep creation writes, management reads and background maintenance as additional rows rather than assuming the read-cache multiplier applies to everything.
Benchmark per-instance serving capacity at the latency and error target, then provision enough survivors for the chosen failure. Coordinate database connection pools across the app fleet. Extra app instances must not create unbounded origin concurrency during a cache outage.
Budget table data, indexes, live copies, logs, backups and maintenance space separately. Define recovery point and recovery time objectives, and perform a restore drill. A read replica does not protect against every historical data mistake.
Monitor gateway latency/errors, app saturation, pool waits, database work, cache misses and logical creation outcomes. A single “service up” metric cannot explain whether a slow redirect is waiting at the gateway, pool or database.
Review five incidents end to end
Lost creation response: the client retries k1 and receives a7 from durable operation state. Cache late fill: the old reader must obey the declared freshness mechanism, or strict reads bypass the copy. Cold cache: admission and origin budgets prevent unbounded rebuild work.
App failure: routing removes the instance after detection, survivors provide the planned capacity, and eligible retries retain operation identity. Replica lag: fresh reads use the authority or a verified progress policy rather than returning a false missing result.
For every incident, name the caller-visible outcome, the component that owns recovery and the evidence that recovery completed. “Retry,” “fail over” and “invalidate” are starting verbs, not complete incident protocols.
Also test combined conditions required by the product. An app failure during cache recovery may consume more reserve than either isolated event. State the failure scope you support and the degraded behavior outside it.
Grow only when a measured limit changes the decision
If read demand exceeds the tested database budget, investigate query efficiency, caching and suitable replica reads. If write or storage ownership needs partitioning, choose a shard key from the dominant access path and design the owner-listing secondary path.
If a single key becomes viral, adding shards may leave that key’s owner overloaded. If a region is lost, DNS and routing changes must account for caches, connections and surviving data authority. Growth changes specific constraints; it does not make every distributed mechanism necessary at once.
Keep a decision log with choice, benefit, cost and revisit trigger. Example: one relational authority simplifies uniqueness and transactions; revisit when measured write/storage capacity or required failure isolation exceeds the tested design.
The finished packet should be executable by another engineer: requirements, workload worksheet, API outcomes, schema, request diagrams, transaction boundaries, cache contract, recovery tests and growth triggers. That is the standard for a self-contained design, beyond naming the right components.
Guided tutorial
Create the executable schema
Run the SQL below in a disposable PostgreSQL database. It creates links, the owner-listing index and operation records, then commits a7 with k1 in one transaction. Verify that the final SELECT returns k1 and a7 with the stored destination.
Review replay and conflict behavior
Attempt to insert another operation for the same caller and k1 in a separate transaction. The unique key must reject it. Explain how the real handler would roll back or use a conflict-aware claim, then read the stored fingerprint and result rather than blindly create another link.
Perform a conditional destination edit
Update a7 only where owner_id=7 and version=1, set a new destination and advance version to 2. Repeat an update expecting version 1 and observe zero changed rows. Explain the response to the stale client and the cache-freshness implications of the successful update.
Submit and challenge the design packet
Include the concrete API outcomes, 57.87 peak read QPS, 1.825 GB logical annual data, schema and both creation/redirect paths. Walk all five incidents from the explanation. Then increase traffic by 100× and identify the first measurement needed before choosing the next architectural change.
PostgreSQL capstone setup
-- Run once in a disposable PostgreSQL database.
CREATE TABLE sd_capstone_links (
code text PRIMARY KEY,
owner_id bigint NOT NULL,
destination text NOT NULL,
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX sd_capstone_owner_recent
ON sd_capstone_links (owner_id, created_at DESC, code DESC);
CREATE TABLE sd_capstone_create_operations (
caller_id bigint NOT NULL,
operation_key text NOT NULL,
request_fingerprint text NOT NULL,
result_code text REFERENCES sd_capstone_links(code),
state text NOT NULL DEFAULT 'pending' CHECK (state IN ('pending','completed')),
PRIMARY KEY (caller_id, operation_key),
CHECK ((state = 'pending' AND result_code IS NULL)
OR (state = 'completed' AND result_code IS NOT NULL))
);
-- One successful attempt. A real handler binds values and handles conflicts.
BEGIN;
INSERT INTO sd_capstone_create_operations
(caller_id, operation_key, request_fingerprint)
VALUES (7, 'k1', 'create:https://example.com/guide');
INSERT INTO sd_capstone_links (code, owner_id, destination)
VALUES ('a7', 7, 'https://example.com/guide');
UPDATE sd_capstone_create_operations
SET result_code = 'a7', state = 'completed'
WHERE caller_id = 7 AND operation_key = 'k1';
COMMIT;
SELECT o.operation_key, l.code, l.destination
FROM sd_capstone_create_operations o
JOIN sd_capstone_links l ON l.code = o.result_code
WHERE o.caller_id = 7 AND o.operation_key = 'k1';
-- Owner edit: only the expected version can be changed.
UPDATE sd_capstone_links
SET destination = 'https://example.com/updated', version = version + 1
WHERE code = 'a7' AND owner_id = 7 AND version = 1
RETURNING code, version;
Check your result
Your submission contains an API contract, schema, capacity estimate, labeled request paths and a failure review. Walk one creation retry and one stale-cache race from the caller’s perspective.
04 Cheat sheet
Review the decisions
Use this table to explain the mechanism and its limit without memorizing a product name.
| Choice or concept | Remember | Check |
|---|---|---|
| Creation | Authenticated idempotent management action | Durable scoped replay and unique code |
| Redirect | Lookup plus documented redirect | Missing vs unavailable; cache freshness |
| Capacity | Workload and surviving budgets | Warm, cold and failure tests |
| Growth | Measured revisit threshold | Secondary paths and migration cost |
05 Where & why
Check the primary references
Use official references for protocol and implementation details. The questions here are teaching prompts.
06 Interview questions
Explain the reasoning out loud
Answer before opening the explanation. The questions progress from vocabulary to decisions and failure behavior.
What does Design packet mean?
What does Trade-off log mean?
What does Failure domain mean?
What makes this more than separate documents?
Why is sharding optional in the capstone?
Where is code uniqueness enforced?
What belongs in the failure table?
How do you defend a future scaling change?
07 Practice problems
Practice a new case
Solve the new cases before revealing hints. Complete worked solutions follow the set.
Identify the primary path
EasyShow the hint
Name the authority
EasyShow the hint
Capacity check
MediumShow the hint
Unknown create
MediumShow the hint
Mutable destination
MediumShow the hint
Capstone defense
HardShow the hint
Worked solutions
1. Identify the primary path
A redirect starts with code; an owner listing starts with authenticated owner identity.
2. Name the authority
The relational source of truth owns the mapping and uniqueness contract; an eligible cache is replaceable serving state.
3. Capacity check
400,000 / 86,400 × 4 ≈ 18.52 peak read QPS before extra internal work.
4. Unknown create
Retry the same scoped idempotency key and payload under the documented retry window, then replay or recover the stored result. A new key can mean a new action.
5. Mutable destination
Use a coordinated stronger freshness path or bypass cache for these mutable reads. Define in-flight behavior and test the stale-fill race.
6. Capstone defense
A defensible initial design uses stateless app instances, an authenticated management API with scoped idempotency, a relational source of truth with unique codes, and cache-aside only under a stated freshness policy. the estimation module's modeled load alone does not justify sharding. Include overload limits, instance-loss capacity and a replica read policy if replicas are used. If growth forces sharding, explain code routing, the owner index, migration and cross-shard costs.