← the whole book on one page

Chapter 7: API Architecture — Contracts, Traffic, and Evolution

An API is a promise made to strangers. Architecture is deciding which promises you can keep.

Encore is about to do something it cannot undo: publish an application programming interface (API) to the outside world. The requests have been piling up. Venues want to embed ticket sales in their own websites, resale partners want a feed of available inventory, and a festival wants fans to check out inside its own mobile app. Each request is reasonable, and each opens a door that will not easily close again. The reason is simple once we say it out loud. The day the first partner ships code against api.encore.com, Encore has consumers it cannot see, running software it cannot change, on release schedules it does not set. If Encore then renames a field or removes an endpoint, it is not fixing its own code; it is breaking someone else’s, and that kind of breakage ends in contract disputes and lawyers rather than a quick patch.

That makes a public API a different kind of boundary from the ones we have drawn so far. The internal boundaries of Chapters 5 and 6 connect teams who share a Slack workspace: when one Encore team needs another to change an interface, the two teams can talk it over at lunch and agree on a migration date. A public API connects strangers across a trust boundary, and it connects them for years. We cannot ask a festival’s app developers to upgrade by Friday, and we cannot fully trust what their code will send us. There is one more reason to take this boundary seriously: a public API is the first platform most companies build, whether they notice it or not. Once others build their products on top of ours, we have stopped shipping a product and started running a platform, which is where this book is heading.

The underlying idea is old. Parnas’s criterion from Chapter 1 told us to hide each decision likely to change behind an interface, so that the change stays local. In the late 1980s Bertrand Meyer recast that interface as a contract, with obligations on both sides: the caller promises to send valid requests, and the supplier promises what it will return. What has changed since then is not the idea but the audience. When a class’s interface changes, its callers sit in the same codebase and the same team can fix them in the same commit. When a public API changes, its callers are people we will never meet, who built against our promises on schedules we do not control. Keeping promises to that audience, while still letting our own system evolve underneath, is the essential complexity of this chapter.

7.1 Designing APIs

Modeling APIs on the business domain

Every API designer begins with a choice of what to show the world, and the beginner’s choice is the obvious one: the database itself, with URLs in front of it (GET /tables/orders?join=...). It feels efficient, because the tables already exist and the endpoints almost write themselves. The trouble is what it exports. The shape of our tables is the most volatile thing we own; we split a column, rename a field or move a table into another service whenever the business teaches us something new. An API that mirrors the schema hands that volatility to the people least able to absorb it, partners who learn of our refactoring only when their integration breaks. An architect’s API takes the opposite stance. It models what a consumer may meaningfully do: browse events, hold seats, purchase, refund. I call these the domain’s affordances, borrowing the designer’s word for what an object invites us to do with it. A door handle affords pulling, a flat plate affords pushing, and nobody needs to read the hinge mechanism to get through the door. In the same way, POST /holds affords claiming a seat, and nobody needs to know how Encore stores seats to claim one. That gives us a simple test for any API: could a partner integrate without a diagram of your schema? If the honest answer is no, the schema has leaked. Four decisions carry most of the weight in passing that test.

Resources and workflows. The first decision is how to name things. The nouns of the domain become resources, addressable things a client can read and act on, such as the seats of an event at /events/{id}/seats. Processes that unfold over several steps become workflow resources: the process itself gets a name and an address, instead of hiding inside a verb. Encore’s seat hold shows why this matters. A hold is a temporary claim on a seat with a five-minute time to live (TTL), after which the seat returns to sale. Encore models it as POST /holds, which creates a hold resource and returns it with its expiry, instead of a flag tacked onto the seat, such as ?action=lock. The difference looks cosmetic, but it is not. A hold that is a resource has an identity, so a partner can read it back to see how long it has left, cancel it when a fan changes their mind, and authorize who may touch it, while Encore can audit every hold ever made. A hold that exists only as the side effect of a query parameter has none of these: the partner cannot ask about it, cannot cancel it, and can only hope it is still there when the fan reaches checkout. The same thinking covers operations that take longer than a request should wait. Instead of holding the connection open, the server answers 202 Accepted, meaning “I have your request and am working on it,” and returns a status resource that the client polls until the work is done.

Errors as contract. The second decision concerns what happens when things go wrong, and it deserves more care than it usually gets. Partners write more code against our failures than against our successes. The happy path is written once and rarely touched again; the failure paths are where their edge cases, their retries and their support tickets live. So an error is part of the contract, not an afterthought. Encore’s error bodies use the problem-details format of Request for Comments (RFC) 9457, a standard JSON shape for describing an HTTP error, and each carries a stable, machine-readable code that partners can branch on without parsing English sentences. Above all, each error tells the caller whose move it is. There are only three answers. The caller must fix the request, because something in it was wrong and sending it again will fail again. The caller may retry later, because the problem is on Encore’s side or temporary. Or the caller should give up, because the thing they wanted cannot happen, as when the seat has already been sold to someone else. An error that answers that question lets a partner’s code do the right thing without a human reading the logs.

