An API here is the local kind: a function signature, an interface, a library’s exported surface. The same rules recursively govern HTTP APIs and RPC services, but the small-API craft is the foundation — most “distributed system is hard” pain is, on inspection, the local API being badly shaped first.
This topic is the design moves that make APIs survive their callers across years of caller churn.
Designing a Function Signature
A signature is a contract written once and read thousands of times. Three rules compound:
- Few positional parameters. Beyond three positional args, every callsite is wrong on the first try. Move to an options object for
n > 3or when any param is optional. - Required first, optional last. Surprising order is the most common call bug. Required → optional → defaults.
- Booleans are usually a smell.
setMode(true)reads as tax-form code;enableFeature()anddisableFeature()read as intent. Prefer enums or two functions when the boolean means a mode switch.
Bad: redirectTo(user, targetUrl, true, false) // what are 1, 0?
OK: redirectTo(user, { target: url, preserveHistory: true, force: false })
Best: redirectTo(user, { target: url }) // when defaults are right for 95% of calls
Return Shapes
A function returns either a value or a disposition (success or failure). The shape of the return signals to the caller how to handle it.
| Return shape | When it’s right | Failure mode |
|---|---|---|
Value or null | A truly absent case | null propagates; callers forget the check |
| Value or throw | Truly exceptional failure | Catch-all swallows in caller code |
Result<T, E> | Two expected outcomes | Some languages lack native support |
| Optional / nullable monad | The pipeline semantics | Forces downstream to acknowledge absence |
The rule that ages well: prefer Result/error over null over throw. The caller cannot ignore a Result — the compiler makes them handle both cases. null is forgettable. Exceptions are sometimes forgotten and sometimes too loud (the function aborts one caller’s unrelated test).
Naming That Scales
A name is the documentation that ships with the function. Three tests:
- The one-call test: from any callsite, does the function’s purpose mean alone?
fetchUser(id)should not need a comment. - The signal test: does the name disambiguate from siblings?
getUserandfindUsershould differ — one throws and one returnsnull? Name themrequireUserandfindUser. - The honest test:
validateXshould not also normalise X; if it does, rename tonormaliseX. Honest verbs enable honest naming.
The cost of a bad name is every future reader. Spend the extra minute.
The Pit of Success
An API is a pit of success if the easiest way to use it is the correct way. The opposite — a pit of failure — makes the wrong path shorter than the right one.
Pit of failure:
Map<String, Object> config = new HashMap<>();
config.put("timeout", 5000); // integer seconds or milliseconds?
config.put("secure", "yes"); // any string works, only "yes" means yes
Pit of success:
client.withTimeout(Duration.ofSeconds(5))
.withSecureFlag(true) // strong types, named methods
Three properties create the pit:
- Strong types — a misspelt config key is a compile error, not a silent runtime miss.
- Builder style — every method returns the type being configured, so completion guides the caller.
- Sensible defaults — the 95% case is one line; the 5% case is clearer than before.
Versioning Internals
Internal APIs do need versioning — any function called from more than one module is an internal API. The minimum:
- Semantic versioning at the package level: MAJOR.MINOR.PATCH. A breaking change is always a major bump; the major number is the compatibility contract.
- Deprecation cycle for any breaking change:
- Mark old symbol
@deprecatedwith a migration note pointing at the replacement. - Add the replacement alongside, enabled by default for new callers.
- After one release cycle, route remaining callers to the new symbol.
- After another release cycle, remove the old symbol.
- Mark old symbol
Skipping the cycle is the cause of every “we upgraded and everything broke” story.
Deprecation as a Process
Killing an API is harder than writing it. The process:
| Stage | Audience state | Developer cost |
|---|---|---|
| Announce | All current callers | Write the migration doc; ship the replacement |
| Coexist | New code uses replacement; old still works | Maintain both during transition |
| Warn | All callers see a deprecation log | Some will migrate; some will not |
| Forbid (if needed) | Old symbol raises or is gated | You now own the rollout cost |
| Remove | Old symbol gone | Most callers migrated; breakage audits the rest |
The grace period depends on your audience. An internal library can move in a quarter. A public open-source library may need years. The discipline is: never break silently, never break without a path, never break without a date.
Practice Trajectory
- Pick a function with
n > 3positional params; refactor to an options object. Compare callsites before and after. - Find any function returning
nullor nullable that callers forget to check; rewrite its return asResult<T, E>(or your language’s equivalent) and watch the compiler force handling. - Audit a boolean parameter on an exported function: replace with two named methods or an enum, and document which one was right for which callsite.
- Add a deprecation cycle to one symbol in your own code: announce in a tag, coexist in the next, warn in the next.
- Read an unfamiliar library API you use; identify one pit-of-failure in it. Propose — not in a PR, on paper — the smallest shape change that would turn it into a pit of success.
When It’s the Right Tool
| Situation | Takeaway |
|---|---|
| Signatures growing more optional params | Refactor to options object before positional order surprise bites |
| Functions returning nullable that callers ignore | Move to Result so the compiler enforces handling |
Naming indistinguishable siblings (get vs find) | Rename to express the disposition, not just the action |
| Breaking change being proposed | Schedule the deprecation cycle; never break silently |
| New API being designed | Make the right path the easy path: pit of success, not pit of failure |