skip to content

Internal DSLs

Making a host language read like domain notation through chained calls, nested scopes with an implicit receiver, and compile-time checks. Interviewers ask when a plain API would have been better.

on this pageshow

explore

questions

12

In a chained configuration API, what makes one call follow another, and what does the closing call add?

level: juniorimportance: must knowfreq 65%

answer

  1. one call feeds the next
  2. the return value is the thread
  3. state accumulates on the receiver
  4. the last call is a different kind
  5. closing call validates, then produces

basics

~20 s

Each configuring call returns a receiver carrying everything recorded so far, so the next call can be written straight onto it. The closing call is different: it validates the accumulated state and produces the finished result.

solid answer

~40 s

A chain works because every configuring step hands back an object the next step can be called on. That returned receiver carries the accumulated state forward, which is why the steps can be written as one expression instead of a sequence of statements against a named variable. The steps themselves usually only record data — a path here, a handler there. The last call is a different kind of call: it looks at everything recorded as a whole, checks it is coherent, and returns the finished result, whose type is normally *not* chainable. So the chain describes something and the closing call realises it; drop the closing call and you have typed a description nobody ever asked to be built.

code

pseudocode · 11 lines
pseudocode
table = newRoutingTable()
    .route("/orders")
    .method("GET")
    .handler(listOrders)
    .route("/orders")
    .method("POST")
    .handler(createOrder)
    .build()

// each configuring call returned a receiver
// build() returned the routing table itself

go deeper

for a junior

Recall the one rule: a configuring call hands back an object the next call is written on, and the last call turns the accumulated description into a result.

for a middle

Explain where the accumulated state lives, why the closing call is the only place a whole-configuration check can happen, and why its return type is deliberately not chainable.

for a senior

Show you can read an unfamiliar chain cold: find the origin, the closing call and any step whose meaning depends on an earlier one, and say what the design makes hard to diagnose.

for a principal

Weigh the readability win against the costs a team lives with: coarse failure reporting, invisible ordering rules, and half-built values that are legal to pass around.

## The rule that makes a chain possible A fluent API is an ordinary API plus one convention: **every configuring call returns an object that the next call can be written on**. Because each call gives a receiver back, the caller never has to name a variable between steps, and what would have been a paragraph of statements collapses into a single expression that reads close to a sentence. Two consequences follow immediately: - **The return value is the thread.** A step that returned nothing would end the chain there. The whole readability effect rests on what each step hands back, not on how the step is named. - **State has to accumulate somewhere.** Each step records something — a path, a request method, a handler. The object that comes back must carry everything recorded so far, or later steps would have nothing to attach to. That is the entire mechanism. Everything else in this style is a consequence of it. ## Two kinds of call in one chain Reading an unfamiliar chain is mostly a matter of separating the two kinds of call in it. | | Intermediate call | Closing call | |---|---|---| | What it returns | a receiver that can be chained further | the finished result, normally a different type | | What it does | records one piece of state | inspects the accumulated state and produces the result | | If it is omitted | that one piece is simply unset | usually nothing is produced at all | | How often it appears | many times, in a legal order | once, at the end | The closing call is the call that earns the design. Until it runs, the chain has only *described* something. It is also the only point at which the accumulated state is visible as a whole, which is why it is the natural home for checks no single step could make on its own: that a path was actually given, that a handler was attached to it, that two entries do not claim the same route. ## Reading a chain you did not write 1. **Find the origin.** Something produced the first receiver — a factory call, an empty collection of entries, a fresh configuration object. That is what the whole chain is accumulating into. 2. **Find the closing call.** It is normally the last call, and normally the only one whose name is a verb of completion rather than a noun of configuration. 3. **Classify the middle.** Each remaining call is one recorded fact. Their order may or may not matter — that is a property of the specific API, not of chaining. 4. **Ask what the chain's value is used for.** If the expression's result is assigned or passed on, the closing call is present and its result matters. If the whole expression stands alone as a statement, look hard: either a step had an effect of its own, or the chain does nothing. ## Where the accumulated state actually lives There are two common contracts, and the difference is invisible at the call site: - **Mutate and return the same receiver.** Each step changes one object and hands the very same object back. Cheap, and the chain reads identically. - **Return a fresh receiver.** Each step produces a new object carrying the old state plus one addition, leaving the previous one untouched. Both chain. They differ only once a partly built chain is stored in a variable and used more than once — which is exactly why an API that supports that use has to say which contract it offers. ## What the style costs Chaining is not free, and an interviewer usually wants to hear that you know the bill: - **A failure is reported against the whole expression.** One long expression gives a diagnostic less precise than a statement per step. - **Ordering rules become invisible.** If a step is only meaningful after another, nothing in a uniform chain says so. - **A half-finished chain is a perfectly legal value.** It can be stored, passed and returned, even though it represents nothing usable yet. - **Discoverability depends entirely on return types.** What may be written next is whatever the returned type offers, so a single broad receiver type offers everything everywhere — convenient to build, uninformative to read. The pay-off is the one everybody notices first: a reader who has never seen the API can often follow what a chain configures on the first pass, because the calls sit in the order the domain would state them.

  • Why is it useful that the closing call returns a type that cannot be chained further?
    It separates describing from having. A non-chainable result type tells the reader the chain is over, stops configuration calls being written after the state was already frozen, and lets the surrounding code demand that type — which in turn makes an omitted closing call visible rather than silent.
  • Does the order of the configuring calls in a chain matter?
    It depends on the API. If each step records an independent fact, order is free. If a step refines whatever the previous step opened, or overwrites a value an earlier step set, order is part of the contract — and a uniform chain gives the reader no signal either way, so it has to be documented or encoded in the return types.

