# Open-sourcing Metals v2: Databricks’ Java and Scala language server for multi‑million line codebases

> Source: <https://www.databricks.com/blog/open-sourcing-metals-v2-databricks-java-and-scala-language-server-multi-million-line-codebases>
> Published: 2026-08-11 18:52:50.676029+00:00

How closing the code intelligence gap made Cursor work for our 26M-line Bazel monorepo

Most code at Databricks is now written by agents. For the moments engineers still go hands-on, they reach for lightweight editors that spin up quickly and let them navigate the code with little setup. For Scala and Java, though, IntelliJ has set the standard for years. At the scale of our monorepo, it was effectively the only editor that could keep up. This post shares how we built an alternative solution by extending Metals, the widely-used Scala language server, to have first-class Java support and scale for the size of our monorepo. In collaboration with the upstream Metals team, we have now open-sourced Metals v2 so that anyone with a large Java and Scala codebase can pair their coding agent with a lightweight editor.

Metals v2 is available today in Cursor, VS Code, and Neovim with installation instructions available on the [Metals website](https://metals-lsp.org).

In May 2025, we began standardizing Databricks’ day-to-day editor workflow around Cursor. Cursor and VS Code were already widely used at Databricks for frontend and other non-JVM work, and both had strong SSH remote support for our cloud-based development environment. However, most of our services are written in Scala and Java, and navigating them at the scale of our monorepo was the holdout; the problem we set out to solve.

Standardizing on one editor mattered beyond individual preferences. A unified IDE platform creates a flywheel: teams share one baseline for code exploration and development, while the platform team can concentrate investments in one place. Cursor became the primary editor for JVM work in our monorepo, and we consolidated enough that we did not renew the majority of our IntelliJ seats this year.

Three signals show how far the shift to Cursor went: overall IDE usage, Scala and Java file open events where Metals sits on the critical path, and adoption outside Databricks.

The broadest signal is overall IDE usage. Cursor adoption grew on its own at Databricks but stalled by September 2025, where it held for months. Growth resumed with the Metals v2 rollout and by July 2026, 92% of weekly active IDE users open Cursor compared to 12% for IntelliJ. Among engineers who only use a single IDE, 2.4k are now on Cursor compared to 120 for IntelliJ.

A closer signal than overall IDE usage is the ratio of files opened in each IDE for Scala and Java, the languages where Metals v2 sits on the critical path. Since the first major Metals v2 improvements landed in Cursor in October 2025, Cursor's share of Scala and Java file open events has risen from 40% to 78%. The climb is slower than aggregate IDE adoption because Scala and Java are where IntelliJ was most entrenched, but Cursor's share continues to trend upwards month-over-month.

Metals v2 started as a Databricks fork, but the goal was always to bring the work back to the open-source community. Together with the Cursor team and the core Metals maintainers at VirtusLab, we’re preparing a stable Metals v2 release to supersede the current stable v1 release.

We're contributing to Metals v2 to make Cursor work well for multi-million line Java codebases with a focus on improving Bazel support, debugging, and testing.—Kevin Niparko, Cursor

AI is changing how developers use IDEs, and Metals v2 moves in the right direction: fast startup, reliable codebase orientation, and an architecture built for large codebases. Databricks validated the approach at exceptional scale, and VirtusLab is excited to help bring this work to the broader Scala and JVM community.—Krzysztof Romanowski, Head of Development Productivity, VirtusLab

Conversations with other large JVM codebases have reinforced the same pattern we saw at Databricks: demand for Cursor, VS Code, and Neovim with strong SSH remote support, platform pressure toward unified tooling, and no credible path through existing JVM language servers at monorepo scale.

We began rolling out Metals V2 at Stripe less than a month ago, yet we're constantly impressed with how well it works in our Java codebase, the excitement from our engineers, and how delightful it is to work with and learn from the maintainers.—Mahib Hosain, Developer Platform, Stripe

This is the model we want for Metals v2: shared infrastructure for large JVM codebases, maintained in the open, and shaped by the companies that need it to work at scale. Ongoing development is led by VirtusLab, reach out to them with questions, feedback, or contributions via the [GitHub issue tracker](https://github.com/scalameta/metals) or [metals@virtuslab.com](mailto:metals@virtuslab.com).

Everything above is the case for Metals v2. The rest is for readers who want to better understand the engineering behind low-latency code intelligence across a 26M-line monorepo.

Because agents write most of the code, fast codebase orientation mattered more than comprehensive Language Server Protocol (LSP) completion and refactoring coverage. That narrowed the problem, but it did not make the solution obvious: we still had to deliver easy-to-setup, low-latency navigation for a Scala and Java Bazel monorepo this large, and off-the-shelf LSPs were not designed to support that scale. We had to build repository-scale code intelligence from first principles, and treat startup usefulness as a key metric we could measure and improve.

**Time-to-initial-intelligence (TTII)** measures how quickly the editor becomes useful after opening the repository. We start the clock when the language server activates and stop when the most important features are available, assuming no user intervention. These critical features include fuzzy searching workspace symbols, jump-to-definition, and finding symbol usages across the entire repository.

Metals v2 is a language server for Scala and Java. We started from Metals v1, the official Scala language server, but reaching our TTII target took more than tuning it around the edges. We forked it and reworked three central layers: the repo index, the Scala and Java compiler pipelines, and the build integration boundary.

The sections below walk through each layer in turn.

mbt stands for Metals Build Tool, and the mbt index is the main enabler of TTII or the “useful immediately” contract. It is a content-addressed index of workspace sources: Metals uses the command `git ls-files --stage`

to discover files and Git blob OIDs to decide which index entries can be reused. With repo-wide information available before build sync, Metals can answer “first mile” questions:

The mbt index is effectively a hash map from source file to a file-local summary. An entry records the file’s package declarations, definitions with source locations, and compact bloom filters for identifiers referenced in the file. The definitions power workspace symbol search and jump-to-definition while the bloom filters let Metals quickly rule out files that cannot contain a reference before doing more precise checks. Because each entry is derived only from one file, incremental updates stay straightforward: when a file changes, Metals recomputes that file’s entry and replaces it.

In our monorepo, the persisted mbt index weighs 936MB uncompressed and contains information about 2.9m symbols across over 142k Scala, Java, and Protobuf files. A clean benchmark build takes 22 seconds at full CPU utilization across 32 cores, while parsing a pre-built index from disk takes 5 seconds. In production, we measure TTII as the time to start the server, load a stale mbt index, update it against the latest `git ls-files --stage`

state, and restart the Scala and Java presentation compilers: **p50 8.7s, p90 36.7s**. Fuzzy symbol search across 2.9m workspace symbols is **p50 10ms, p90 95ms**. There is room to reduce TTII further, but at these numbers it is not the bottleneck we need to address next.

The Scala pipeline is built around the *presentation compiler*, a mode of the Scala type-checker that caches and reuses symbol table information across compilation runs. This reuse, combined with the compiler's lazy symbol resolution, lets a single instance keep the full 24M-line Scala codebase in scope while publishing diagnostics at **p50 0.9s, p90 8.9s**.

That single Scala compiler instance runs in one of two modes, depending on how much information Metals has from the build server about the file being edited. Before a build sync, a *fallback compiler* takes a permissive view, treating every source file in the repository as an eligible dependency candidate making navigation useful immediately, even across code that does not yet compile in Bazel. After a build sync, a *precise compiler* restricts itself to the classpath and sourcepath boundaries the build server reports, which is what makes its diagnostics and dependency information build-accurate. Both the precise and fallback modes lean on the same two techniques to keep a sourcepath this large tractable:

Precise mode adds a third technique: it keeps transitive dependency sources on the sourcepath, so edits across files from different Bazel targets are reflected immediately in the editor without waiting on Bazel to produce a new classpath, a constraint that held back Metals v1.

Holding a 24M-line codebase in a single presentation compiler instance is well outside what the Scala compiler was originally built for. Metals v2 achieves it through two compiler modes built on a shared set of techniques for scaling the sourcepath. Jump-to-definition, the editor's most-used feature, runs across the entire repository at **p50 7ms, p90 575ms**. Scala is the language most of our engineers work in, so this pipeline was the piece that had to work for Cursor to become a genuine alternative to IntelliJ in our monorepo.

Metals v2 implements the Java LSP surface directly on `javac`

APIs. We evaluated reusing an existing Java language server (JDT- and NetBeans-based implementations), but both are build-centric in the same way Metals v1 was, the exact coupling v2 set out to remove. Building on `javac`

instead let Java share Metals v2's sourcepath and build-sync model with Scala, which still makes up most of our monorepo, and the Java surface we needed was small enough to maintain ourselves.

The `javac`

APIs provided enough compiler access to implement diagnostics, navigation, semantic highlighting, and other key LSP methods, and they held up well on partially broken code, which matters for an interactive editor workflow. As with Scala, we deliberately kept active editing features such as refactorings and completions narrower in scope.

The main scalability issue with this architecture appeared in files that triggered pathological performance in the `javac`

"enter" phase by transitively importing millions of lines of code at the symbol outline layer. Metals v2 addresses this with a `javaSymbolLoader: "turbine-classpath"`

mode, enabled by default, which uses a modified version of the [Turbine](http://github.com/google/turbine) header compiler to produce a repository classpath that works in an IDE setting, including graceful handling of naming resolution errors. Turbine processes close to one million lines of Java code per second on a single thread, which means Metals can recompile the whole Java codebase on a regular interval. This keeps nearly all cross-file symbols on the classpath instead of the sourcepath, so the `javac`

"analyze" phase can run close to its practical limit for interactive use: almost 100k lines of code per second from our benchmarks.

“Useful before build sync” does not mean “ignore the build.” Metals still needs build-graph fidelity for a long tail of features including navigation to third-party dependencies or generated sources, respecting custom shading rules, discovering test suites and auto-configuring debug launchers. At Databricks, that means making metadata from 285k Bazel JVM targets queryable at editor latencies.

Our production implementation of this layer is an internal BSP server written in Go and tailored to our Bazel rules. This BSP server is not part of this open-source release, but its design choices are still worth carrying over to other Bazel BSP implementations.

First, Metals v2 never invokes Bazel via the BSP server unless the user explicitly asks it to. In a large monorepo, background IDE sync can take the Bazel lock and compete with developer-initiated builds, so build sync is an explicit user action rather than startup behavior or a background maintenance task. The resulting metadata is stored in a JSON snapshot that scales through constant pooling for repeated labels, paths, and repository prefixes. When users sync, the BSP server incrementally adds more targets to this snapshot and serves the updated metadata over BSP.

Second, there is no shared sync configuration format. Users sync individual files or directories on-demand as they are editing to improve navigation or diagnostic fidelity. In our experience, predefined sync sets grow over time, get copied between teams, and become slower than the focused sync the developer actually needs.

Together, the build-free index, the compiler-backed pipelines, and the metadata-first build integration deliver low-latency code intelligence in multi-million line Bazel codebases. By open-sourcing it under Apache 2.0, we want to give the broader ecosystem a choice that did not exist before: to pair a coding agent and a lightweight editor with rich Scala and Java navigation, at a scale where existing JVM language servers offered no credible path.

Subscribe to our blog and get the latest posts delivered to your inbox.
