Chapter
Turn Requirements Into APIs and Data
Define the system boundary, derive stored information from access patterns, and make failure behavior explicit.
Learning objectives
- Translate core workflows into small API contracts
- Design records around required reads and writes
- Explain primary keys and indexes in terms of access paths
- Include validation, idempotency, and failure responses in the contract
Contracts connect the product to the architecture
The requirement brief says users create short links and visitors follow them. The capacity estimate describes how often those actions occur. Before choosing infrastructure, define the boundary through which clients request those behaviors.
An API contract describes:
- what operation a client can request;
- what information the client supplies;
- what the system returns on success;
- what can fail and how that failure is represented.
HTTP is one way to expose the contract. The important design work is the meaning, not the particular punctuation of a URL.
Derive one operation from each core workflow
The creation workflow can become:
POST /links
Content-Type: application/json
Idempotency-Key: 7d81...
{
"destination": "https://example.com/a/long/path",
"expiresAt": "2027-01-01T00:00:00Z"
} A successful response might be:
HTTP/1.1 201 Created
{
"code": "aZ91kLm",
"shortUrl": "https://sho.rt/aZ91kLm",
"destination": "https://example.com/a/long/path",
"expiresAt": "2027-01-01T00:00:00Z"
} The redirect workflow can become:
GET /aZ91kLm If the code exists and has not expired:
HTTP/1.1 302 Found
Location: https://example.com/a/long/path Why 302 rather than 301? A permanent redirect may be cached aggressively by browsers
and intermediaries, causing future requests to bypass the service. That can be desirable
for an immutable mapping and undesirable if expiration, abuse controls, or redirect metrics
must remain enforceable. The choice follows product behavior, not habit.
Error responses are part of the contract
Write important failures explicitly:
| Situation | Possible response | Meaning |
|---|---|---|
| Destination is malformed | 400 Bad Request | The client can correct the request |
| Short code is unknown | 404 Not Found | No mapping exists |
| Mapping expired | 404 or 410 Gone | Choose whether clients may distinguish expiration |
| Creation rate exceeded | 429 Too Many Requests | Client should slow down |
| Storage temporarily unavailable | 503 Service Unavailable | Operation may succeed later |
The exact status is less important than consistent semantics. Clients need to know whether to correct, retry, authenticate, or stop.
Why creation may need idempotency
A client may submit a creation request, lose the response, and retry. If each retry creates a different short code, one intended action produces multiple records.
An idempotency key identifies repeated attempts of the same logical request. The server stores the result associated with that key for a limited period and returns the same result when the request is retried.
first request with key X → create code abc1234, remember X → abc1234
retry with key X → return abc1234, do not create another mapping Idempotency does not mean every endpoint naturally has no effect when repeated. It is a contract and storage decision that makes retries safe.
Data modeling begins with access patterns
An access pattern is a question the system must answer or a write it must perform.
Our MVP needs:
Write: store one new code → destination mapping
Read: retrieve one active mapping by code
Delete or expire: stop redirecting after expiresAt It does not need to search destinations by keyword or list links by owner because those features are outside the current scope.
A minimal record could be:
Link
- code: string
- destination: string
- createdAt: timestamp
- expiresAt: timestamp or null The short code is the natural lookup value for redirects, so make it the primary key. A primary key uniquely identifies a record and gives the database a direct access path to it.
CREATE TABLE links (
code VARCHAR(12) PRIMARY KEY,
destination TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP NULL
); This schema is illustrative; it does not mean a relational database is always required. It makes the record and uniqueness rule concrete.
An index is an additional search structure
An index helps the database locate rows by selected fields without scanning every row. The
primary key already provides an index-like access path by code.
Adding an index has a tradeoff:
- reads using that field may become faster;
- writes must also update the index;
- the index consumes storage.
Do not add indexes for hypothetical queries. If expiration cleanup frequently asks for all
rows whose expiresAt is before now, an expiration index may be justified. If cleanup can
use another mechanism or expired records are rare, it may not be.
Generating a short code
The key-space estimate showed that seven base-62 characters provide trillions of possible codes. We still need a method to choose one.
Random code with collision detection
Generate seven random base-62 characters, attempt to insert the record, and retry if the primary-key uniqueness check reports a collision.
Advantages:
- codes do not reveal creation order;
- generation can happen on many application instances;
- the approach is simple while the namespace is sparsely occupied.
Costs:
- collisions are possible and must be handled;
- randomness must be generated correctly;
- repeated retries become less attractive as occupancy grows.
Numeric ID encoded as base 62
Obtain a unique number and encode it with base-62 symbols.
Advantages:
- uniqueness follows from the numeric ID;
- no collision retries are needed.
Costs:
- generating IDs may require coordination or allocated ranges;
- sequential codes can reveal traffic or be easy to enumerate;
- one global counter can become a bottleneck if designed naively.
Neither method is universally superior. The choice depends on security expectations, throughput, operational simplicity, and how uniqueness is coordinated.
Validation is architecture too
Accepting an arbitrary destination can turn the service into an abuse tool. The creation path should at minimum consider:
- allowing only supported schemes such as
httpsand perhapshttp; - rejecting malformed or excessively long destinations;
- rate limiting clients;
- blocking known malicious destinations;
- preventing access to internal network addresses if the service later fetches URLs;
- defining reporting and removal behavior.
The MVP does not need to solve global abuse prevention, but the design should identify the trust boundary: user-controlled input becomes a redirect served to other people.
Practice: contract before components
For a notification service, define:
- one API that accepts a notification request;
- the fields required to send it and deduplicate retries;
- success, validation-failure, rate-limit, and temporary-failure responses;
- a minimal stored record;
- the three most important reads or writes;
- which fields require uniqueness or an index.
Do not draw a queue or worker yet. First make the behavior and data boundary precise.
Chapter checkpoint
You should now be able to explain why the URL code is the primary lookup key, why a retry can create duplicate state, and why adding a database index is not free. The output of this chapter is a contract and data model that the first architecture can implement.