There are many criteria through which an API can be characterized, six of which are introduced below, and which form the threads that we will cover in more depth throughout this refcard.
How often as an engineer have you downloaded a library as a Maven dependency, and then wondered which class gets you started with using the API? An API should not be considered successful if a developer cannot intuitively understand how to use it.
Developers should give consideration to the entry points into their API. Complete documentation is useful and helps developers understand the bigger picture, but ideally, we want to ensure minimal friction for the developer, and therefore we should provide developers with the minimal steps to get started at the very top of our documentation. From here, good APIs try to expose their functionality through this entry point, to hold the developer’s hand as they make use of the primary use cases enabled by the API. More advanced functionality can then be discovered through external documentation as necessary.
Because we expect others to use our APIs, we should put in the effort to document it. Our focus in this refcard is on high-quality, detailed JavaDoc content.
A good API should not surprise its users, and one way we can fail at this is by not being consistent. When we speak of consistency, we mean ensuring that we repeat the same concepts in our API, rather than introduce different concepts in an adhoc fashion. Examples include:
- All of our methods should have the form
xyz(), but not both forms.
- If there are two methods (one a conveience overload of the other, e.g. one taking
Object... (that is, a var-args of
Object) and the other taking
Collection<? extends Object>), that overload should be available everywhere.
The point here is to establish a team-wide vocabulary and “cheat sheet” that we use to apply a veneer of consistency across our entire SDK.
4. Fit for purpose
In developing an API, we must ensure that we target it at the right level for the intended user. This can be thought of in two ways:
- Do only one thing, and do it right.
- Understand your user and their goals.
A good example is the Collections APIs included with the JDK. The user should be able to store and retrieve items from a collection very easily, without first defining a reallocation threshold, configuring a factory, specifying a growth policy, a hash collision policy, a comparison policy, a load factor, a caching policy, and so on. Developers should never be expected to know the inner working of a collections framework to benefit from its functionality.
Creating a new API can happen almost too quickly, but we should remember that for every new API, we are potentially committing to a lifetime of support.
The actual cost of our API decisions depends a lot on our project and its community - some projects are happy to make breaking changes constantly, whereas others (such as the JDK itself) are eager to break as little as possible. Most projects fall in the middle ground, adopting a semantic versioning approach that carefully deprecates an API before removing it only in major releases.
Some projects even go so far as to include various markers to indicate experimental, beta, or preview functionality that makes it available in releases for feedback before a final API is locked down. A common approach is to introduce this new, experimental functionality with the
@Deprecated annotation, and to then remove the annotation when the API is considered ready.
For every API decision we make, we are potentially backing ourselves into another corner. As we make API decisions, we must take the time to view them in the wider context of the future releases of the SDK.