skip to content

Walk through the BSP request lifecycle when an IDE syncs a Gradle project — starting from build/initialize.

level: seniorimportance: should knowfreq 22%

answer

  1. initialize -> initialized -> buildTargets
  2. javacOptions carries the classpath
  3. taskStart/Progress/Finish notifications
  4. didChange enables incremental resync
  5. shutdown then exit

basics

~10 s

The IDE sends build/initialize to handshake capabilities, then build/initialized, then workspace/buildTargets to list modules, then buildTarget/sources and buildTarget/* for dependencies and classpath, and finally compile/test on demand.

solid answer

~50 s

A sync session is a JSON-RPC conversation with a defined lifecycle: 1. **`build/initialize`** — the client sends its `rootUri`, `displayName`, version, and a `capabilities` object (which languages it supports). The server replies with *its* capabilities: which `buildTarget/*` operations it can serve. 2. **`build/initialized`** — a notification; no further requests are valid until it is sent. 3. **`workspace/buildTargets`** — returns the list of **build targets** (Gradle source sets / subproject units), each with an id URI, tags, languages, and capability flags (`canCompile`, `canTest`, `canRun`). 4. Per-target discovery: **`buildTarget/sources`** (source roots), **`buildTarget/dependencySources`**, **`buildTarget/resources`**, and language-specific extensions like `buildTarget/javacOptions` / `buildTarget/scalacOptions` (which carry the classpath and output dirs). 5. On user action: **`buildTarget/compile`**, **`buildTarget/test`**, **`buildTarget/run`**, with progress streamed back as `build/taskStart`/`build/taskProgress`/`build/taskFinish` notifications. 6. Teardown: **`build/shutdown`** then **`build/exit`**. For Gradle the server fulfills these by invoking the Tooling API and translating its model into BSP responses.

code

bash · 10 lines
bash
// build/initialize request (JSON-RPC)
{
  "jsonrpc": "2.0", "id": 1, "method": "build/initialize",
  "params": {
    "displayName": "IntelliJ-BSP",
    "version": "1.0", "bspVersion": "2.1.0",
    "rootUri": "file:///home/me/proj",
    "capabilities": { "languageIds": ["java", "kotlin"] }
  }
}

go deeper

for a junior

Know that sync starts with a handshake and then the IDE asks for the list of modules.

for a middle

Name the main steps: initialize, initialized, buildTargets, then sources/dependencies, then compile/test.

for a senior

Walk the full ordered lifecycle, explain capability negotiation, where the classpath comes from, and the progress-notification stream.

for a principal

Reason about how the server caches the Gradle model across requests, incremental didChange resync, and protocol-version negotiation across a fleet of editors.

## The lifecycle, in order BSP defines a strict ordering so client and server stay in sync. Violating it (e.g. sending `workspace/buildTargets` before `build/initialized`) is a protocol error. ### 1. Initialization handshake `build/initialize` is the capability negotiation. The client passes: - `rootUri` — the workspace root (file URI). - `displayName`, `version`, `bspVersion`. - `capabilities.languageIds` — e.g. `["java", "kotlin"]`. The server responds with a `BuildServerCapabilities` object describing what it can do: `compileProvider`, `testProvider`, `runProvider`, `dependencySourcesProvider`, `resourcesProvider`, `buildTargetChangedProvider`, etc. The client then sends the `build/initialized` **notification**; only after that may it issue real requests. ### 2. Discovering structure `workspace/buildTargets` returns `BuildTarget[]`. Each `BuildTarget` carries: - `id` — a `BuildTargetIdentifier` (a URI, e.g. `file:///proj/?id=:app:main`). - `tags` — e.g. `library`, `test`, `application`. - `languageIds`. - `capabilities` — `canCompile`, `canTest`, `canRun`, `canDebug`. - `dependencies` — other targets it depends on. - `dataKind`/`data` — tool-specific extension payloads. The IDE then asks, per target: - `buildTarget/sources` -> source directories and individual files, each tagged as standard source or generated. - `buildTarget/dependencySources` -> `-sources.jar` locations for navigation. - `buildTarget/resources` -> resource roots. - `buildTarget/javacOptions` (or `scalacOptions`) -> compiler args, the **full classpath**, and the **class output directory** — this is how the IDE learns the resolved compile classpath. ### 3. Build/test/run When the user compiles or runs tests, the client issues `buildTarget/compile` / `buildTarget/test` / `buildTarget/run` with an `originId` for correlation. The server streams progress via notifications (`build/taskStart`, `build/taskProgress`, `build/taskFinish`) and reports diagnostics via `build/publishDiagnostics`. ### 4. Change notifications If capable, the server can push `buildTarget/didChange` notifications so the IDE re-syncs only affected targets instead of a full reimport — a key advantage over static file generation. ### 5. Shutdown `build/shutdown` (request) gracefully stops work; `build/exit` (notification) terminates the process. ## Why the ordering matters for Gradle A Gradle BSP server typically runs the configuration phase once via the Tooling API to build the model, caches it, and answers structure requests from that. Compile/test requests delegate to actual Gradle task execution. Honoring the handshake lets the server defer expensive configuration until after capabilities are agreed. ``` client -> build/initialize (capabilities) server <- capabilities client -> build/initialized (notification) client -> workspace/buildTargets client -> buildTarget/sources client -> buildTarget/javacOptions (classpath!) ... user clicks run ... client -> buildTarget/test server -> build/taskStart/Progress/Finish (notifications) ```

  • Which request reveals the resolved compile classpath to the IDE?
    Language-specific extensions: buildTarget/javacOptions (or scalacOptions). Their response includes the classpath entries and class output directory, which is how the IDE indexes dependencies for the target.
  • How does BSP support incremental resync rather than a full reimport?
    If the server advertises buildTargetChangedProvider, it pushes buildTarget/didChange notifications scoped to the affected targets, so the client re-queries only those instead of rebuilding the whole project model.
  • What happens if the client sends workspace/buildTargets before build/initialized?
    It is a protocol violation; the server should reject it. The handshake (initialize then initialized) must complete before structural requests are valid.

saying these in an interview costs you the question

  • Confusing build/initialize (request, capability negotiation) with build/initialized (notification, readiness signal).
  • Claiming the classpath comes from workspace/buildTargets — it comes from buildTarget/sources plus the language-specific options request.

context