skip to content

Chained Builders

A Gatling test is a typed, compiled method chain, not an XML element tree and not a loose script. Interviewers probe it because which builder you chain decides what a virtual user does.

on this pageshow

explore

questions

26

In Gatling, how do you assemble a browse-then-checkout scenario out of two reusable chain values?

level: juniorimportance: must knowfreq 72%

answer

  1. Flows are values, not statements
  2. Two builder types, one shared vocabulary
  3. scenario names it, exec composes it
  4. Chains attach sequentially in the order given

basics

~20 s

Store each flow in a chain value returned by a standalone exec call, then hand both to scenario("Shopper").exec(browse, checkout). Chains are ordinary values, so one scenario composes as many of them as you like, in order.

solid answer

~40 s

Gatling gives you two builder types. `scenario("Shopper")` returns a `ScenarioBuilder` — a named chain that can be injected with a population. A bare `exec(...)` returns a `ChainBuilder` — a detached fragment with no name, which you can store in a constant, pass around and reuse. Both expose the same DSL methods, so you build `browse` and `checkout` as `ChainBuilder` values and then write `scenario("Shopper").exec(browse, checkout)`; the chains are attached sequentially in the order given. Nothing executes where you write it — these calls only build definitions, and only the tree that reaches `setUp` ever runs. The Java, Kotlin, JavaScript and Scala spellings of `scenario` and `exec` are identical.

code

java · 20 lines
java
import io.gatling.javaapi.core.*;

import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;

public class BrowseThenCheckoutSimulation extends Simulation {

  private static final ChainBuilder BROWSE =
      exec(http("Home").get("/"))
          .exec(http("Category").get("/books"));

  private static final ChainBuilder CHECKOUT =
      exec(http("Cart").get("/cart"))
          .exec(http("Place order").post("/orders"));

  {
    ScenarioBuilder shopper = scenario("Shopper").exec(BROWSE, CHECKOUT);
    setUp(shopper.injectOpen(atOnceUsers(1)));
  }
}

go deeper

for a junior

Be ready to name the two builder types and show, in code, a scenario built from two stored chains.

for a middle

Be ready to explain that both types share one builder API, and that a bare exec call produces a detached, reusable fragment rather than an action.

for a senior

Be ready to argue for naming flows as chain constants in a simulation others will maintain, and to spot dangling definitions that never reach setUp.

for a principal

Be ready to set the convention for how flows are named and shared across a team's simulations, and to say what that convention buys and costs.

