Rust in the Enterprise: A Java Team’s Honest Assessment After 18 Months of Production Use
We are a Java shop. Have been for twelve years. We know Spring Boot, we know how to tune G1GC, we know our way around a JVM heap dump at 3am. When we decided to introduce Rust, we read every comparison article we could find. Most of them were wrong about what would actually happen.
1. Why We Looked at Rust in the First Place
The decision to evaluate Rust was not driven by hype. It was driven by two specific production problems that our Java services could not cleanly solve within our existing stack.
The first was a real-time data processing pipeline that ingested high-frequency market events — about 800,000 events per second at peak — and performed enrichment logic against an in-memory reference dataset. We had squeezed what we could out of the JVM: Generational ZGC, off-heap memory via Unsafe, careful object pooling. But GC pause variability at the P99.9 tail was still causing downstream issues that manifested as phantom order rejections. The business case for eliminating those pauses was quantifiable and real.
The second was a cryptographic verification service we were considering building from scratch. The verification logic was deterministic, CPU-bound, and needed to run in a highly constrained sidecar with strict memory limits. We knew we would spend six months tuning a Java service that a Rust equivalent might handle correctly on first build.
Neither of these was a case of “Rust is modern and Java feels old.” Both were specific problems where Rust’s properties — no GC, deterministic memory, near-C performance — mapped directly onto our constraints. That framing matters, because it is what saved us from a much broader and much more painful adoption story.
The teams that succeed with Rust in a Java enterprise adopt it surgically, for specific services where Java’s GC or memory model is a measurable constraint. The teams that struggle adopt it ambitiously, for broad categories of services where the problem is not actually the runtime. We nearly made the second mistake.
2. The Eighteen-Month Timeline
The honest account of what actually happened, month by month, is more useful than any benchmark.
Months 1–3: Evaluation and initial enthusiasm- The honeymoon phase — and the first wall
Two senior engineers began with the Rust Book, Rustlings exercises, and a small internal tool. Both had systems programming backgrounds (one had written C++ professionally). Progress was fast for the first three weeks. Then the first real service attempt — a simple HTTP endpoint with database access — produced a two-week engagement with the borrow checker over ownership of database connection handles. The code that would have taken two hours in Java took two weeks in Rust. This is not a complaint; it is information. The language is teaching you something. But the teaching is expensive at first.
Months 4–7: The productivity trough- Development velocity dropped 30–50% — as documented, not imagined
We shipped the first production Rust service in month six: the cryptographic verification sidecar. It worked correctly on deployment and has run in production without incident since. We were proud of it. We were also honest with ourselves that a comparable Java service would have shipped in month two. The borrow checker caught genuine bugs at compile time — three ownership errors that would have been data races in Java under concurrent load. But it also generated enormous friction for logic that was simply not safety-critical. Business rule validation code that Java developers write in an afternoon took our Rust engineers days.
Months 8–12: The second service — and the async reckoning- The event processing pipeline, and meeting Tokio
The market event processing pipeline — our original motivation — went into production in month ten. The results were what we hoped: P99.9 latency dropped from ~18ms to ~1.2ms. GC pauses disappeared because there is no GC. Memory footprint at peak load was approximately 40 MB versus 380 MB for the Java equivalent. These are real numbers from our production monitoring. But writing the service required learning async Rust, which is a significantly different challenge from synchronous Rust. Pinning, lifetimes across await points, and the distinction between async functions and async blocks took another steep learning curve that we had not budgeted for.
Months 13–18: Stabilization and an honest reckoning- What we would do differently
By month eighteen, we had three Rust services in production: the crypto sidecar, the event processor, and a high-throughput serialization library exposed to Java via the Foreign Function API. All three were the right decisions. We had also attempted and aborted a fourth service — a business rule engine — that we migrated back to Java at month fourteen. That experience provided the most useful data for our decision framework. The borrow checker is not the enemy. Choosing Rust for problems where the borrow checker’s cost-to-benefit ratio is wrong is the enemy.
Team Productivity and Outcomes Over 18 Months

