The idea
What it is
Designing the API first means deciding what the caller sees — the public method signatures, the interface, the contract — before you write any implementation. You write down book(request): Result<Ticket> and agree on it, and only then do you fill in the body. It flips the usual habit of coding the logic first and letting whatever methods fall out become the "API" by accident. Here API means the public face of any class or module — its methods, parameters, and return types — not just a web or REST endpoint.
Think of wiring a building. You don't invent a brand-new socket shape in every room and then hope appliances fit. Everyone agrees on the shape of the socket first — the contract — and then electricians wire behind the wall and appliance makers build plugs, independently, confident the two will meet. The socket is the API: a small, stable, public shape that hides an ocean of wiring nobody calling it needs to think about. Design that shape on purpose, up front, and everything that plugs into it gets simpler.
The one sentence to remember
Design the contract — the public signatures a caller sees — before the implementation: decide what plugs into the socket before you wire the wall.
Mechanics
How it works
Design from the caller's perspective
The single most useful trick is to write the client code you wish you could call first, before any implementation exists. Pretend the class is already done and write the few lines that use it: const ticket = lot.park(car);. Does that read cleanly? Is it obvious what comes back, and what happens when the lot is full? You're designing the API by consuming it, which is the only perspective that matters — an API is judged by the people who call it, not by the person who wrote it. This is the heart of API-first (sometimes called consumer-driven or outside-in) design.
Write the usage code first
Before implementing anything, write three or four lines of example client code that call the method as you wish it existed. If those lines are awkward to write, the signature is wrong — fix it now, while it costs nothing.
What a good signature communicates
A method signature is a tiny contract that should answer four questions at a glance: what does it do (the name), what does it need (the parameters), what does it give back (the return type), and how can it fail (the error style). A signature like boolean doIt(int a, int b, boolean flag) answers none of them — you have to read the body to learn anything. Result<Ticket> park(Vehicle vehicle) answers all four: it parks a vehicle, it needs a vehicle, it returns a ticket, and the Result wrapper says it can fail in a way you must handle. Good signatures are intention-revealing: the name and types tell the story so the body doesn't have to.
The decisions you surface early
Designing the API first forces a handful of decisions into the open before they harden into accidents:
- Return type —
void(the caller learns nothing), a rawTicket(fine on the happy path, but what about failure?), or aResult<Ticket>/Optional<Ticket>that makes "might not produce one" explicit in the type. - Error-handling style — throw an exception, return null, return a
Result/Optional, or return an error code. Pick one deliberately and be consistent; mixing them is what makes a surface miserable to call. - Naming — vague verbs like
process(),handle(),doIt()hide intent;park(vehicle),cancelBooking(id),reserveSeat(...)reveal it. - Parameter shape — a long positional list (
park(plate, color, height, isElectric, level)) is easy to mis-order; a single parameter object (park(ParkRequest)) is self-documenting and grows without breaking callers. - Command–query separation — a method should either do something (a command, often
voidor returning a receipt) or answer something (a query, with no side effects), not quietly both. - Idempotency — can the caller safely retry
parkafter a timeout without double-parking? Designing the contract is where you decide and document that. - Minimal surface — expose the fewest public members that let callers do their job; every extra public method is a promise you must keep forever.
Keep the surface minimal — hide the internals
The public API is a promise: once a caller depends on a method, you can't change it freely without breaking them. So the contract should expose intent and hide mechanism — this is encapsulation and abstraction in action. A clean park(vehicle) says nothing about how spots are tracked, whether there's a database, or what an int spotIndex means internally. The moment a signature leaks HashMap<String,Integer> spots or a Connection conn parameter, the caller is coupled to your implementation, and you've lost the freedom to change it. A deep module — a small interface over a lot of hidden functionality — is the goal; a shallow one that exposes its guts is a liability.
Don't leak implementation or persistence concerns
Never let database rows, ORM entities, connection objects, internal indices, or framework types appear in your public signatures. save(Connection c, Row r) welds every caller to your storage choice. The contract should speak the domain's language (book(request): Result<Ticket>), not the wiring's.
Contracts let implementations be swapped (DIP)
When the API is an interface rather than a concrete class, you gain the ability to swap what's behind it. interface ParkingLot { park(v): Result<Ticket>; } can be backed by an in-memory lot today and a distributed one tomorrow, and no caller changes. High-level code depends on the abstraction, not the concrete type — that's the Dependency Inversion Principle. Designing the API first naturally produces these thin contracts, because you're describing what the caller needs, not how you'll provide it.
Why agreeing first unblocks everyone
Once the contract is agreed, work parallelizes. One engineer builds the real implementation behind the interface; another builds the caller against a mock of that same interface; a third writes tests that assert the contract's behavior. None of them wait on each other, because the interface is the shared truth. Testing gets easier for the same reason: you mock against the interface, verify the caller handles every documented outcome (including the failure cases the Result type forces it to acknowledge), and you're done. A contract designed first is a coordination point; an API that fell out of the implementation is a surprise everyone discovers late.
Leaky vs. clean: a contrast
Here's the same operation as a leaky contract and a clean one. The leaky version returns a bare boolean, smuggles results out through an out-parameter, takes an error-prone positional list, and exposes an internal index. The clean version is an intention-revealing interface returning an explicit result type over a parameter object:
// ❌ LEAKY — boolean return, out-param, positional soup, exposed internals
class Lot {
// did it work? caller can't tell why it failed.
park(plate: string, color: string, h: number, ev: boolean,
outSpotIndex: { value: number }): boolean { /* ... */ }
}
const out = { value: -1 };
if (!lot.park("AB-12", "red", 1.6, false, out)) { /* why?? */ }
// caller must know what 'outSpotIndex' means — that's an internal detail
// ✅ CLEAN — intention-revealing interface, result type, parameter object
interface ParkingLot {
park(vehicle: Vehicle): Result<Ticket>; // one obvious thing; failure is in the type
}
const r = lot.park(car);
if (r.ok) issue(r.value); // r.value is a Ticket — a domain object
else show(r.error.message); // the caller MUST handle the failure caseInteractive prototype
See it. Build it. Break it.
A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.
About this simulation
Pick the design decisions for a single park(vehicle) operation — its return type, error-handling style, parameter style, and method name — and watch the client code a caller would write re-render live on the right, alongside a caller-experience score with concrete pros and cons. Choose return null and it warns about forgotten null checks; choose a Result type and errors become explicit. Hit Snap to clean API to see the ideal contract, or Reset to start awkward again.
Hands-on
Try these yourself
Open the prototype above, predict what happens, then verify.
Flip the return type and read the client code
Start with the Return type selector on void. The Client code panel shows a call that throws away its result — the caller learns nothing. Switch it to Ticket, then to Result<Ticket>, and watch the snippet re-render each time. Notice how Result<Ticket> forces an if (r.ok) check into the caller's code, and the caller-experience score climbs as failure becomes visible in the type.
Make the contract leak, then watch the cons appear
Set Error handling to return null and read the live con in the experience panel: "caller can forget the null check → NPE risk." Now switch Parameter style to long param list and Naming to the vague doIt() — the score bar drops into the red and the console narrates each regression. This is exactly the leaky contract: easy to mis-call, impossible to read.
Snap to the clean API
Press Snap to clean API. Every selector jumps to its best choice — Result<Ticket> return, Result error handling, a parameter object, and the intention-revealing park(vehicle) name — and the Client code panel renders the ideal caller snippet. The score maxes out, the pros light up green, and you can see the leaky-vs-clean contrast at a glance. Hit Reset to return to the awkward starting contract.
In practice
When to use it — and what trips people up
When designing the API first pays off most
Reach for API-first design whenever a contract will be shared or outlive its first caller: a public library or SDK, a service consumed by another team, a module sitting at a subsystem boundary, or anything you'll mock heavily in tests. The cost of a bad signature scales with the number of callers depending on it — a published interface is expensive to change because you can't fix every caller in one commit. Spend the design effort up front exactly where the API will be hard to change later.
Published vs. merely public
There's a difference between public (anyone in your codebase can call it) and published (callers outside your codebase depend on it). A published interface is a contract you can't quietly refactor — every breaking change ripples out to people you can't see. Design those with the most care, and keep their surface as small as you honestly can.
Don't over-engineer the contract either
API-first is not an excuse to invent interfaces for code with exactly one caller that'll never be swapped. Throwaway internal helpers don't need a hand-crafted contract. Design the API first where the surface is shared, stable, or hard to change — not on every private function.
What it gives you
- Callers come first — the contract is judged by the people who use it, so the public surface stays clean and intention-revealing.
- Parallel work unblocks — implementation, callers, and tests all proceed against the agreed interface at once.
- Easy testing — mock the interface, and the contract's documented outcomes (including failures) are simple to assert.
- Swappable implementations — an interface contract lets you change what's behind it (DIP) without touching callers.
- Failure modes are explicit — choosing a Result/Optional return makes 'this can fail' impossible for the caller to ignore.
Common mistakes
- Up-front effort — agreeing the contract before coding feels slower at the very start of a feature.
- Premature contracts can mislead — designing an API before you understand the problem can lock in the wrong shape.
- Over-abstraction risk — wrapping single-caller internal code in interfaces adds indirection for no gain.
- A published API is a promise — once callers depend on it, breaking changes are costly, so the surface must be chosen carefully.
- Result/error-style consistency takes discipline — mixing exceptions, nulls, and result types across one surface confuses callers.
Reference
Code & further reading
A minimal reference implementation and pointers worth bookmarking.
// 1) DESIGN THE CONTRACT FIRST — the interface a caller depends on.
// No implementation yet; just the shape of the socket.
interface Result<T> { ok: boolean; value?: T; error?: { code: string; message: string }; }
interface ParkRequest { vehicle: Vehicle; preferEv?: boolean; } // parameter object
interface Ticket { id: string; spot: string; issuedAt: number; }
interface ParkingLot {
park(request: ParkRequest): Result<Ticket>; // intention-revealing, failure in the type
release(ticketId: string): Result<void>;
isFull(): boolean; // a pure query — no side effects
}
// 2) WRITE THE CLIENT CODE YOU WISH YOU COULD CALL — against the interface only.
function handlePark(lot: ParkingLot, car: Vehicle) {
const r = lot.park({ vehicle: car });
if (r.ok) return r.value!; // a Ticket — a domain object
console.warn("could not park:", r.error!.message); // failure is impossible to ignore
}
// Implementations (in-memory, distributed, mock-for-tests) can be swapped behind ParkingLot.References & further reading
7 sources- Paperresearch.google.com
Joshua Bloch — "How to Design a Good API and Why it Matters" (paper)
The canonical OOPSLA paper: design from the caller's side, keep the surface minimal, names matter, when in doubt leave it out. The foundation of this whole lesson.
- Talkinfoq.com
Joshua Bloch — API design talk (InfoQ)
The talk version of the paper, with the famous maxims spelled out — the same ideas if you'd rather watch/read than skim a PDF.
- Book
Effective Java — Joshua Bloch (book)
Items on minimizing accessibility, designing method signatures, returning empties/Optionals over null, and favoring interfaces — the API-first principles applied chapter by chapter.
- Articlemartinfowler.com
Martin Fowler — "PublishedInterface" (bliki)
Why the line between public and published matters more than public vs. private: once callers outside your codebase depend on an API, you can't freely change it.
- Articlemartinfowler.com
Martin Fowler — "MinimalInterface" (bliki)
Keep the public surface as small as honestly possible — the counterpart to 'hide the internals' and 'when in doubt, leave it out.'
- Book
A Philosophy of Software Design — John Ousterhout (book)
The 'deep vs. shallow modules' argument: a great API is a small interface over a lot of hidden functionality — exactly the minimal-surface goal.
- Docsgoogle.aip.dev
Google API Improvement Proposals (aip.dev)
Google's house rules for designing APIs consistently — the web/RPC analogue of class-level API-first thinking, with concrete naming and contract conventions.
Knowledge check
Did it land?
Quick questions, answers revealed on submit. Sign in to save your best score.
question 01 / 05
What does "designing the API first" primarily mean?
question 02 / 05
What is the recommended first concrete step when designing an API?
question 03 / 05
Why is returning a Result<Ticket> or Optional<Ticket> often better than returning null on failure?
question 04 / 05
Which signature leaks implementation details into the public contract?
question 05 / 05
How does agreeing on the API (as an interface) first help a team work in parallel?
0/5 answered