Gatling simulations are not scripts that run top to bottom. A simulation is **assembled** from builder values, and only the tree of builders that ultimately reaches `setUp` is executed by the engine. Knowing which value each DSL call hands back is most of what scenario authoring is. ## Two builder types, one shared API | call | returns | can it be injected? | |---|---|---| | `scenario("Shopper")` | a `ScenarioBuilder` — a **named** chain of actions | yes | | `exec(http("Home").get("/"))` | a `ChainBuilder` — a **detached**, unnamed fragment | no | | `scenario("Shopper").exec(...)` | a new `ScenarioBuilder` | yes | | `browse.exec(...)` | a new `ChainBuilder` | no | Both types extend the same internal `StructureBuilder`, which is why they expose exactly the same vocabulary — `exec`, `group`, the loop constructs, the conditional constructs. The only substantive differences are that a `ScenarioBuilder` carries a name and offers the entry point that turns it into a population, while a `ChainBuilder` is a fragment you keep in a variable. That symmetry is the point. A chain is a **value**, not a statement. You can declare it as a constant, return it from a method, put it in a list, and reuse the same instance from several scenarios in the same simulation. ## Building browse-then-checkout ```java public class BrowseThenCheckoutSimulation extends Simulation { private static final ChainBuilder BROWSE = exec(http("Home").get("/")) .exec(http("Category").get("/books")); private static final ChainBuilder CHECKOUT = exec(http("Cart").get("/cart")) .exec(http("Place order").post("/orders")); { setUp(scenario("Shopper").exec(BROWSE, CHECKOUT).injectOpen(atOnceUsers(1))); } } ``` Three things are worth noticing: 1. `BROWSE` and `CHECKOUT` are plain constants. They are built once, when the class is initialised, and nothing about them is specific to the scenario that later uses them. 2. `exec(BROWSE, CHECKOUT)` attaches the two chains **sequentially** — a virtual user runs everything in `BROWSE`, then everything in `CHECKOUT`. 3. No HTTP traffic happens where those lines are written. `http("Home").get("/")` is a definition; if you build one and never attach it to anything that reaches `setUp`, it is a dangling definition with no effect at all. ## The shapes of `exec` In the Java API, `exec` comes in three forms, and the same three exist on `ScenarioBuilder` and on `ChainBuilder`: - `exec(Function<Session, Session>)` — attach a step that manipulates the virtual user's session. - `exec(head, tail...)` — attach one or more executables. Both `ChainBuilder` and every action builder (an HTTP request builder, for example) implement the same `Executable` interface, so you can freely mix them: `exec(BROWSE, http("Ping").get("/ping"), CHECKOUT)`. - `exec(List<ChainBuilder>)` — attach a whole list at once, which is handy when the set of chains is computed rather than written out. There is also a bare, static `exec(...)` — the one that bootstraps a fresh `ChainBuilder` from nothing. That static form is what you call when you write `ChainBuilder browse = exec(...)`, and the instance form is what you call when you write `browse.exec(...)`. ## The same shape in the other SDKs The `scenario` and `exec` spellings are identical in Java, Kotlin, JavaScript, TypeScript and Scala; only the surrounding syntax differs (`val` versus `ChainBuilder`, semicolons or not). Scala uses the same names: ```scala private val browse = exec(http("Home").get("/")).exec(http("Category").get("/books")) private val checkout = exec(http("Cart").get("/cart")).exec(http("Place order").post("/orders")) private val shopper = scenario("Shopper").exec(browse, checkout) ``` The SDKs do diverge at the point where a scenario becomes a population — Scala has a single `inject`, while the Java API (which Kotlin and the JavaScript/TypeScript package sit on) has `injectOpen` and `injectClosed` — but that is registration, not composition. ## What the scenario name is for The string you pass to `scenario` identifies the population in the run's output. Gatling normalises it on the way in: carriage returns, newlines and tabs are each replaced by a space and the result is trimmed, so a name containing whitespace oddities is silently rewritten rather than rejected. ## One chain, several scenarios Because a chain is an immutable value, the same `BROWSE` constant can be attached to a shopper scenario, an admin scenario and a smoke scenario in the same simulation, and none of them affects the others. That is what makes a small library of named flows worth having: each flow is written once and reviewed once, and the scenarios that use it read as a list of business steps rather than as a wall of requests. It also means a chain can be produced by a method. A helper with the signature `static ChainBuilder search(String term)` is a perfectly ordinary way to parameterise a flow, because there is nothing stateful about the value it hands back — the helper builds a fresh chain per call and the caller owns it. ## Common mistakes - Writing `exec(...)` on its own line and expecting a request to be sent there. - Assuming a chain belongs to the first scenario that used it and cannot be shared. - Trying to inject a `ChainBuilder`: only a named `ScenarioBuilder` becomes a population. - Inlining every action into one enormous `scenario(...)` call instead of naming the flows — which costs readability long before it costs anything else.

  • In Gatling, what happens to a tab or a newline inside a scenario name?
    It is normalised, not rejected. `scenario(name)` replaces every carriage return, newline and tab with a single space and trims the result, so the name that reaches the output is a cleaned-up version of what you typed. Gatling's own scenario reference says a tab is forbidden; the implementation actually rewrites it.
  • In Gatling's Java DSL, can you attach a computed list of chains instead of naming each one?
    Yes. `exec` has an overload taking a `List<ChainBuilder>`, and the varargs overload accepts anything implementing `Executable` — which covers both chains and individual action builders. In every form the pieces are attached one after another, in list order.

A chain is a paragraph you keep in a folder; a scenario is the document you paste paragraphs into. Copying a paragraph into a second document does not remove it from the folder.

saying these in an interview costs you the question

  • Believing exec sends the request where the line is written
  • Assuming a chain belongs to only one scenario
  • Trying to inject a detached chain instead of a scenario
  • Thinking scenario() must list every action inline
open as a page

In a Gatling scenario, how do you repeat a chain a fixed number of times or for a fixed duration, and how do the Java and Scala spellings of those loops differ?

level: juniorimportance: must knowfreq 70%

basics

~10 s

repeat(n) runs the wrapped chain n times; during(d) runs it until d has elapsed. Java, Kotlin and JavaScript attach the body with .on(...), while Scala passes it in a second parameter list.

open as a page

In Gatling, what must a Java or Kotlin simulation declare before its first request — which class does it extend, and which imports put the DSL in scope?

level: juniorimportance: must knowfreq 68%

