Chapter

Turn Requirements Into APIs and Data

Define the system boundary, derive stored information from access patterns, and make failure behavior explicit.

65 min 4 objectives
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:

Code
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:

Code
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:

Code
GET /aZ91kLm

If the code exists and has not expired:

Code
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:

SituationPossible responseMeaning
Destination is malformed400 Bad RequestThe client can correct the request
Short code is unknown404 Not FoundNo mapping exists
Mapping expired404 or 410 GoneChoose whether clients may distinguish expiration
Creation rate exceeded429 Too Many RequestsClient should slow down
Storage temporarily unavailable503 Service UnavailableOperation 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.

Code
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:

Code
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:

Code
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.

Code
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 https and perhaps http;
  • 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:

  1. one API that accepts a notification request;
  2. the fields required to send it and deduplicate retries;
  3. success, validation-failure, rate-limit, and temporary-failure responses;
  4. a minimal stored record;
  5. the three most important reads or writes;
  6. 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.