Poutsma Principles Consulting & Training

Making Your API Feel Like Home

One of the most important properties of a good API is its level of consistency.

“Consistency” can mean many things in computer science, but in the context of API design, it typically refers to the consistency of user interfaces. In other words: once part of the API has been learned, how easily can the rest be guessed? Can users successfully combine known elements in new ways, or apply familiar patterns to unfamiliar elements?

xkcd on Consistency

Even within that definition, consistency applies to many aspects of an API:

  • Naming: do types, methods, and parameters that serve similar roles also have similar names?
  • Parameter order: are similar arguments presented in a consistent sequence?
  • Usage patterns: for example, are objects thread-safe, immutable, or do they follow a common lifecycle?

All of these contribute to the overall learnability and usability of your API.


Thinking about your API as a kind of user interface opens up useful comparisons.

For example, it reminded me of a blog post by Joel Spolsky—former CEO of Stack Overflow and, before that, an avid blogger—who wrote:

In most UI decisions, before you design anything from scratch, you absolutely have to look at what other popular programs are doing and emulate that as closely as possible. If you’re creating a document editing program of some sort, it better look an awful lot like Microsoft Word […]

That post was written in 2000, when desktop programs like Microsoft Word were the gold standard for rich user experiences. Today, that role has largely shifted to the Web. But the underlying idea still holds: be consistent with your ecosystem.


If you are designing a Java API, align with the conventions of the Java standard library. If your library builds on Spring, follow the way Spring does things. Borrowing from established APIs makes your own API feel familiar, predictable, and comfortable to use.

Even if you personally disagree with some design decisions—for instance, if you believe that prefixing all interfaces with an I is preferable—you are working in an ecosystem where millions of developers have internalized a different convention. That makes it effectively the right one.


The Broader Lesson

When designing an interface meant for human consumption—whether a graphical UI or a programming API—there is value in following established patterns. Copying popular idioms and styles helps users feel at home. They do not have to learn everything from scratch. Instead, they can transfer what they already know.

Fluent Internals

Part 4 of Crafting Fluent APIs

So far in this series on Crafting Fluent APIs, I have focused on the surface of the API. In this post, I want to show how that surface is designed, using RestClient as an example.

This is Part 4 of a series where I explore various design considerations behind fluent API design. You can find an overview of the series here.


When reading through the Javadoc for RestClient, you may notice a large number of internal types that support the fluent API:

Some of these types have intimidating signatures, such as:

interface RequestHeadersUriSpec<S extends RequestHeadersSpec<S>>
    extends UriSpec<S>, RequestHeadersSpec<S>

In the previous post, I argued that a fluent API’s surface is more important than the complexity of the internal types that support it. That trade-off becomes clear when browsing the Javadoc. Fluent APIs are designed to be used through Ctrl + Space, not read through documentation. (Though usage examples do help.)


Instead of Javadoc, it can be more helpful to visualize the fluent flow with a state diagram. Here is a simplified version for RestClient:

State Diagram for RestClient

This diagram highlights a few key points:

  • Creating a request that cannot have a body takes you to RequestHeadersUriSpec; if it can have a body, you go to RequestBodyUriSpec.
  • From either state, you can specify a URI, or skip it if a base URL is already configured.
  • You can repeatedly specify headers (and/or a body) until you call retrieve or exchange.
  • exchange ends the chain immediately, while retrieve gives you several response-handling options first.

Let us now return to that earlier type signature:

interface RequestHeadersUriSpec<S extends RequestHeadersSpec<S>>
    extends UriSpec<S>, RequestHeadersSpec<S>

This expresses that you can set both a URI and headers at this stage. The type parameter S is used as the return type for most methods in UriSpec and RequestHeadersSpec, like:

S uri(URI uri);
S header(String headerName, String... headerValues);

This is a form of self-referencing generic. It ensures that methods inherited from different interfaces consistently return the correct type—in this case, a subtype of RequestHeadersSpec. A similar pattern is used in Java itself, in the declaration of java.lang.Enum.


The Broader Lesson

The proof of an API is not in its documentation; it is in how it feels when a developer navigates it with Ctrl + Space.

Creating that feeling often requires complex internal types and generic gymnastics, but that complexity should always serve one purpose: a clean, intuitive, and discoverable surface.

Behind every fluent API that feels obvious is a careful type design that models allowed states and transitions. Done well, the result is something that feels simple—even when it is not.

Crafting Fluent APIs — Overview

Overview of the Crafting Fluent APIs series

Here is an overview of the posts in the Crafting Fluent APIs series, where I explore practical design techniques to make fluent APIs more expressive, usable, and developer-friendly.

Responding to Real Usage

Part 3 of Crafting Fluent APIs

I ended the last post in this series on Fluent APIs with:

ultimately, the real test is in how the API feels in code.

As an API author, it is difficult to predict exactly how developers will use your API until you see it used in practice. And because of the Change Event Horizon, you are no longer able to change much once it is released.