Idempotency keys. The third decision brings Chapter 4’s duty of idempotency to the edge, where the network is least reliable of all: a fan’s phone on a crowded mobile network, a partner’s server across the public Internet. Suppose a partner sends a purchase and the connection drops before the response arrives. The partner cannot tell whether the purchase happened, so it retries, and without protection the fan is charged twice. Encore’s POST /purchases therefore accepts an Idempotency-Key header, a unique value the client chooses for each purchase attempt and sends again with every retry of it. The first request with a given key does the work and stores the outcome; any retry with the same key gets that stored outcome replayed, instead of a second charge. If a client reuses a key with a different payload, a different seat, say, or a different amount, Encore returns an error, because that is a bug in the client and guessing which request it meant would be worse. The header is still an Internet Engineering Task Force (IETF) draft rather than a finished standard, but that is no reason to wait: no API that touches money should ship without the pattern.

Collections and push. The fourth decision covers data that arrives in bulk or arrives later. Lists come back in pages, and the obvious way to page, by offset (?offset=100), fails exactly when Encore is busiest. During an on-sale, holds and orders are being inserted continuously, so between a partner fetching rows 1 to 100 and fetching rows 101 to 200, new rows shift everything along; the second page repeats some rows and silently skips others. Encore pages instead by an opaque cursor, a token the server hands back that marks exactly where the last page ended, so the next page resumes from that position whatever has been inserted in the meantime. “Opaque” means the partner treats it as a sealed value and never builds one by hand, which leaves Encore free to change what is inside. The server also caps page size, so no single request can ask for a million rows. Some consumers do not want to ask at all; they want to be told when something changes. For them Encore offers webhooks, HTTP calls that Encore makes to an address the partner registers, and webhooks need a contract of their own. Payloads are signed, so the partner can verify they really came from Encore. Delivery is at-least-once with backoff, meaning Encore keeps retrying, at growing intervals, until the partner acknowledges, which in turn means the partner will sometimes receive the same event twice. So every event carries an ID the receiver deduplicates on. Finally, there is a replay endpoint for the partner whose systems were down all weekend and who needs to catch up on what they missed, because sooner or later one will be.

Choosing a protocol

Once we know what the API should say, we must choose how it travels, and here the arguments tend to grow heated out of proportion to the stakes. Three protocols cover nearly every case. Representational State Transfer (REST) over HTTP with JSON bodies is the web’s native idiom: resources at URLs, acted on with the standard HTTP verbs. gRPC sends compact binary messages over HTTP/2, with client and server code generated from a shared, strictly typed definition. GraphQL exposes a single endpoint and a typed schema, and lets each client write a query naming exactly the fields it wants. None of them is better in general. Each is better at something, so the sensible way to choose is the one Chapter 1 taught for architecture as a whole: by the characteristics that matter to the caller in question, as Figure 7.1 lays out.

  REST/HTTP+JSON gRPC GraphQL
Sweet spot public APIs, broad reach internal service-to-service, low latency client-driven aggregation, many UIs
Typing schema by discipline (OpenAPI) strict, compiled strict schema, flexible queries
Caching HTTP-native, excellent roll your own harder: persisted queries (GET-cacheable) or normalized client caches
Client diversity anyone with curl generated stubs; browsers need gRPC-Web or Connect needs investment in client tooling
Failure surface familiar to every developer deadlines and cancellation built in one endpoint, so authorization must be checked per field

Figure 7.1 — The protocol decision, made on characteristics. Encore uses REST for the public API, for its reach and cacheability; gRPC where internal services talk to each other on the hot path; and GraphQL nowhere, for now. Chapter 8 shows the scale at which a backend-for-frontend (BFF) layer would justify it.

Encore’s answer is unglamorous, and I count that in its favor. Its public callers are strangers, and strangers arrive with every HTTP client ever written, from a venue’s decade-old ticketing system to a developer testing with curl on a laptop. REST reaches all of them, and ordinary HTTP caches, from the browser to the content delivery network, already know how to cache it, which matters when a crowd of fans is loading the same event page at the moment an on-sale opens. Inside the platform the callers are Encore’s own services, built by Encore’s own teams, so the strict typing and low latency of gRPC are worth the generated stubs. GraphQL earns its complexity when many different user interfaces need to assemble data in many different ways, and Encore does not yet have that problem.

