skip to content

Step Constructs

The constructs a virtual user's behaviour is assembled from: actions, loops, conditional switches, failure guards and waits. Interviewers ask because each one wraps a chain, not a line.

on this pageshow

explore

questions

17

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 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 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

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

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

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

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