Poutsma Principles Consulting & Training

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.

Designing for Ctrl + Space

Part 1 of Crafting Fluent APIs

Though undoubtedly there are still vi and Notepad users out there, code completion has become the primary way most developers interact with your API. As such, it is worth considering what they see when they hit Ctrl + Space in their IDE of choice.

APIs with a clean, minimal completion surface are easier to use and harder to misuse—especially when designed fluently, using method chaining and step-by-step invocation.

This is Part 1 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.


Most developers, when exploring an unfamiliar API, do not start with the documentation. They type an object name, hit Ctrl + Space, and scan the list of suggestions. That list becomes both documentation and user guide.

Each additional option in that list increases cognitive load. The more methods a developer has to scan and distinguish between, the harder it becomes to discover the right one. A key goal in API design, then, is to reduce the number of visible options at any given moment.

When a developer cannot see the forest for the trees, the API is too noisy. Ideally, the completion menu should contain only a few relevant choices at each step.

RestTemplate

Take the RestTemplate, introduced in Spring Framework 3. It follows the same template pattern as JdbcTemplate, JmsTemplate, and others.

Like those, RestTemplate exposes a flat surface—dozens of similar-looking methods: convenience methods per HTTP method (getForObject, postForEntity, etc.), as well as generic methods (exchange and execute) for more flexibility. Moreover, each method has three overloaded variants to be able to offer URL template variables as varargs, as a Map, or to offer the URL as a java.net.URI.

All of these appear at once, creating an overwhelming set of choices.

Method Options for RestTemplate

To be fair, RestTemplate did not start with so many methods. Over time, more were added out of necessity (like patchForObject), but each new method added surface area and complexity.

Eventually, we made the decision to stop adding features to RestTemplate. Every addition meant another overload, and another burden for the user.

WebClient and RestClient

With Spring Framework 5 and the new reactive stack, we had the opportunity to apply what we had learned. WebClient was born, and later, RestClient for the non-reactive use case.

A key design goal was to limit the number of options offered at each step, using method chaining and narrower return types to guide the developer. The implementation behind this — particularly how return types control visibility — will be covered in a future post.

What used to be a single overloaded method call in RestTemplate became a chain of calls in RestClient, each handling one concern in the HTTP exchange.

First, choose the HTTP method:

Choosing a HTTP method with RestClient

Then specify the URI. As with RestTemplate, multiple variants are supported.

Second Option for RestClient

Because the method is POST, the next step offers options for setting the request body. If this were GET, body-related methods would not appear.

Third Option for RestClient

Finally, choose how to handle the response:

Fourth Option for RestClient

Compare this to the earlier approach:

String response = 
    template.postForObject(         // method
        "https://example.com/post", // URI
        "Hello World",              // request object
        String.class);              // response type

Now, using RestClient:

String response = 
    client.post()                          // method
          .uri("https://example.com/post") // URI
          .body("Hello World")             // request object
          .retrieve()
          .body(String.class);             // response type

Each step surfaces only what is relevant at that stage—a much cleaner experience for the developer.


The Broader Lesson

The key difference between RestTemplate and RestClient lies not in what they do, but in how they guide the developer to do it.

Where RestTemplate exposes a wide, flat surface with dozens of methods and overloads, WebClient and RestClient breaks the interaction into smaller, focused steps—each with a limited set of choices. This narrowing of the completion surface at each stage reduces cognitive load and makes the API easier to explore through code completion alone.

Fluent API design is not just about chaining calls. It is about structuring the journey—helping the developer move through a series of decisions in the right order, with just the information they need, when they need it.

In modern development, code completion is the user’s first interface with your API. Designing that experience deliberately is what separates usable APIs from user-friendly APIs.

When Fixing a Mistake Takes Five Years: the HttpMethod enum

One of the differences between working on a library or framework and building an application is what Dan North has called the Change Event Horizon: the amount of time it takes to undo a mistake.

In an internally deployed application, the horizon is short—you can fix a mistake in the next release. But in a library or framework, especially one as widely used as Spring, the horizon stretches much further. Mistakes can linger for years.

That is because every feature—no matter how flawed—is used in someone’s codebase. Fixing it means breaking backward compatibility. Unless the issue is critical, such as a security vulnerability, breaking user code is usually off the table.

Even when users agree with the change in principle, they might not be able to adopt it. The outdated API might be used in a third-party library they cannot change.

Maintaining backward compatibility—even across major versions—has been a key factor in Spring Framework’s long-term popularity. I will undoubtedly return to this topic in future posts. In this one, I want to highlight a specific mistake from Spring 5 that was quietly fixed in Spring 6—without most users ever noticing.


The HttpMethod Enumeration

Spring Framework 3.0 introduced the HttpMethod enum, listing HTTP request methods (GET, POST, DELETE, etc.). It was designed for use within ClientHttpRequest and ServerHttpRequest, part of Spring’s internal HTTP abstraction layer.

Over time, its usage expanded. First an abstraction over both client and server side was created with HttpRequest. Later, in version 5.0, it was reused for reactive support across both Netty and Servlet containers.

That latter expansion turned out to be a problem. Originally intended for RESTful, client-side usage, HttpMethod now had to represent all possible HTTP methods—including extensions like those used in WebDAV (LOCK, COPY etc.).

This mismatch came to light late in the release candidate phase of 5.0, when a bug was discovered: encountering an unknown HTTP method would throw an IllegalArgumentException. But it was too late for a clean fix.

The workaround was to introduce a getMethodValue() method that returned the HTTP method as a String. This allowed users to access the raw value, even if it was not part of the enum.

HttpMethod method = request.getMethod();
if (method != null) {
    // use enum
} else {
    String methodValue = request.getMethodValue();
    // fallback for unknown methods
}

This workaround worked—but it was ugly. It forced users to handle null values and duplicate logic. Worse, it made the enum effectively redundant.

Despite this, the enum could not be removed. Too much code relied on it. The usual option in this kind of situation is to introduce a new, better type—say, HttpMethodValue—and deprecate the old one. But then you end up maintaining two parallel APIs for years, leading to confusion and frustration.


The Fix in Spring 6

Spring 6 brought a rare opportunity: the transition from Java EE to Jakarta EE required users to recompile their code anyway. That gave us the chance to finally fix HttpMethod.

In milestone 1 of Spring 6, HttpMethod was changed from an enum to a class.

Because a Java enum is just a special kind of class, we could retain most of its behavior:

  • Constant fields for GET, POST, etc.
  • A valueOf factory method
  • A values() method
  • Private constructor
  • An implementation of Comparable and Serializable

With this change, getMethodValue() could finally be deprecated.

Most users probably never noticed. The API continued to work, and existing constants and methods were still available. Only in edge cases—like using HttpMethod in a switch statement—did source changes become necessary.


The Broader Lesson

If you are writing code that others depend on, backward compatibility matters.

Breaking changes—especially for cosmetic or subjective reasons—are a great way to frustrate your users and push them away. Keeping your API stable is a sign of respect for their time and trust.

When a mistake is made, the cleanest fix is often to introduce an improved alternative, deprecate the original, and wait. Sometimes, with enough patience (or the right major version bump), you can remove the mistake entirely.

In the case of HttpMethod, we were lucky. We fixed it without breaking most users. But chances like that are rare—and worth waiting for.