Whatever the protocol, one rule holds: the contract comes first. The OpenAPI document that describes a REST API (or its AsyncAPI counterpart, for Chapter 6’s streams) is written, reviewed and linted (checked automatically against the house rules) before anyone writes the code behind it. The reason is longevity. The implementation will be rewritten, perhaps several times, but the contract outlives every version of it, because partners have built against it. Railways show the same pattern. They replace their locomotives generation by generation, steam giving way to diesel and diesel to electric, while the gauge, the distance between the rails, stays fixed, because everything else on the line depends on it. A contract is the gauge of an API. The analogy breaks in one instructive place, though. A railway that decides to regauge a line owns every train that runs on it, so it can convert the rolling stock at the same time as the track. We own none of our clients. If Encore changes its contract, it cannot convert the partners’ code, and that asymmetry is the subject of the next section.

Recap. A good API models the domain’s affordances, what a consumer may meaningfully do, rather than the shape of its tables. Workflows such as a seat hold become resources with their own identity, every error tells the caller whose move it is, and idempotency keys are part of the contract, so a retried purchase never charges twice. Lists page by cursor, and webhooks come with signatures, deduplication IDs and replay. The protocol is chosen by the characteristics each caller needs, and the contract is written before the code because it will outlive the code.

Exercise 7.1. Design the resources and verbs for Encore’s flow from seat hold to purchase: list each endpoint with its HTTP method, path and the resource it returns. Include two error responses, card declined (402) and hold expired, each with its machine-readable code. For each error, state whose move it is (fix the request, retry later, or give up) and what the partner’s code should do next.

7.2 Evolution and Compatibility

Breaking changes and versioning

Designing an API is the easy part; living with it is the hard part. Chapter 5’s expand–contract discipline returns here, with higher stakes. Recall how it works: to change a contract, we first expand it so the old and new shapes are both supported, wait until every consumer has moved to the new shape, and only then contract by removing the old one. Inside Encore, that waiting is short, because internal consumers redeploy weekly and their teams can be asked to hurry. Outside, the waiting can be very long. The festival app reaches fans’ phones only after app-store review, and once installed it stays installed; a fan who never updates is still calling whatever version of our API that build was written against. Such an app may pin your API for a year.

In my experience, longer, and the lesson is that consumers depend on far more than we think we promised. I maintain MockK, an open-source mocking library used across a great many Kotlin and Android builds. If I delete one convenience overload, a small helper I would have described as incidental, the issue tracker fills with strangers’ broken builds. Nobody asked my permission to depend on it; they simply could, so they did. Hyrum Wright, an engineer at Google, lent his name to the general form of this lesson, now called Hyrum’s law: with enough users, every observable behavior of an interface will be depended on by somebody, whatever the documentation promised. For Encore, that means a partner may rely on the order in which seats come back, on the exact wording of an error message, or on a hold response arriving within a certain time, none of which we ever wrote down as a guarantee. So the real public API is everything its consumers can observe, and that is always more than we documented. To keep that larger, unwritten surface stable, Encore’s public API follows four rules:

The third rule descends from Jon Postel’s robustness principle, written into the early Internet protocol specifications: be conservative in what you send and liberal in what you accept. It was wise advice for a young network, where getting machines from different makers to talk at all mattered most. Protocol designers have since learned its price. A receiver that accepts anything teaches senders that anything is acceptable. Suppose Encore quietly accepted dates in three different formats; partners would send all three, and the day we tightened the parser we would break them, so our generosity would have become a promise we never meant to make. Yesterday’s tolerance hardens into tomorrow’s obligation. So we keep our tolerance narrow and precise: ignore what you do not recognize, apply a default when something is missing, and refuse what is malformed, with an error that tells the sender how to fix it.

Links offer another way to evolve without breaking anyone. Roy Fielding’s 2000 dissertation, which defined REST, made links central to the style: a response should tell the client what it can do next and where to do it, the way a web page carries its own links. Much of the industry kept his tidy URLs and quietly dropped the links. That loss matters for evolution. Suppose an order response includes "cancel": {"href": ...}. A client that follows that link, instead of building the path itself, keeps working if Encore later moves cancellation to a different endpoint or adds a step to it, because the server simply hands out a new link. A client that hardcoded /orders/{id}/cancel breaks. Links take some extra work on both sides, so Encore uses them where workflows are likeliest to change.

Contract tests at the border. Rules only help if something checks them before a change ships. On every build, the provider’s continuous integration (CI) pipeline replays the recorded expectations of every registered consumer, including the few external partners who publish how they use the API; this is Chapter 5’s consumer-driven contract machinery, extended past the company’s edge. Most partners publish nothing, so for them two other checks stand in. A breaking-change differ for OpenAPI (such as oasdiff) compares the new contract with the old one and flags anything removed, renamed or made stricter. And replaying sampled real partner traffic against the new build catches the unwritten dependencies Hyrum’s law warns about. The payoff is timing: a change that would break the resale partner fails the build on Tuesday, in front of the engineer who made it, instead of causing an outage on Friday, in front of fans.