basics

~10 s

A Gatling simulation in Java or Kotlin extends io.gatling.javaapi.core.Simulation and needs two imports per DSL family: the package wildcard for the types, plus a static import of CoreDsl and HttpDsl for the DSL methods.

open as a page

Which npm packages does a Gatling HTTP simulation written in JavaScript or TypeScript depend on, and which of them provides the gatling command-line tool?

level: juniorimportance: must knowfreq 45%

basics

~10 s

Three: @gatling.io/core for the DSL and the simulation function, @gatling.io/http for the HTTP protocol builder, and @gatling.io/cli, a dev dependency that supplies the gatling command you invoke as npx gatling.

open as a page

In a Gatling scenario, how long does pause(10) wait, and what does pause(1, 4) do differently?

level: juniorimportance: must knowfreq 68%

basics

~20 s

pause(10) waits a fixed ten seconds: a bare number means seconds in every Gatling SDK. pause(1, 4) draws a fresh duration uniformly at random between one and four seconds each time a virtual user reaches that step.

open as a page

In Gatling, why does calling .exec(...) on a stored chain have no effect unless you keep the value it returns?

level: middleimportance: must knowfreq 60%

basics

~20 s

Gatling's builders are immutable. Every exec, group or loop call returns a brand-new builder and leaves the receiver untouched, so a call whose result you discard is a no-op. Keep the returned value, or chain in one expression.

open as a page

In a Gatling simulation file written in JavaScript or TypeScript, what must the module export, and where does the setUp function it calls come from?

level: middleimportance: must knowfreq 40%

basics

~20 s

The module's default export must be the value returned by simulation(...), imported from @gatling.io/core. That call takes one callback, and Gatling hands setUp into it as an argument — there is no class to extend and nothing to construct.

open as a page

In a Gatling scenario, what does pace(5) do that pause(5) does not, and which of the two holds a virtual user's request rate steady when the server slows down?

level: middleimportance: must knowfreq 60%

basics

~20 s

pace holds an iteration rate, pause does not. pause(5) always adds five seconds on top of the work; pace(5) waits only the remainder of a five-second window measured from the user's previous visit to that step.

open as a page

In a Gatling scenario, a token-refresh request intermittently returns 401 — how do you retry just that step, and what do the failed attempts do to the run's request statistics?

level: seniorimportance: must knowfreq 52%

basics

~20 s

Wrap the refresh step in tryMax(n): any failure inside restarts the wrapped chain, up to n attempts in total. Every attempt is recorded, so the failed ones still appear as KO requests in the run's statistics.

open as a page

In Gatling, how do you wrap several actions of a scenario in a named group, and how does the Java spelling differ from Scala's?

level: middleimportance: should knowfreq 40%

basics

~10 s

Java, Kotlin and JavaScript write group("Checkout").on(actions...); Scala writes group("Checkout")(actions...). Either way the wrapped actions are bracketed by start and end markers so Gatling records the block as its own unit, and groups can nest.

open as a page

In Gatling's Java DSL, what does exec(session -> ...) let you do, and what must never go inside that function?

level: middleimportance: should knowfreq 46%

basics

~20 s

It inserts a step that receives the virtual user's session and returns one, so you can set attributes between actions. It runs on Gatling's shared threads, so never block inside it, and SDK builders built there have no effect.

open as a page

In a Gatling scenario, how do doIf, doSwitch and randomSwitch each choose which chain to run, and what happens when none of them matches?

level: middleimportance: should knowfreq 48%

basics

~20 s

doIf runs its chain when a boolean condition holds, doSwitch matches a resolved key against case keys, and randomSwitch draws by percentage weight. When nothing matches the block is skipped, unless an OrElse variant supplies a fallback.

open as a page

In Gatling's scenario DSL, how does asLongAs differ from doWhile, and what do asLongAsDuring and doWhileDuring add?

level: middleimportance: should knowfreq 45%

basics

~20 s

asLongAs tests its condition before each iteration, so the body can run zero times; doWhile tests it after, so the body always runs at least once. The During variants add a maximum duration that also ends the loop.

open as a page

A Kotlin Gatling simulation fails to compile on `global().failedRequests().count().is(0L)` — what is wrong, and what are the two ways to write it?

level: middleimportance: should knowfreq 34%

basics

~20 s

Kotlin reserves is as a keyword, so it cannot appear as a bare method name. Escape it with backticks, or call Gatling's shouldBe alias. Kotlin's in has the same problem, aliased within on conditions and during on throttle steps.