3. JNI vs. the Foreign Function API: The Interop Reality
One of our early architectural questions was how to structure the boundary between the Java monolith and the new Rust services. We evaluated three options: pure microservices (Rust service over HTTP/gRPC, no shared process), JNI for in-process Rust calls, and the Foreign Function and Memory API (Project Panama, finalized in Java 22).
JNI: the original option, and why we moved on
The jni crate makes writing JNI bindings from Rust significantly less painful than raw JNI, but JNI itself remains fundamentally awkward. You must annotate Rust functions with the Java package path as the function name (for example, Java_com_example_MyClass_nativeMethod), manage the distinction between Java heap and native memory explicitly, and handle every Rust panic before it crosses the JNI boundary — an unhandled Rust panic across JNI causes a JVM abort with no useful stack trace. Bridging async Rust with Java’s threading model was particularly painful: JNI methods cannot be declared async, so calling async Rust code from Java requires building a completion-based bridge using CompletableFuture and blocking the Rust side until the future resolves.
The FFM API: the better answer
The Foreign Function and Memory API, finalized in Java 22 and present in Java 25 LTS, changed this significantly. It provides type-safe function descriptors, JIT-optimizable method handles, and arena-based off-heap memory management without the boilerplate of JNI. For our Rust library integration, the workflow using the FFM API is: compile Rust to a dynamic library, run jextract (the FFM binding generator) against the C header produced by cbindgen, and use the generated Java bindings directly. In benchmarks from Tweede Golf’s Rust interop research, Project Panama consistently outperformed JNR-FFI and matched JNI performance, while requiring significantly less boilerplate.
For new integrations on Java 22+: use the FFM API with
jextractfor binding generation andcbindgenon the Rust side to generate C headers. For performance-critical paths where the call overhead needs to approach zero: evaluate JNI. For operational simplicity at the cost of higher latency: put the Rust service behind gRPC and treat it as a separate microservice — this is the safest option for teams new to native interop.
4. Where Rust Genuinely Outperformed Java
Let us be specific. The wins were real, measurable, and repeatable. But they were concentrated in a narrow category of workloads.
| Service / Use Case | Java Result | Rust Result | Verdict |
|---|---|---|---|
| Event enrichment pipeline (800K events/s) | P99.9: ~18ms (GC pauses). RSS: ~380 MB | P99.9: ~1.2ms (no GC). RSS: ~40 MB | Rust wins decisively |
| Cryptographic verification sidecar | Feasible but required off-heap tuning, still ~8 MB per verification batch | ~0.8 MB per batch, zero pause. Zero runtime surprises in 14 months | Rust wins |
| Serialization hot path (FFM call from Java) | ~320 µs per batch at 1000 records | ~45 µs per batch (FFM call overhead included) | Rust wins on hot path |
| Business rule engine (domain logic) | Shipped in 3 weeks, full coverage, idiomatic | Abandoned at month 2.5 — returned to Java | Java wins |
| REST API with DB access | Spring Boot, 2 hours to first green test | Axum + SQLx, 3 days. Same functionality | Java wins on velocity |
| Long-running stability (18 months) | Two OOM incidents (resolved). Several GC-related timeout spikes | Zero production incidents on Rust services | Rust wins on reliability |
The reliability result is worth dwelling on. The Dynatrace Senior Product Architect quoted in Corrode’s production Rust analysis captured our experience precisely: “We always talk about the performance gains when using Rust but honestly I much more look for the stability gains.” Our Rust services have never paged anyone. They have never produced a heap dump. They have never required emergency memory tuning. The borrow checker’s up-front cost is, in a real sense, a down-payment on operational stability. The question is whether you are building a service that will reward that investment.
P99.9 Latency: Java (ZGC) vs. Rust — Event Processing Pipeline at Peak Load