Recap. Evolving an API among strangers means keeping old promises while making new ones, and those promises include every behavior partners can observe, not only what we documented. Changes are additive, enum values are open sets, and a field’s meaning never changes. Both sides read tolerantly, but narrowly. A v2 is a last resort, run with a measured migration funnel and a sunset date in writing, and contract tests turn breakage into a failing build before it becomes a partner’s outage.

Exercise 7.2. Regulation requires Encore to split price into base_price and fees. Design a migration that breaks no one. Produce three things: the field strategy (which fields exist in each phase, and what price means while both shapes coexist); a timeline from first announcement to sunset, with the headers and partner communication at each step; and a description of exactly what the last unmigrated partner sees at sunset.

7.3 API Traffic Management

The API gateway

So far we have designed the promises an API makes and learned how to change them without breaking anyone. Now we turn to the traffic that arrives to test those promises. All public traffic enters Encore through an API gateway, a single front door that every outside request passes through before it reaches any service. The gateway’s job is to handle the cross-cutting concerns: the questions that must be asked of every request, whatever it is for. Who is calling, and are they who they claim to be (authentication)? How fast are they calling (rate limiting), and how much of their agreed allowance have they used (quotas)? Can a recent answer be reused (caching)? Does the request need tidying before it travels on, such as normalizing headers or rejecting an oversized body (request shaping)? And should this particular request go to the new release or the old one (canary routing)? None of these questions depends on what the caller wants to buy. That gives us a simple rule for drawing the line: anything that knows what a seat is belongs in a service, not in the gateway.

An old picture helps here. A medieval town’s gatekeeper checked who came through the gate and collected the toll, but he did not tell the bakers how to bake. His knowledge was about travelers, not bread. A gateway should work the same way: it may know who is calling and how often, but never what they are buying. The analogy breaks at the walls, and the break is instructive. The medieval gate guarded a town whose walls kept everyone else out, so a traveler who had passed the gate could be trusted inside. A modern system has a gate but no wall worth trusting: an attacker may already be inside through a compromised service or a stolen credential. So every service behind the gateway still checks the credentials of each request it receives, as Section 7.4 insists. The gateway is the first check, never the only one.

The new-ESB trap. Every gateway product will happily host more than cross-cutting rules. It will let you write rules that transform payloads from one shape to another, orchestrate a sequence of calls to several services, and gradually accumulate business logic. Each rule seems convenient on the day it is written. Add enough of them and you have rebuilt the enterprise service bus (ESB), with the consequence that a change to the product now means redeploying shared middleware that every team depends on. A practical test: if a change to the gateway’s configuration needs a domain expert to review it, business logic has leaked into the gateway.

We have seen this story before. Chapter 2 told how the buses of service-oriented architecture (SOA) went exactly this way: they began as plumbing between services and ended as the place where the business rules lived, owned by a central team that every change had to queue behind. The gateway offers the same temptation with a friendlier configuration language, and the old lesson still holds: keep the intelligence in the endpoints and the pipes ordinary. When the rules that decide how a hold becomes a purchase live in the Orders & Payments service, the team that owns that service can change them; when they live in the gateway, nobody quite owns them.

The gateway also has a natural boundary in the other direction. Architects describe traffic by the direction it flows on a diagram. North–south traffic crosses the edge, from outside callers such as fans’ apps and partners’ servers into Encore. East–west traffic flows sideways between Encore’s own services, as when one of Encore’s services calls another to complete a purchase. The gateway handles the first kind; the second belongs to the service mesh of Chapter 5. The mechanics of the two look similar, since both sit in the path of requests, route them and apply policy to them, but the trust differs. The gateway faces strangers whose identity must be proven and whose appetite must be limited; the mesh connects services that Encore deployed itself. Merging the two into one component would be a mistake, because it would tie the partner-facing surface to Encore’s internal topology: every time a team split or renamed an internal service, the component that partners depend on would have to change with it.

Edge vs. interior: gateway and mesh Partners and apps reach the API gateway at the edge, which handles authentication, quotas, rate limits and canary routing and forwards into the interior; there the BFFs and public API services call Inventory and Orders east–west, inside the service mesh with mTLS, retries and telemetry. EDGE · NORTH–SOUTH INTERIOR · EAST–WEST · SERVICE MESH Partners & apps Gateway authn · quotas · rate limits · canary BFFs Chapter 8 Public API services Inventory Orders mTLS · retries · telemetry between services Outside callers Edge gateway: north–south Service in the mesh: east–west Call

