Java API Design
Purpose
Design surfaces that callers use correctly on the first attempt and that can evolve
without breaking them. Two failure modes: the API that leaks its implementation (callers
learn internals, every refactor becomes a breaking change), and the API frozen by fear
because nobody can classify which changes are safe. Every public member is a liability
accepted on behalf of unknown callers — publish deliberately, evolve deliberately.
Workflow
Before proposing code, inspect compiler release/toolchains, dependencies, CI/runtime versions,
the previous public API and supported consumers. No single authoring baseline is declared;
the references use Java SE 25, while the record snippets require Java 16+.
JPMS and enhanced deprecation require Java 9+, List.copyOf Java 10+, and record patterns
Java 21+ without preview. Adapt to the project's target; do not upgrade it or enable preview.
If release or consumer evidence is missing, state the gap and keep compatibility claims
conditional rather than declaring a safe minor release.
- Name from the caller's side. The vocabulary is the caller's domain (
settle,
authorise, refund), never the implementation (processData, handleRequest).
When choosing or reviewing names, read references/naming.md
for the heuristics and the false positives.
- Minimise the surface. Package-private is the default;
public is the exception
that needs a caller. In named modules, an unexported package is inaccessible to ordinary
external source access; classpath use, reflective access and explicit overrides need
separate review. Use exports for intended API packages only and identify the actual
supported surface before deciding a deprecation cycle is unnecessary.
- Shape the signatures. Parameter count is a signal, not a threshold. Boolean flags,
transposable same-typed arguments, recurring data clumps, optionality and independent
evolution often justify a parameter object or split method; a cohesive four-argument
operation may be clearer as-is. Constructor
ergonomics — when a plain constructor or record suffices, builders, staged
construction — are java-fluent-apis' territory.
- Check the overload set. Overloads must be interchangeable in behaviour, differing
only in accepted form. Never overload where boxing, widening or generics make
resolution surprising — different behaviour gets a different name.
- Classify every change to a published API as binary, source and behaviourally
compatible or not, using
references/compatibility.md, before choosing the
version number. For an end-to-end design-and-evolve pass, read
references/worked-example.md.
Rules
- Prefer positive boolean predicates (
isActive, hasCapacity, canSettle) and match the
published family/framework convention; records may naturally expose active(). A negative
concept can be legitimate when it is the domain state, but avoid forcing callers through
double negation.
- Collection-valued names are plural (
lineItems()), and collection returns are never
null—empty means empty. Also specify encounter order, mutability, snapshot/live-view semantics,
ownership and concurrency; List alone answers none of those.
- No abbreviations except those established in the caller's domain (
VAT, IBAN,
TTL); calcAmt saves four characters and costs every reader a guess.
- Discoverability is structural: each return type should offer the natural next call, so
the IDE's completion list reads as documentation. A method returning
String or Map
where a domain type exists throws that thread away.
- Accept the least-specific abstraction the operation needs and return the most-specific useful
contract, but do not expose an internal mutable collection.
List.copyOf creates an
unmodifiable shallow snapshot and rejects null elements; Collections.unmodifiableList is a
live read-only view. Choose and document one rather than calling both “immutable.”
- Keep
exports (compile/link access) distinct from opens (deep reflective access) in JPMS.
Framework reflection may require a qualified opens ... to ...; exporting a package merely to
make reflection work expands the caller API unnecessarily.
- Treat overloads accepting functional interfaces,
null, varargs, boxing or related generic
types as a source-compatibility hazard. Compile representative lambda/method-reference call sites
when adding one; existing binaries do not redo overload resolution.
- Document nullability, thread safety, blocking, ownership, idempotency and exception guarantees
where relevant. These are behavioural API surface even when Java's type system cannot encode
them. Cross-process wire compatibility remains rpc-and-api-contracts' responsibility.
- Deprecate with a migration:
@Deprecated(since = "...", forRemoval = true) when removal is
actually intended, plus a Javadoc @deprecated naming the replacement or explaining why no
direct substitute exists. Removal follows the published compatibility window—commonly a major
version—not merely the annotation.
- Semantic versioning is a compatibility claim, not a counter: behavioural breaks are
breaks — a stricter precondition on an existing method is a major version even though
every caller still compiles and links.
- Which exceptions a method throws is part of its contract — design that surface with
java-exception-design.
References
For a review, deliver the affected declaration and caller evidence, compatibility impact,
proposed adjustment and focused validation. For an implementation, compile representative
callers at the target release; for published changes also run old binaries and relevant
contract tests. Separate executed checks from proposed checks and unavailable consumer evidence.
- Naming — heuristics for method, boolean, collection and type
names, and the false positives (long names, domain jargon, family symmetry). Read when
choosing or challenging a name.
- Compatibility — the change-kind table: binary, source
and behavioural impact of each API change, with the JVM errors old clients actually
see. Read before shipping any change to a published type.
- Worked example — designing a small settlement API,
then evolving it one minor version without breaking callers. Read when doing either.
1---2name: java-api-design3description: Naming and API design for Java code that others call: names carrying domain vocabulary, method and boolean naming conventions, arity and parameter objects, overload hazards, discoverability, public versus internal surface (package-private, JPMS exports), and API evolution — binary, source and behavioural compatibility, deprecation, semantic versioning. Use when designing or reviewing a public type, when a signature has grown past three parameters, when adding a method, overload or record component to a published API, or when deciding what a module exports. Does not cover builder and fluent-chain mechanics (java-fluent-apis) or exception contracts (java-exception-design).4---56# Java API Design78## Purpose910Design surfaces that callers use correctly on the first attempt and that can evolve11without breaking them. Two failure modes: the API that leaks its implementation (callers12learn internals, every refactor becomes a breaking change), and the API frozen by fear13because nobody can classify which changes are safe. Every public member is a liability14accepted on behalf of unknown callers — publish deliberately, evolve deliberately.1516## Workflow1718Before proposing code, inspect compiler release/toolchains, dependencies, CI/runtime versions,19the previous public API and supported consumers. No single authoring baseline is declared;20the references use Java SE 25, while the record snippets require Java 16+.21JPMS and enhanced deprecation require Java 9+, `List.copyOf` Java 10+, and record patterns22Java 21+ without preview. Adapt to the project's target; do not upgrade it or enable preview.23If release or consumer evidence is missing, state the gap and keep compatibility claims24conditional rather than declaring a safe minor release.25261. **Name from the caller's side.** The vocabulary is the caller's domain (`settle`,27 `authorise`, `refund`), never the implementation (`processData`, `handleRequest`).28 When choosing or reviewing names, read [references/naming.md](references/naming.md)29 for the heuristics and the false positives.302. **Minimise the surface.** Package-private is the default; `public` is the exception31 that needs a caller. In named modules, an unexported package is inaccessible to ordinary32 external source access; classpath use, reflective access and explicit overrides need33 separate review. Use `exports` for intended API packages only and identify the actual34 supported surface before deciding a deprecation cycle is unnecessary.353. **Shape the signatures.** Parameter count is a signal, not a threshold. Boolean flags,36 transposable same-typed arguments, recurring data clumps, optionality and independent37 evolution often justify a parameter object or split method; a cohesive four-argument38 operation may be clearer as-is. Constructor39 ergonomics — when a plain constructor or record suffices, builders, staged40 construction — are java-fluent-apis' territory.414. **Check the overload set.** Overloads must be interchangeable in behaviour, differing42 only in accepted form. Never overload where boxing, widening or generics make43 resolution surprising — different behaviour gets a different name.445. **Classify every change to a published API** as binary, source and behaviourally45 compatible or not, using46 [references/compatibility.md](references/compatibility.md), before choosing the47 version number. For an end-to-end design-and-evolve pass, read48 [references/worked-example.md](references/worked-example.md).4950## Rules5152- Prefer positive boolean predicates (`isActive`, `hasCapacity`, `canSettle`) and match the53 published family/framework convention; records may naturally expose `active()`. A negative54 concept can be legitimate when it is the domain state, but avoid forcing callers through55 double negation.56- Collection-valued names are plural (`lineItems()`), and collection returns are never57 null—empty means empty. Also specify encounter order, mutability, snapshot/live-view semantics,58 ownership and concurrency; `List` alone answers none of those.59- No abbreviations except those established in the caller's domain (`VAT`, `IBAN`,60 `TTL`); `calcAmt` saves four characters and costs every reader a guess.61- Discoverability is structural: each return type should offer the natural next call, so62 the IDE's completion list reads as documentation. A method returning `String` or `Map`63 where a domain type exists throws that thread away.64- Accept the least-specific abstraction the operation needs and return the most-specific useful65 contract, but do not expose an internal mutable collection. `List.copyOf` creates an66 unmodifiable shallow snapshot and rejects null elements; `Collections.unmodifiableList` is a67 live read-only view. Choose and document one rather than calling both “immutable.”68- Keep `exports` (compile/link access) distinct from `opens` (deep reflective access) in JPMS.69 Framework reflection may require a qualified `opens ... to ...`; exporting a package merely to70 make reflection work expands the caller API unnecessarily.71- Treat overloads accepting functional interfaces, `null`, varargs, boxing or related generic72 types as a source-compatibility hazard. Compile representative lambda/method-reference call sites73 when adding one; existing binaries do not redo overload resolution.74- Document nullability, thread safety, blocking, ownership, idempotency and exception guarantees75 where relevant. These are behavioural API surface even when Java's type system cannot encode76 them. Cross-process wire compatibility remains rpc-and-api-contracts' responsibility.77- Deprecate with a migration: `@Deprecated(since = "...", forRemoval = true)` when removal is78 actually intended, plus a Javadoc `@deprecated` naming the replacement or explaining why no79 direct substitute exists. Removal follows the published compatibility window—commonly a major80 version—not merely the annotation.81- Semantic versioning is a compatibility claim, not a counter: behavioural breaks are82 breaks — a stricter precondition on an existing method is a major version even though83 every caller still compiles and links.84- Which exceptions a method throws is part of its contract — design that surface with85 java-exception-design.8687## References8889For a review, deliver the affected declaration and caller evidence, compatibility impact,90proposed adjustment and focused validation. For an implementation, compile representative91callers at the target release; for published changes also run old binaries and relevant92contract tests. Separate executed checks from proposed checks and unavailable consumer evidence.9394- [Naming](references/naming.md) — heuristics for method, boolean, collection and type95 names, and the false positives (long names, domain jargon, family symmetry). Read when96 choosing or challenging a name.97- [Compatibility](references/compatibility.md) — the change-kind table: binary, source98 and behavioural impact of each API change, with the JVM errors old clients actually99 see. Read before shipping any change to a published type.100- [Worked example](references/worked-example.md) — designing a small settlement API,101 then evolving it one minor version without breaking callers. Read when doing either.