5. Where the Borrow Checker Cost More Than It Saved
The borrow checker deserves an honest assessment from a Java team’s perspective, because the framing in most Rust advocacy is incomplete. It does not say: “the borrow checker is brilliant in some contexts and genuinely counterproductive in others.”
The contexts where the borrow checker cost more than it saved, in our experience, share a common property: the code’s correctness constraints are semantic rather than ownership-structural. Business rule validation, workflow orchestration, stateful saga logic, domain event processing — none of these have the properties that the borrow checker protects against. There are no shared mutable references to protect. There are no dangling pointer risks. The code’s correctness depends on business logic being right, not on memory safety invariants being upheld. The borrow checker cannot help with that and actively impedes it by demanding ownership ceremony for code that does not need it.
Java — business rule engine (3 weeks)
A Java developer can iterate freely on a domain model — add a field to a record, change a validation rule, restructure a pipeline — without considering ownership at each step. The GC handles the object lifecycle. The type system enforces interfaces. The test suite verifies correctness. The feedback loop from a code change to a red test is measured in seconds.
The iteration model is: change → compile (fast) → run test → iterate. A senior Java developer produces working, tested business logic at a sustained, high velocity.
Rust — same business rule engine (abandoned at 2.5 months)
The same domain model in Rust required constant ownership decisions for objects that were passed between validation stages. Using Arc<Mutex<T>> to share state removed the borrow checker errors but introduced lock contention that did not exist in the problem. Cloning structs avoided borrow issues but felt like giving up on the language’s promise. Lifetime annotations proliferated.
The iteration model became: change → fight compiler for an hour → get green → realize the logic was wrong → repeat. The borrow checker was not protecting us from any real hazard. It was slowing us down for no benefit.
This is not a criticism of the borrow checker. It is a design choice with specific trade-offs: it eliminates a class of bugs that appear in systems programming contexts, at the cost of adding friction to code that does not have those bugs in the first place. Java developers moving to Rust for domain logic are paying the friction cost without receiving the safety benefit. That is a bad trade.
“Every error the compiler caught was a bug that would have haunted me for months in production.” That is true for the hot-path event processor. It is not true for the business rule engine that the same compiler rejected for ownership reasons that had nothing to do with whether the rules were correct.— Internal retrospective, month 14
6. The Compile Time Problem Nobody Warned Us About
Every article about Rust acknowledges compile times as a disadvantage. None of them made clear how significantly they would affect our day-to-day experience. This section fixes that.
Our medium-sized Rust service — roughly 15,000 lines of code plus dependencies — compiled in approximately three minutes from clean. With incremental compilation and caching, a typical developer code-change-to-binary cycle was 30–90 seconds. Our comparable Java service compiled in about 12 seconds. The Rust 2024 Edition improved things slightly, and tools like sccache helped in CI. But the gap is real and substantial.
The practical consequence was not just slower development cycles. It was a changed debugging experience. In Java, when you are uncertain how something works, you add a print statement and run it in under 15 seconds. In Rust, the same experiment takes 45 seconds minimum. Over a full working day, those seconds accumulate into meaningfully different experiences of the development process. One of our engineers described the Rust compile time as “turning every hypothesis into a full experiment rather than a quick test.” This slows exploration of unfamiliar APIs and framework behavior — exactly the kind of exploration that is heaviest in the first three to six months of adoption.
The tools that materially helped:
cargo-watch(auto-recompile on save),cargo-nextest(parallel test runner, significantly faster than default),sccache(shared compilation cache in CI), and splitting large crates into smaller ones to improve incremental compilation boundaries. None of these eliminates the problem. They reduce it to something more manageable for experienced Rust developers. New adopters will not know to set these up on day one.
7. Async Rust Was a Different Beast
If the synchronous borrow checker is one learning curve, async Rust is a second, steeper one. This was the single biggest gap between our expectations and reality.
In Java, virtual threads mean you write blocking code and the JVM handles the concurrency transparently. In Rust, async code is explicitly structured around futures, async functions, and an explicit runtime (we used Tokio). The language does not have a built-in async runtime — you choose one, and the choice affects which libraries you can use (libraries written for Tokio are not compatible with async-std without friction). Lifetimes across await points are particularly complex: a reference that is valid before an await may not be valid after it returns, and the compiler is correct to reject this. Understanding why it is correct requires understanding the state machine that the Rust compiler generates from async functions. That is not beginner-level knowledge.
We observed that roughly 65% of the borrow checker friction our engineers encountered came from async code specifically. Pinned futures, Send trait requirements on futures that cross thread boundaries, and the interaction of async code with Arc<Mutex<T>> shared state were the three most common friction points. Engineers who had mastered synchronous Rust still needed several weeks to become productive with async Rust.
The honest async pictureIf your use case for Rust is high-concurrency I/O (HTTP servers, event processors, network services), you will be writing async Rust, which is harder than the Rust tutorials suggest. Budget for it explicitly. The learning curve is not “Rust + some extra stuff.” Async Rust is genuinely a separate competency. Java teams adopting Rust for async I/O workloads should expect that the initial friction is significantly higher than adopting Rust for CPU-bound synchronous work.
8. The Hiring Situation, Honestly
We did not hire Rust developers. We grew them from our Java team. That decision was deliberate, and looking back, it was correct — but it has implications that organizations evaluating Rust adoption need to understand.
The hiring market for Rust is not like hiring Java engineers. The talent pool is small, and the gap between “loves Rust” and “has shipped production Rust code” is larger than in most languages. According to Lemon.io’s assessment of hundreds of technical screenings, roughly 26% of Rust developers use it in professional projects, while 65% use it primarily for side or hobby projects. This means a significant fraction of people who list Rust on a resume have not dealt with database migrations, graceful shutdown handling, or monitoring in production Rust code.
Growing Rust engineers from a Java team is realistic but requires an honest timeline. Our experience: a strong Java engineer with some systems programming background reaches meaningful Rust productivity in four to six months. An engineer without systems experience should budget eight to twelve months. The Rust community’s figure of 67% of new Rust developers needing two or more months to feel productive understates the bar for production-ready work.
The staffing model we recommendIdentify one or two engineers on your existing team who have systems programming background (C, C++, or similar) and genuine enthusiasm for Rust. Give them protected time — six months of reduced feature velocity expectation. Do not hire external Rust contractors for initial production services. The shared context from your Java architecture is more valuable than raw Rust experience. External Rust contractors who do not know your domain will write correct Rust that does not fit your system.
9. Our Decision Framework for Future Services
After eighteen months, we have a usable framework for deciding when to write a new service in Rust versus Java. It has three questions.
Choose Rust vs. Java — Decision Framework by Workload Type