Figure 7.2 — Edge vs. interior. The gateway alone handles north–south traffic, the requests from outside callers, applying authentication, quotas, rate limits and canaries. The mesh around every service handles east–west traffic, the calls between Encore’s own services, applying mutual TLS (mTLS), retries and telemetry. The mechanics are similar; the trust is not.

Rate limiting as fairness engineering. It is tempting to think of rate limits as a defensive afterthought, but they are better understood as part of the design. Consider a bridge’s posted load limit. Engineers choose it from the bridge’s real capacity, publish it on a sign, and enforce it, so that no driver has to discover the capacity by experiment. Encore’s limits work the same way. Each partner gets a contractual quota, which caps its total usage, and a burst allowance, which lets it exceed its steady rate briefly. The usual mechanism for this is a token bucket: imagine each partner has a bucket that refills with tokens at a steady rate, up to a fixed size. Each request spends one token. A partner that has been quiet has a full bucket and can send a short burst; a partner that sends continuously is held to the refill rate; and a partner whose bucket is empty must wait. During on-sales Encore adds event-scoped limits, applied per partner to the specific event on sale, so that one partner’s polling cannot starve another’s checkout. Without them, a resale partner refreshing seat availability for a popular concert hundreds of times a second could use up the capacity that a venue’s fans need to complete their purchases. Encore documents its limits, returns the caller’s current standing in RateLimit and RateLimit-Policy headers (following the IETF draft), and enforces them with a 429 Too Many Requests response and a Retry-After header that says when to try again. That choice reflects a conviction: a predictable rejection is better than unexplained slowness. A partner who receives a 429 with a time to retry knows exactly what happened and what to do; a partner whose requests merely grow slow cannot tell overload from failure, and usually responds by retrying harder.

Releases at the edge. Because every outside request passes through it, the gateway is also a natural place to release new versions carefully. It can run a canary, sending a small portion of real traffic to the new version while the rest stays on the old one, and choose that portion by request header, by partner tier or by percentage. For example, Encore might first send only requests carrying a test header from its own engineers, then those from one willing partner, then 5% of all traffic. It can also send shadow traffic: it copies production requests to the next version, lets that version process them, and throws its responses away, so the new code is exercised under real load while callers only ever see the old version’s answers. On an API that moves money, shadowing needs care, because the new version really does process what it receives. A mirrored purchase would make a second charge and place a second hold on the seat. So Encore mirrors only reads, or sends shadowed writes to stubbed payment and inventory sinks that accept the calls and do nothing with them. The two safeguards catch different problems. Section 7.2’s contract tests catch semantic breaks, where the new version answers differently than consumers expect; shadow traffic catches performance breaks, where the answers are right but too slow or too costly under real load.

Recap. The gateway owns what every caller shares (identity, rate, quota, caching, request shaping and release routing) and leaves the domain to the services; the day it learns what a seat is, it starts becoming an ESB. The mesh owns the traffic between services inside the edge, under a different kind of trust. Rate limits are published contracts that make overload fair and predictable, and canary and shadow traffic let us watch a release under real conditions before outside callers feel it.

Exercise 7.3. List everything your edge does, whether it runs on a gateway, a load balancer or nginx, and sort each item into two columns: “cross-cutting” (it would apply to any request, whatever the domain) and “knows the domain” (it depends on what the request is about, such as a rule about seats or prices). Everything in the second column is leaked logic.

7.4 API Security

Securing a public API

Encore’s API now fronts two things attackers want: money, and inventory that is scarce by design. A front-row seat for a sold-out show can be sold only once, and a payment cannot be taken back without cost. These are exactly the assets the Open Worldwide Application Security Project (OWASP) API Security Top 10 was written to protect. It helps to think of the internet as one more integration partner, but a hostile one, and a diligent one: it reads your documentation more carefully than your friends do, because every endpoint you publish is a door it will try. Long before anyone wrote a web API, Saltzer and Schroeder set down design principles for protecting information, which Chapter 10 takes up in full. Two of them matter most here. Least privilege says that every caller should hold only the rights its job requires. Complete mediation says that every access to every object must be checked, every time, with no shortcuts for callers who were checked a moment ago. Three defenses put those principles into Encore’s architecture: the right OAuth flows, well-made tokens, and object-level checks against the Top 10’s worst threats.

OAuth 2.0 and OpenID Connect (OIDC), cast correctly. OAuth 2.0 is a framework for handing out tokens that let a caller act on an API, and OpenID Connect adds a standard way to say who the user is. The framework offers several flows, ways of obtaining a token, and the first decision is casting: which flow each kind of caller gets. Encore follows the OAuth Security Best Current Practice (RFC 9700, 2025), the current consolidated advice on doing OAuth safely, which OAuth 2.1 gathers into a single specification. Encore has two kinds of caller, and they get different flows because their situations differ.

