Fluent APIs Are More Than Just Chaining
Part 2 of Crafting Fluent APIs 20 May 2025A 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:
When you create a POST
request using RequestEntity.post(...)
, more options appear:
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.