skip to content

In REST Assured, what does RequestSpecBuilder.build() return, and what happens when you call it twice?

level: middleimportance: should knowfreq 36%

answer

  1. no copy, no freeze, no clone
  2. same reference every build()
  3. later setters edit what you handed out
  4. one builder per template

basics

~20 s

build() returns the RequestSpecification the builder has been mutating all along, the same object every call, never a copy. Two specs built from one builder are one aliased spec, and later setter calls change a template already handed out.

solid answer

~40 s

`build()` is not a copy step. `RequestSpecBuilder` creates one `RequestSpecification` in its constructor and mutates that object as you call setters; `build()` simply returns it, the same reference every time. Two consequences bite real suites. First, calling `build()` twice on one builder yields two references to a single specification, so a rounds template and a crates template assembled from the same builder are one template carrying whichever `setBasePath` ran last. Second, a setter called after `build()` mutates a specification you have already handed out, which turns a later line of setup into action at a distance on tests that read fine in isolation. Give each template its own builder, or expose a factory method that constructs a fresh `RequestSpecBuilder` per call, and treat a built specification as read-only.

go deeper

for a junior

Remember that build() gives back the builder's own specification, not a copy. One builder makes one template, so construct a new builder whenever you want a different one.

for a middle

Explain the aliasing precisely: two build() calls return one reference, and a setter after build() edits a specification already in use. Be ready to show the two-basePath example and say what each variable ends up carrying.

for a senior

Catch it in review. Look for a stored builder, a repeated build(), or a setter after build(), and explain why the resulting failure looks like flaky ordering rather than a shared-object bug.

for a principal

Set the convention that keeps this impossible: factory methods returning built specifications, no builder ever stored in a field, and shared setup composed rather than re-edited. Argue it as a mutability boundary, not as a style rule.

## build() hands back the builder's own object `RequestSpecBuilder` does not accumulate instructions and assemble a specification at the end. It creates a `RequestSpecification` in its constructor and mutates that one object as you call setters; `build()` returns it. There is no copy, no freeze and no defensive clone. Two consequences follow, and both produce tests that read correctly and behave wrongly. ## Two templates from one builder are one template Because `build()` returns the same reference every time, a builder reused to make a second template does not make a second template: ```java RequestSpecBuilder b = new RequestSpecBuilder() .setBaseUri("https://api.milkround.test") .setContentType(ContentType.JSON); RequestSpecification rounds = b.setBasePath("/v2/rounds").build(); RequestSpecification crates = b.setBasePath("/v2/crates").build(); ``` `rounds` and `crates` are the same object, and `setBasePath` is a `setX` method, so the second call overwrote the first. Both variables carry `/v2/crates`. Every test that thought it was hitting the rounds endpoints is now hitting crates, and because the variable names say otherwise, the code review reads fine. ## A setter after build() reaches back in time The mirror image is worse, because it is action at a distance: - `build()` gives out a live reference, so any later setter call on the builder edits a specification other code is already holding. - The edit lands wherever that specification is used, including in tests declared far away from the line that made the change. - Nothing warns you. There is no "already built" state on the builder and no exception. - The failure appears only when the two pieces of setup run in a particular order, which makes it look like flakiness rather than a bug. The rule that follows: **treat a built specification as read-only, and treat its builder as finished.** ## The patterns that avoid both 1. **One builder per template.** The cheapest fix, and the one to reach for first — construct a fresh `new RequestSpecBuilder()` for each specification you intend to hand out. 2. **A factory method, not a shared field.** Expose `static RequestSpecification roundsSpec()` that builds and returns in one expression, so no caller can reach a half-configured builder. 3. **Compose instead of re-editing.** Build a narrow base template once, then build each variant with its own builder and fold the base in with `addRequestSpecification(baseSpec)`. 4. **Never store the builder.** A `RequestSpecBuilder` in a field is an invitation to call a setter on it later; a `RequestSpecification` in a field is not. ## What this looks like in a milk-round suite | Shape | What the suite gets | |---|---| | One builder, two `build()` calls | One specification aliased under two names | | One builder, setter after `build()` | A template that changes under tests already using it | | One builder per template | Independent specifications, order-insensitive | | Factory method per template | The same, plus no reachable builder to misuse | So the milk-round support class exposes `roundsSpec()`, `cratesSpec()` and `subscriptionsSpec()`, each constructing its own builder, each folding in a shared `depotSpec` through `addRequestSpecification`. Nothing in the suite holds a builder, and no specification can be edited after it is handed out. ## Reviewing spec-building code Three questions catch essentially all of this in review: - Is any `RequestSpecBuilder` stored in a variable that outlives the expression that built it? - Is `build()` called more than once on the same builder? - Does any setter call appear after a `build()` on that builder, anywhere in the file? A "no" to all three means every specification the suite hands out is fixed at the moment it was created, which is the property the rest of the suite quietly assumes. A "yes" to any of them is worth a minute of reading, because the symptom — a template that is not the template you thought you built — never points at the builder that caused it. ## Why the library works this way It helps to know that `RequestSpecBuilder` is a facade rather than a collector. It constructs the specification up front and every setter simply forwards to the matching method on that object, which is why the builder's methods line up so closely with the specification's own. Seen that way, `build()` returning the same reference stops being surprising: there was never a separate "unbuilt" state to copy out of. What that costs you is the guarantee a true builder normally gives — that the product is independent of the thing that made it — and getting that guarantee back is entirely on the calling code. One builder, one `build()`, one template, and the builder is unreachable afterwards.

  • How do you build two genuinely independent templates?
    Construct a separate `new RequestSpecBuilder()` for each one, ideally behind a factory method that builds and returns in a single expression so no caller can reach a half-configured builder. If they share setup, build the shared part once and fold it into each with `addRequestSpecification`.
  • What makes a setter called after build() so hard to spot in review?
    Nothing signals it: the builder has no built state, throws no exception, and the edit lands on a specification other code already holds. The failure only shows when both pieces of setup run in a particular order, so it reads as flakiness rather than as the aliasing bug it is.

saying these in an interview costs you the question

  • Assumes build() returns a fresh copy each time
  • Reuses one builder to produce several different templates
  • Expects a second build() call to throw or to freeze the spec
  • Keeps a RequestSpecBuilder in a field and edits it later
  • Thinks two variables holding one spec cannot diverge silently