Question 1: Is the runtime the actual bottleneck? If GC pauses, memory footprint, or CPU throughput are measurable production problems — not theoretical concerns, but things that are currently paging people or costing real money — then Rust is worth the cost. If the bottleneck is database queries, network latency, or business logic correctness, Java is almost certainly the right answer and Rust will make those problems harder to fix, not easier.
Question 2: Is the code primarily safety-critical system logic or primarily domain logic? Hot-path data processing, cryptographic operations, serialization libraries, network protocol implementations — these have ownership and safety properties that the borrow checker directly protects. Business rules, workflow orchestration, API handlers — these do not. The former are good Rust candidates. The latter are not.
Question 3: Do we have or can we grow the staff for this? Rust services written by engineers who are still fighting the language are not the stability win the language promises. A Rust service written by engineers who do not yet think in ownership will have more bugs than the Java service it replaced, because those bugs will be logic bugs that neither the compiler nor the GC caught. Staff readiness is a prerequisite, not an outcome.
10. What We Have Learned
Rust is a genuinely excellent language for a specific category of problems. In our eighteen months, it delivered everything the documentation promised for the workloads it was designed for: the event processor’s P99.9 latency dropped from 18ms to 1.2ms. The crypto sidecar has not paged anyone in fourteen months of production. The serialization library runs at 7× the throughput of the Java equivalent on the same hardware. The borrow checker caught data race conditions at compile time that would have been intermittent production incidents in Java.
It also cost significantly more than we expected in three specific areas. Development velocity dropped 30–50% for the first six months, exactly as documented elsewhere, and the async Rust learning curve was a second steep climb that we had not budgeted for. Compile times changed the development experience in ways that accumulate over months into meaningful productivity differences. And the hiring market is small enough that growth from an existing team is not optional — it is the only realistic path for most organizations.
The conclusion that we come back to: Rust adoption succeeds when it is treated as an investment in specific services with specific requirements, not as a platform migration. The Java team that replaces its Spring Boot fleet with Rust services will have a bad time. The Java team that adds two Rust services to address measurable GC and memory problems, grows two Rust engineers from its existing team, and evaluates each subsequent candidate service against a clear criteria will have a very good time. We are firmly in the second category, and we have no regrets about the decision — only about the scope creep we almost allowed into it.