Two habits are ruled out entirely. Callers never send passwords in API calls, and long-lived keys are never embedded in mobile binaries, where anyone with a decompiler can read them. Finally, scopes, the named permissions a token carries, name affordances: holds:write, events:read. This reuses the design vocabulary of Section 7.1, where the API was modeled as what a consumer may meaningfully do, and it gives least privilege a precise meaning. A partner that only displays event listings receives events:read and nothing else. The smallest set of affordances a caller needs is exactly the set of scopes it is granted.

Tokens. Encore’s tokens are JSON Web Tokens (JWTs): signed, self-contained statements that the gateway can verify without calling anyone. Four properties keep them safe. They are short-lived, so a stolen token soon expires. They are audience-bound, naming the API they are meant for, so a token issued for one service is refused by another. They are algorithm-pinned: the verifier accepts only the one signing algorithm Encore uses and refuses anything else, which closes a well-known class of attacks in which a forged token simply announces a weaker algorithm, or none at all. And they are verified at the gateway, so a malformed or expired token never reaches a service.

Encore, of all businesses, should recognize the weakness of a plain bearer token. It is a paper concert ticket: whoever holds it gets in, and the door staff never ask how they came by it. A ticket picked up off the sidewalk works as well as one bought at the box office. DPoP turns it into a named ticket, checked against identification at the door. The client holds a private key, the token is bound to that key, and every request carries a fresh proof signed with it. A thief who copies the token but not the key holds a ticket in someone else’s name, and the door refuses it.

The gateway is not the end of the story, because a request travels on from the gateway to the services behind it, and those services need to know two different things. The mesh’s workload identity, the mTLS certificate each service carries, proves which service is calling: this request really comes from Orders & Payments and not from an impostor inside the network. It cannot say who the request is for, and that is the question a service must answer before it shows a fan’s orders. So the end user’s claims travel downstream alongside the workload identity, either in the forwarded token itself or through RFC 8693 token exchange, in which a service trades the incoming token for a new one scoped to the next service it calls. Each layer checks what only it can check. That is defense in depth, and it previews Chapter 10’s zero trust, in which no request is trusted merely because it comes from inside.

JWTs carry a trade-off. Because the gateway verifies them locally, they are cheap to check, but they stay valid until they expire, even after Encore wants them revoked (say, when a partner’s contract ends or a fan’s account is compromised). Opaque tokens make the other choice. An opaque token is a random string that means nothing by itself, and the API must ask the authorization server whether it is still valid, a call known as introspection. Revocation then takes effect at once, at the cost of that extra call. Opaque tokens fit where revocation latency matters more than validation cost.

The Top 10 for ticketing. Of the OWASP list, two entries hit a ticketing business hardest. The first is broken object-level authorization. Consider GET /orders/{id}. A fan who is properly signed in, with a valid token and the right scope, changes the ID in the URL from her own order to the next number. If the service checks only that she is signed in, it hands her a stranger’s order, name, seats and all. The rule must therefore be that the service checks the caller owns order {id}, on every object and every request. This is complete mediation at its most concrete. The gateway cannot do this check, because it knows who is calling but not who owns order 48213. Only the service that owns the object can say who owns it.

The second is unrestricted access to sensitive business flows (API6 in the 2023 list). Here no single request is wrong. For Encore, the case is bots mass-creating seat holds to lock up inventory during an on-sale. Each request is well-formed and authenticated, and each looks just like a fan holding a seat; taken together, they are an attack that empties the venue of real buyers. No per-request check can see that, which is why Bot Screening (Chapter 1) sits in front of this API, judging the pattern of behavior rather than the validity of each call. A neighboring entry, unrestricted resource consumption, covers the polling floods that per-partner quotas (Section 7.3) absorb: a partner refreshing availability so often that it consumes capacity other callers need.

All three threats become fitness functions, the automated checks that keep an architectural property true as the system changes. Object-level authorization gets per-endpoint authorization tests in CI, which try to read and change one caller’s objects as another caller and fail the build if they succeed. Abuse of business flows and resource exhaustion get abuse scenarios in the load suite, which replay a bot swarm or a polling flood and confirm that the defenses hold.

Recap. API security applies two old principles, least privilege and complete mediation, with new machinery. Each audience gets its own OAuth flow: fans’ apps use the authorization code with PKCE, and partners’ servers use client credentials proven by a private key or mutual TLS. Scopes grant the smallest set of affordances a caller needs. JWTs are short-lived, audience-bound and algorithm-pinned, with DPoP binding the costly ones to their holder. Object-level checks belong to the service, which alone knows who owns what, and abuse resistance is tested under load.