open as a page

In Gatling, why does a Java or Kotlin simulation need two imports per DSL family while a Scala simulation needs only `import io.gatling.core.Predef._`?

level: middleimportance: should knowfreq 38%

basics

~20 s

Scala's Predef is one object that inherits every DSL method, aliases the types and carries the implicits, so one wildcard import covers all three. Java keeps types in a package and DSL methods as statics on CoreDsl, so each needs its own import.

open as a page

Running npx gatling run does not offer a TypeScript simulation you just added, so which folder and file-name pattern does the Gatling JavaScript CLI search, and how do you point it at a different folder?

level: middleimportance: should knowfreq 35%

basics

~10 s

By default the gatling CLI searches the src folder for files ending in .gatling.js or .gatling.ts, at that folder's root, each default-exporting a simulation. Point it elsewhere with the --sources-folder option.

open as a page

In a Gatling simulation, what does calling exponentialPauses() on setUp change about every pause(2) in the scenario, and how do you exempt a single one of them?

level: middleimportance: should knowfreq 38%

basics

~20 s

It turns the declared duration into a mean: every pause(2) becomes a draw from an exponential distribution averaging two seconds. To exempt one call, pass a pause type as its extra force argument, for example pause(2, constantPauses).

open as a page

A large Gatling simulation fails to compile with a StackOverflowError or a Method too large error - what causes each, and how do you fix them?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Both come from very long builder chains. StackOverflowError means the compiler ran out of stack, so raise its -Xss. Method too large means the Simulation constructor exceeded the JVM's per-method bytecode limit, so move chains into other classes.

open as a page

When the Gatling JavaScript CLI runs a TypeScript simulation, what is it executing the simulation on, and what does that rule out when you add an npm library to the project?

level: seniorimportance: should knowfreq 25%

basics

~20 s

Not your local Node process. The gatling CLI downloads a Gatling runtime bundle and caches it under a .gatling home directory, and runs your simulation there — so added libraries must avoid native binaries and Node-specific JavaScript APIs.

open as a page

In a Gatling scenario, what does rendezVous(100) do to the virtual users that reach it, and what happens when a user reaches it a second time?

level: seniorimportance: should knowfreq 30%

basics

~20 s

rendezVous(100) holds each arriving virtual user until a hundred are waiting, then releases all of them together. It then becomes a permanent pass-through, so every later arrival, including a user's second visit, goes straight through without waiting.

open as a page

In a Gatling simulation, how would you decide between retrying a failing step, dropping the affected virtual user, and stopping the whole run?

level: principalimportance: should knowfreq 38%

basics

~20 s

Match the block to the blast radius: tryMax retries one chain, exitHereIfFailed ends that virtual user's scenario, and stopLoadGenerator ends the run for everyone. Retry transient faults, drop users whose remaining work is meaningless, stop only when the run is void.

open as a page

In a Gatling loop such as during or asLongAs, what does the exitASAP argument change about when the loop stops?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

With exitASAP on, Gatling re-checks the loop condition before every action in the body, so a virtual user can leave part-way through an iteration. With it off, the iteration already in flight always finishes first.

open as a page

Gatling's reference tells you never to let an IDE optimize the imports of a simulation file — what can that break, and which packages does Gatling treat as its public API?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

Optimizing imports drops members no call site names: Scala's implicits on Predef, and Java statics that are read rather than called, such as CoreDsl.deploymentInfo. Gatling's public API is only the packages its import block names; everything else is private.

open as a page

In a Gatling scenario, a checkout step calls a payment provider you are not permitted to load test - how would you decide what to put in its place?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Gatling's dummy action stands in for a call you cannot make: a chosen response time, a success or failure outcome, and an optional session update. The judgement is which number you attribute and whether the report admits it.

open as a page

A Gatling suite is written in Java and maintained by a platform team while the product engineers write TypeScript, so how would you decide whether to move it onto Gatling's JavaScript and TypeScript SDK?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Decide on ownership, not syntax. The DSL vocabulary and the engine are the same, so the move buys maintainability by the product engineers and costs a rewrite, an npm toolchain, and an audit for constructs the JavaScript SDK does not have.

open as a page

Across a suite of Gatling simulations, would you declare the waiting policy on each pause call, on the injected population, or on setUp, and how would you decide?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Default to setUp, stated explicitly even when it matches the constant default, so one visible line governs the run. Drop to the population only when two user profiles differ; force one call only when that wait is a protocol requirement.

open as a page