That is why it is so important to have an extensive milestone and release candidate phase: to give your users time to try new functionality and provide feedback. It also gives you a chance to observe how people actually use your API—whether by scanning GitHub, watching conference demos, or through other means.

This is Part 3 of a series entitled Crafting Fluent APIs, where I explore various design considerations behind fluent API design. You can find an overview of the series here.


Spring Framework’s reactive HTTP client, WebClient, was introduced in version 5.0 in 2017. Shortly after the first release candidate went out, the Spring I/O conference in Barcelona took place.

I was in the audience as my colleagues Mark Heckler and Josh Long gave a talk on Reactive Spring. In their presentation, they showed how to initialize a WebClient with a base URL:

WebClient client = WebClient.builder()
    .baseUrl("https://example.com")
    .build();

And then how to make a request to the base URL by passing an empty string to the uri method:

Mono<String> result = client.get()
    .uri("")
    .retrieve()
    .bodyToMono(String.class);

Watching this demo, I was immediately bothered. In our fluent API design, we had made the URI a required step—and here that decision forced users to pass an empty string just to proceed. This was not what fluency should feel like.

It reminded me of Steve Jobs motivating the original Macintosh team: by shaving off seconds from boot time, they could save dozens of lives.

We were not saving lives—but we could certainly save lines.

After the conference, we updated the API in the next release candidate to make the uri step optional. Now, with a base URL configured, you can write:

Mono<String> result = client.get()
    .retrieve()
    .bodyToMono(String.class);

Cleaner, simpler, and better aligned with real usage.


The Broader Lesson

Like any user interface, the only real test of a fluent API is watching people use it.

Developers will use your API in ways you did not expect. When that happens, your job is not to correct them; it is to learn from them, and adapt the design accordingly.

The resulting API might be more complex behind the scenes—involving more intermediate types or branching logic—but what matters is that the surface becomes simpler, more intuitive, and more aligned with real usage.

Fluent APIs Are More Than Just Chaining

Part 2 of Crafting Fluent APIs

A common characteristic of fluent APIs is the use of method chaining: allowing calls to be strung together in a single statement without storing intermediate results.

The simplest way to support chaining is to have each method return this.
However, doing so can leave the object in an inconsistent or invalid state—especially when methods are called out of order.

By narrowing or changing the return type at each step, fluent APIs can guide developers more carefully through valid usage flows, reducing the chance of missteps and improving overall clarity.

This is Part 2 of a series entitled Crafting Fluent APIs, where I explore various design considerations behind fluent API design. You can find an overview of the series here.


Let us consider a fluent API that composes an HTTP request, consisting of:

  • HTTP method
  • URL
  • headers
  • body

A basic version of such an API might simply return this from every method, allowing usage like this:

var request = Request
    .method("POST")
    .url("https://example.com")
    .header("Content-Type", "text/plain")
    .body("Hello World");

So far, so good.

However, this naive API would also allow the following:

var request = Request
    .body("Hello World")
    .url("https://example.com")
    .header("Content-Type", "text/plain")
    .url("https://example.net")
    .method("GET");

Besides the confusing order, this introduces a clear violation: a request with a GET method is not supposed to include a body. The API allows it, because everything is exposed at once.

The most common fix is to throw an IllegalStateException when a method is called in the wrong order or in an invalid context. But a more elegant approach is to design the API so that invalid states are impossible to reach in the first place.

RequestEntity

This principle is applied in Spring Framework 4.1’s RequestEntity API.

When you create a GET request using RequestEntity.get(...), your options are limited:

RequestEntity method options when using GET

When you create a POST request using RequestEntity.post(...), more options appear:

RequestEntity method options when using POST

In both cases, the shared headers methods are present—but in the POST case, body-specific methods like body(T), contentLength(long), and contentType(MediaType) are also available.

This behavior is not enforced by runtime checks — it is baked into the API design. The methods for GET return a HeadersBuilder, while those for POST return a BodyBuilder—pun intended, which extends HeadersBuilder.

By narrowing the return type at each step, the API forms a small DSL, guiding the developer toward correct usage and away from invalid combinations.

This approach makes it easier to do the right thing, which was my mantra when working on Spring.

Of course, not every HTTP service adheres to the spec. Some services may accept or even require a GET request with a body. To handle such cases, RequestEntity provides method(...), which bypasses the fluent DSL and exposes more flexibility.


The Broader Lesson

Method chaining alone does not make an API fluent.

A well-crafted fluent API avoids invalid states not by checking for them at runtime, but by using narrow return types that reflect the state of the interaction.

This requires thinking in terms of state transitions: What are the valid states at this point? What methods make sense in each state?

Sketching a state diagram during design can help, but ultimately, the real test is in how the API feels in code. And that feeling often comes down to what shows up when the developer presses Ctrl + Space.