Skip to content

CONCEPT Cited by 1 source

Fluent interface API

Definition

A fluent interface (Fowler, 2005) is an API style in which each method returns an object (often this or a related builder) so that calls can be chained into a single expression that reads as a sentence. The chain accumulates configuration incrementally; a terminal call (sometimes implicit) produces the final value.

builder.withA(x).withB(y).withC(z)

Each withX returns a builder with X added; the final builder represents the whole configuration. The name is due to the call sequence reading like prose — "with A, with B, with C."

Mechanics

Two canonical shapes:

  • Mutating this — each method mutates internal state and returns this. Simple; not thread-safe; each withX call permanently changes the builder.
  • Immutable builder — each method returns a new builder with the added configuration; the original is unchanged. Thread-safe; enables branching (b1 = base.withA(x); b2 = base.withB(y)b1 and b2 are independent); typical in functional or strongly-typed codebases.

With TypeScript, an immutable fluent interface can additionally carry types through the chain — each withX narrows the return type so the terminal call's argument is typed by what has been configured so far. This is how Zalando's tile() API types the props argument of withRender based on the shape returned from withProcessDependencies and the queries declared in withQueries (Source: sources/2021-09-08-zalando-micro-frontends-from-fragments-to-renderers-part-2).

Canonical Zalando instance

The Rendering Engine's Renderer API is a fluent interface:

export default tile()
  .withQueries(({ entity: { id } }) => ({ ... }))
  .withProcessDependencies(({ data }) => ({ action: "render", data, tiles: ... }))
  .withRender((props) => <ViewTracker>...</ViewTracker>);

The post frames this explicitly:

"Renderers are implemented using the fluent interface approach. By calling the tile() function of the Rendering Engine API, we are setting up a Renderer that defines various lifecycle methods."

See concepts/declarative-lifecycle-api for the orthogonal property — what the chain configures (named lifecycle phases run by the framework, not by the author) — which is what makes this fluent interface more than syntactic sugar.

Why teams choose it

  • Reads as prose. The configured object's intent is legible from the call chain.
  • Discoverable via IDE autocomplete. Each . opens a list of the next valid builder methods; for typed languages, earlier calls narrow the autocomplete set.
  • Type-driven correctness. With TypeScript, the chain can enforce that withRender runs only after withQueries + withProcessDependencies, and that props is typed correctly given what came before.
  • Single expression per unit. A Renderer, a policy, a query — each is one chained expression, easy to copy/paste, easy to diff, no scattered this.config.x = assignments.

Contrast with alternatives

  • Constructor with options bagnew Renderer({ queries, processDependencies, render }). Readable but all fields at once; no per-phase narrowing; auto-complete less helpful.
  • Decorator/annotation style (Java, Python) — declarative but language-dependent and harder to type incrementally.
  • Setters on a mutable objectr = new Renderer(); r.setQueries(...); r.setProcess(...); r.setRender(...);. Equivalent semantics, worse readability, no terminal operation implied.

When not to use

  • Configuration order matters and must be flexible. If withA can come before or after withB interchangeably, a fluent chain imposes an artificial sequence.
  • Configuration is dynamic. If fields are computed in loops or conditionals, chained calls become clumsy (if (x) builder = builder.withA(x)).
  • Many independent axes. Fluent chains get long fast; beyond ~7 calls the prose reading advantage disappears.

Seen in

Last updated · 550 distilled / 1,221 read