saying these in an interview costs you the question

  • Thinks each chained call has already applied its change permanently
  • Says chaining is only about saving keystrokes
  • Believes the closing call is optional sugar
  • Claims the closing call returns the same chainable receiver
  • Assumes chaining requires the underlying object to be immutable
open as a page

In a nested policy notation built from blocks, what does it mean that each block runs against an implicit receiver?

level: juniorimportance: must knowfreq 58%

basics

~20 s

Each block is a piece of code the enclosing call evaluates against an object it supplies, so unqualified calls inside the block configure that object. Nesting blocks builds a tree: an inner block targets the node its enclosing call just created.

open as a page

When a host-language notation replaces a plain configuration API, what discoverability does a newcomer lose at the call site?

level: middleimportance: must knowfreq 55%

basics

~20 s

A plain API advertises itself: its members, signatures and defaults are all reachable from the call site. A notation replaces that enumerable list with vocabulary a newcomer must be told about, and has to publish it some other way.

open as a page

In a nested policy notation, why can a call inside an inner block silently configure the enclosing node instead?

level: middleimportance: must knowfreq 50%

basics

~20 s

Enclosing receivers stay in scope when an inner block opens, so a name the inner node cannot answer is looked up outward and answered by the enclosing node. The call is legal, nothing is misspelled, and the wrong node is configured.

open as a page

In a design review, which signals say a proposed domain notation will not repay its cost?

level: middleimportance: should knowfreq 47%

basics

~20 s

The notation does not repay when its authors are the people who wrote it, when its whole gain is punctuation an ordinary helper would remove, when its words are a one-for-one rename of existing members, or when it has begun growing its own control flow.

open as a page

A half-configured chain is stored in a variable and extended two different ways — what decides whether the two interfere?

level: middleimportance: should knowfreq 48%

basics

~20 s

What each step returns decides it. A step that mutates and returns the same receiver makes both branches one object, so they overwrite each other; a step that returns a fresh receiver leaves the stored one untouched and the branches independent.

open as a page

Why does misusing a host-language domain notation often produce a worse error than the same mistake against a plain API?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The host reports the failure in terms of the machinery that implements the notation, not the domain words the author wrote, and it points at the block rather than the offending line. Good messages have to be designed in.

open as a page

A configuration chain runs without error but nothing appears — the closing call was never written. How would you design that away?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Nothing catches it because a chain of calls is an ordinary expression whose value may be discarded. The fix is to make the incomplete chain a different type from the result the surrounding code needs, so only the closing call can produce what the caller must supply.

open as a page

A nested policy notation must reject calls that reach an outer scope from an inner block — how is that enforced?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Group the notation's scope types into one marked family. Inside a block, only the nearest receiver of that family stays implicitly available; an outer one must be named explicitly. Where the language cannot check that, disjoint member names per level are the fallback.

open as a page

At what point does a separately parsed external notation beat one embedded in the host language for a configuration surface many teams edit?

level: principalimportance: should knowfreq 36%

basics

~20 s

It wins once the authors cannot build the program, the text must change without a rebuild, several runtimes must read it, or the vocabulary must be strictly limited. Below those thresholds the embedded form is free-riding on a toolchain worth keeping.

open as a page

In a flat chain defining several entries in a row, what decides which entry a refinement step applies to?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

The receiver decides. Each step returns an object carrying which entry is currently open, and a refinement attaches to that one. Nothing in the source text says so, which is why the chain's return types have to carry the target for the reader.

open as a page

What can go wrong when a call inside a configuration block reads the node that same block is still filling?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

The receiver is a mutable accumulator, not a finished value, so a read mid-block sees only what the statements above it have added. The notation silently acquires ordering rules that its nested shape does not advertise.

open as a page