Exercise 7.4. Take the endpoint GET /holds/{id}. List every class of caller that might use it (at least a fan, a partner and a support tool). For each, write the object-level rule it must satisfy to read a given hold; for example, a fan may read a hold only if she created it. Then say where in your design each rule is enforced (gateway, service or elsewhere), and explain why that is the right place.

7.5 Running an API Program

The moment an API has outside users, it has become a product, and it needs a roadmap whether or not anyone has been staffed to write one. The difference between a product and a piece of internal plumbing is the audience. A colleague in the next team will put up with a confusing endpoint because they are paid to ship and can walk over and ask. A partner’s developer owes Encore nothing: they must find the API, learn it and come to trust it on their own time, and if any of those steps is hard they will quietly build against a competitor instead. The first four sections of this chapter were about the contract itself and the traffic it carries: how to design it, evolve it, manage its load and secure it. This section is about the organization around it, and Encore’s program rests on three disciplines.

Developer experience (DX) is the first, and Encore treats it as a conversion funnel, the way a marketing team treats a sign-up page. A funnel counts how many people start a journey and how many reach its end, and every step in between loses some of them. Here the journey begins when a developer lands on the portal and ends with their first successful call, and Encore’s target is under 10 minutes. Three things make that number reachable. Sandbox keys are issued instantly, so nobody waits days for a human to approve an account before they can try anything. Quickstarts are copy-paste ready, so the first request to, say, list the week’s concerts runs as written. And the reference docs are generated from the OpenAPI contract instead of written by hand, which means they cannot drift from what the API actually does: when a field is added to the contract, it appears in the docs in the same build. Each of these removes a place where a curious developer might give up, and every developer who gives up is a partner Encore never hears from.

Governance ships as linting, and this is the second discipline. Every API program needs rules, such as how resources are named, how lists are paginated and what shape an error takes. The traditional way to enforce them is a review board that reads each new design in a meeting, which is slow, depends on who happens to attend, and turns architects into gatekeepers. Encore instead writes the rules as automated checks in the style of Spectral, an open-source linter for OpenAPI documents, and runs them in the CI pipeline against every contract change. If a team proposes GET /getEventList?page=3, the build fails within seconds and names the rules it broke: paths name resources as nouns rather than actions, and lists page by opaque cursor, not page number (both from Section 7.1). Consistency then comes from tooling instead of review meetings. This is Chapter 5’s paved road, applied to contracts: the right way is also the easiest way, because the tool tells you about a mistake before a reviewer has to.

The aim of those rules deserves a moment, because it is easy to misread. Encore is not trying to appoint a master planner who designs every endpoint from the center; that does not scale to many teams, and it would make the API team a bottleneck. It is trying to give every team a shared vocabulary, so that endpoints built independently still feel as though one hand wrote them. When the team behind event listings and the team behind seat holds page their results the same way and return errors in the same shape, a partner who has learned one corner of the API has learned all of it. Many teams’ endpoints compose into one surface that a partner learns once.

Lifecycle discipline is the third, and it means that every endpoint is born with four things already decided, before its first release rather than after its first incident. It has an owner, a named team that answers when it misbehaves. It has a tier, a statement of how critical it is, so that the purchase endpoint is not treated with the same urgency as one that serves venue photos. It has a service-level objective (SLO), the target for availability and latency against which that team is measured. And it has a deprecation policy, the promise of how much notice partners get before it retires, carried out with the Deprecation and Sunset headers of Section 7.2. Endpoints that are created without these tend to become orphans: still in production, still called by someone, and owned by no one when they break.

Put those three disciplines together and something larger becomes visible. Encore’s API portal offers self-service onboarding, golden-path contracts that teams start from, and guardrails in CI that catch mistakes early. That is exactly the shape of an internal platform, and so the portal is, quietly, Encore’s first platform product, even though nobody named it that. Chapters 14 and 15 take the same pattern of self-service, golden paths and guardrails and scale it to the whole company.

The same machinery also serves a newer kind of caller. When AI agents call these APIs as tools (Chapter 12), they need no special regime of their own. They inherit the contracts, scopes and quotas that already govern every partner: a scope such as events:read becomes the limit of what the agent is permitted to do, and a quota becomes its budget, the most it can spend before Encore says no. An API program built for human developers turns out to be the right control surface for software ones.

