I Built a Java Version Manager by Fixing Other Tools' Open Bugs
There is no point in shipping another Java Version Manager unless it is best in class, so I mined the test suites and bug trackers of SDKMAN, jenv, mise, volta, and asdf.
Join the DZone community and get the full member experience.
Join For FreeEvery Java developer knows the ritual. A JAVA_HOME export in one profile file, a different one in another. sdk use java 21 in this terminal, but the other terminal is still on 8. The build passes in your shell and fails in the IDE because the IDE launched from the dock and never sourced your init line. A teammate's "works on my machine" that turns out to mean "works on my shell."
I got tired of it, so I built Jolta. It's Volta, but for Java. This article is partly about what it does, but mostly about how I built it, because the process is the part I'd recommend to anyone building in a crowded tool category: I mined my competitors' bug trackers and turned their backlogs into my test suite.
The Pitch, in a Paragraph
With Jolta, you never think about Java versions again (if you don't want to). brew install, jolta setup, done. Now cd into any project and java, javac, Maven, Gradle, your IDE's run button, git hooks, and CI scripts all use the right JDK for that directory, including on a fresh machine where the pinned JDK isn't installed yet (it fetches it on first use). There's no sdk use, no jenv add, no remembering to switch back. The pin is a plain .java-version file you commit, and it's authoritative everywhere a process can be launched from.
The Tech, Briefly
Jolta is a single static Rust binary whose shims are the resolver. Each shim (java, javac, jar, and the rest of the JDK toolset) is a symlink back to the binary. Every invocation walks up from its own working directory to the nearest .java-version, picks a JDK from anything on the machine (Jolta's own installs, Homebrew, /Library/Java, the JAVA_HOME_17_X64-style variables CI images set), exports JAVA_HOME, and execs the real tool. Overhead is about two milliseconds.
Two design consequences matter more than any feature list. First, there are no shell hooks and no per-shell state, so nothing can go stale. Resolution happens inside the process launch itself, which is why IDEs, cron jobs, and CI steps get the right JDK without any setup. Second, JAVA_HOME is set per-invocation by the shim, so Maven and Gradle daemons, which read JAVA_HOME directly, can't escape it.
That second point is an architectural difference, not a quality difference. SDKMAN is shell functions by construction: sdk use mutates the current interactive shell, and that model cannot follow a subprocess into a directory with a different pin. jenv shims per-invocation, but its JAVA_HOME comes from a shell plugin that runs at prompt time; keeping it truthful is its longest-running open issue (jenv #232).
The Interesting Part: Building a Test Suite From Other People's Backlogs
This is the part worth stealing.
Version managers are a mature category. volta, jenv, SDKMAN, mise, and asdf have collectively accumulated a decade of bug reports, each one a user hitting an edge case, already triaged and written up for free. So before writing much code, I mined them in three passes:
- Their test suites first. Whatever volta asserts in
tests/acceptance, whatever jenv checks in its bats files, whatever SDKMAN specs: all of it became conformance cases. If a competitor thought a behavior was worth pinning, it probably is. - Then their closed issues. Every fixed bug is an edge case that shipped to users at least once. Each one became a regression test before Jolta could exhibit it.
- Then their open issues. This is the fun part: bugs that are reported, confirmed, and still sitting in a backlog. I fixed them proactively, in a tool where they'd never been reported because they'd never shipped.
A few concrete examples of what that mining caught:
| Upstream issue | The bug | What Jolta does instead |
|---|---|---|
| mise #9679 | A wrong-architecture/wrong-libc JDK "installs" cleanly, then every run dies in the loader with a cryptic error | An exec probe at install time: the JVM must actually run before the install is promoted |
| mise #1887 | A bare GA release ("21") shadows newer point builds (21.0.x) in resolution | Numeric version keys; a major pin always resolves to the highest satisfying build |
| mise #6907 | An early-access build silently satisfies a GA pin (or vice versa) | EA and GA are gated: an -ea spec matches only EA, GA specs prefer GA |
| volta #1183 | A stray directory at a shim path silently blocks that shim forever | The shims directory is wholly owned and cleared entry-by-entry on every reshim |
| volta #2075 | Downloads aren't checksum-verified | Sidecar SHA-256 verification, including for offline mirrors |
| jenv #294 | Shimming a bundled runtime (GraalVM ships node) hijacks the user's other version managers |
Bundled language runtimes are deliberately never shimmed |
| jenv #232 | JAVA_HOME goes stale and Maven/Gradle bypass the manager |
Per-invocation JAVA_HOME from the shim, plus a doctor that names exactly what's shadowing what |
The result is a suite of 300+ regression tests, dozens of them pinned to issues that are still open in other tools' trackers. When someone asks what Jolta does that the others fundamentally can't, this is half the answer: it can't not have these bugs, because they're in CI.
The method generalizes. If you're building anything in a category with incumbents, their issue tracker is a prioritized specification of everything hard about the domain, written by your future users. Read it before you write your architecture.
If You Do Care What Java You're Running
Jolta is fully featured to let you customize your Java versions until your heart is content. Jolta downloads and manages eight distributions (Temurin, Corretto, GraalVM, Oracle, Zulu, Liberica, SapMachine, GraalVM CE) and recognizes twelve for pinning. You can set a preferred vendor: a Corretto shop that pins 11 gets Corretto 11.0.31 even when a higher Temurin build is installed, because vendor preference should beat build-number greed. Exact pins mean exact: 21.0.2 is never quietly satisfied by a neighboring build, and auto-install fetches that exact build. .sdkmanrc files work out of the box for teams migrating. There's an offline mirror mode for air-gapped CI, first-class Windows support (hard-link shims, PowerShell hook, no Developer Mode required), and a jolta doctor whose exit code is the verdict.
What It Doesn't Do
Jolta manages JDKs, period. SDKMAN also manages Maven, Gradle, and Kotlin; mise manages your whole polyglot toolchain. If you want one tool for node + python + java, use mise. It's good. Jolta's bet is narrower: that Java version switching should be correct from every entry point and invisible the rest of the time.
Try It
brew install OneAppPlatform/tap/jolta
jolta setup
The curl one-liner and Windows instructions are in the README.
In every category I could find, I tried to make this version manager best in class. Give it a try and tell me if I hit my mark.
Opinions expressed by DZone contributors are their own.
Comments