Fluent Internals
Part 4 of Crafting Fluent APIs 03 Jun 2025So 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
:
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 toRequestBodyUriSpec
. - 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
orexchange
. exchange
ends the chain immediately, whileretrieve
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.