Gatling simulations aren’t just about hammering your endpoints; they’re about building a dynamic model of your microservice architecture’s actual behavior under stress.

Let’s watch a simple scenario unfold. Imagine you have two services: user-service and order-service. A user requests their order history.

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

class UserOrderSimulation extends Simulation {

  val httpProtocol = http
    .baseUrl("http://localhost:8080") // Base URL for user-service
    .acceptHeader("application/json")

  val scn = scenario("User Order History")
    .exec(http("Get User ID")
      .get("/users/123") // Assume user ID 123 exists
      .check(jsonPath("$.id").saveAs("userId")))
    .pause(1.second) // Simulate thinking time
    .exec(http("Get User Orders")
      .get("/orders?userId=${userId}") // Use the captured userId
      .check(status.is(200))) // Assert the response is OK

  setUp(
    scn.inject(rampUsers(100) during (30.seconds)) // Ramp up to 100 users over 30 seconds
  ).protocols(httpProtocol)
}

This simulation first hits user-service to get a user ID, then uses that ID to request orders from order-service. The pause simulates user think time, making it more realistic than a constant barrage. The rampUsers injects load gradually, mimicking real-world user adoption.

Your microservice architecture is a complex organism. Gatling lets you map its circulatory system. The baseUrl is the entry point, but the real action is in the exec blocks, defining the API calls. Each exec is a transaction. The check statements are assertions – your service is returning what you expect. jsonPath and saveAs are crucial for chaining requests, like passing a user ID from one service to another. rampUsers and atOnceUsers are your levers for controlling the rate and volume of simulated traffic.

The true power emerges when you model inter-service dependencies. Consider a scenario where order-service also calls product-service to enrich order details.

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

class UserOrderProductSimulation extends Simulation {

  val httpProtocolUser = http
    .baseUrl("http://localhost:8080") // user-service
    .acceptHeader("application/json")

  val httpProtocolOrder = http
    .baseUrl("http://localhost:8081") // order-service
    .acceptHeader("application/json")

  val httpProtocolProduct = http
    .baseUrl("http://localhost:8082") // product-service
    .acceptHeader("application/json")

  val scn = scenario("User Order Product History")
    .exec(http("Get User")
      .get("/users/456")
      .check(jsonPath("$.id").saveAs("userId")))
    .pause(1.second)
    .exec(http("Get User Orders")
      .get("/orders?userId=${userId}")
      .check(jsonPath("$.orders[*].id").findAll.saveAs("orderIds"))) // Find all order IDs
    .pause(1.second)
    .foreach("${orderIds}", "orderId") { // Loop through each order ID
      exec(http("Get Order Details")
        .get("/orders/${orderId}/details")
        .check(status.is(200)))
    }

  setUp(
    scn.inject(atOnceUsers(50)) // 50 users all at once
  ).protocols(httpProtocolUser, httpProtocolOrder, httpProtocolProduct)
}

Here, we define separate httpProtocol instances for each service, allowing us to target different base URLs. The simulation now fetches orders, extracts all their IDs using findAll.saveAs, and then iterates through them with foreach to fetch details for each. This replicates a common pattern: a primary service calling downstream services based on the data it retrieves.

The most surprising thing about microservice load testing is how often the bottleneck isn’t the service you’re directly hitting, but a downstream dependency that’s silently failing under load. Gatling’s ability to trace these call chains and report latencies at each hop reveals these hidden weaknesses. You’re not just testing a single API; you’re testing the interaction graph of your services.

The next problem you’ll grapple with is managing dynamic data. If your simulation always fetches /users/123, it’s not a very robust test. You’ll want to explore Gatling feeders to inject varying user IDs, product IDs, or other data from CSV files or even other services.

Want structured learning?

Take the full Gatling course →