I have seen how much of this work is human rather than technical. At a treasury-software company I led a turn toward API-first development, and we ran it as an internal startup, with a mission to win over the rest of the company rather than a mandate to impose on it. I wrote the first guidelines (89 rules, RFC-style, argued line by line), the linting that enforced them and a fair share of the early contracts, and I spent as much time pitching the idea as coding it. Adoption was slow at first, and then it became cultural: after more than 1,000 pull requests and about 200 contracts, the company built contract-first by default, and nobody had to be told to. What changed minds was not the style guide. It was the review comments and the lunch conversations, because a rule becomes a habit only when someone explains why it exists. A lint rule can tell an engineer that they broke a convention; it takes a person to persuade them the convention is worth keeping.

Recap. An API program is a product discipline, because outside developers owe the API nothing and must be won over. DX is measured in minutes to first successful call, with a target under 10. Governance ships as lint rules in CI, which give many teams one vocabulary instead of one master planner. Every endpoint is born with an owner, a tier, an SLO and a deprecation policy. Together these make the API portal Encore’s first platform product, and the same contracts, scopes and quotas govern AI agents that call the API as tools.

Exercise 7.5. Time a colleague, or yourself, from a standing start (no account, no key, no prior knowledge of the API) to the first successful call against your team’s API. Write down each step and how long it took. For every minute past 10, name the friction that caused it, such as waiting for a key, a broken example or a missing doc, and the change that would remove it.

Kata: The Public API

Your brief: “Atlas.” A freight-visibility company whose shippers embed its tracking in their own products. Atlas publishes three assets. Container positions are updated roughly hourly. Customs status is event-driven, fed by streams like those in Chapter 6. Estimated-time-of-arrival (ETA) predictions come from a machine-learning (ML) model and are revised continuously; a v2 model, whose confidence intervals have a different shape, ships in six months. There are three kinds of consumer: five enterprise shippers with service-level agreements (SLAs) and SOC 2 security questionnaires; about 200 self-service mid-market integrators; and one giant retailer that “will send 50× everyone else’s traffic and negotiate like it.” Regulatory note: a shipper may see customs status only for countries where it is the importer of record, the party legally responsible for bringing the goods into that country.

Before you begin, notice what makes Atlas harder than it first looks, because each difficulty maps onto a section of this chapter. The three assets move at three different rhythms (hourly positions, event-driven customs updates, continuously revised predictions), so a single polling design will either waste traffic or miss changes, and the push design has to suit each one. The ETA model will change shape in six months, which means the contract you publish today must already leave room for a prediction it cannot yet describe; that is Section 7.2’s evolution problem with a date on it. The three consumer classes want different things from the same surface: the enterprise shippers want guarantees and audit answers, the mid-market integrators want to get started without a sales call, and the retailer wants capacity on a scale that could crowd out everyone else, which is the fairness problem of the edge. And the customs rule is an object-level authorization rule: whether a shipper may see a given customs event depends on facts about that shipment and that country, which only the service holding the customs data knows. Keep these four pressures in view as you work, and the deliverables below will feel less like a checklist and more like one design seen from five sides.

Deliverables:

  1. Contract — An OpenAPI sketch with resources for shipments, positions, customs events and ETAs; an error catalog in which every error tells the caller whose move it is (fix the request, retry later, or give up); and a push design, using webhooks or streams.
  2. Evolution policy — The rules for additive change, a migration plan for the ETA v2 model, and the mechanics of sunsetting the old version.
  3. Edge design — A diagram like Figure 7.2; quotas for each consumer tier, including the retailer; and a canary strategy for releasing changes.
  4. Security architecture — The OAuth flow for each consumer class, a scope model, and the object-level rule for per-country customs data, with the place where that rule is enforced.
  5. Program sheet — Targets for the DX funnel, three lint rules you would enforce from day one, and the tier and SLO of each endpoint.

Rubric:

Criterion The question to ask yourself
Affordance modeling Could a shipper integrate from the documented resources and workflows alone, without seeing your internal schema?
Stranger-proof evolution Does the ETA v2 plan still work for a consumer who ignores your emails for a year?
Fairness engineering Does the retailer’s 50× traffic fit within limits you designed, or only within exceptions you negotiated?
Enforcement placement Is every authorization rule enforced in the component that holds the knowledge the rule depends on?
Program as product Could a mid-market integrator reach a first successful call within 10 minutes without talking to you?

Where you now stand

You can now design contracts that model a domain’s affordances, evolve them without breaking callers you have never met, and run the edge as both infrastructure and product. But every response we have designed in this chapter ends up on a browser or phone screen, and the book has not yet looked at what happens there. The frontend has an architecture of its own: the economics of where and when a page is rendered, the composition of one screen from the work of several teams, and a security perimeter that runs inside a device we do not control. A team that treats all of this as “just the view layer” can undo a well-decomposed backend on the very first page a fan opens. That is the subject of Chapter 8.

References