Project Panama in Production: A Success Story with Native FFI Editorial Team, January 9, 2026January 9, 2026 For generations of Java developers, the Java Native Interface stood as the sole passageway from the language’s controlled ecosystem to the unrestrained capability of system-level code. Deemed necessary, those engineers who have operated it understand its core reality: a temperamental, paperwork-intensive, and perilous architecture. The routine meant writing Java, followed by composing adhesive C snippets, shepherding breakable custom integrations, and fearing inexplicable memory violations. This persisted as our standard—until we launched our venture with Project Panama. This is the story of how we migrated a performance-critical component from a legacy JNI quagmire to Project Panama’s modern Foreign Function & Memory API (FFM). The result wasn’t just a technical upgrade; it was a dramatic simplification of our stack, a significant performance gain, and a developer experience transformed from dread to delight. Table of Contents Toggle The Burden of Our JNI LegacyWhy Panama? The Promise of a Modern BridgeThe Migration: A Phased ApproachThe Production Payoff: A Success Story in NumbersLessons from the TrenchesConclusion: Not Just a New API, a New Paradigm The Burden of Our JNI Legacy Our service, a high-throughput data transformer, relied on a battle-tuned C library for proprietary matrix operations and cryptographic routines. Our JNI integration worked, but at a steep cost: The Boilerplate Beast: Every new function required a tedious dance: define a native method in Java, generate a C header, implement the stub, manage JNIEnv calls, and manually convert types. A simple process_data(Data* in, Data* out) call bloated into hundreds of lines across two languages. Memory Duality: We constantly juggled Java heap and native memory. Direct ByteBuffer helped, but was clunky. Off-heap leaks were ghosts in the machine, visible only as a gradual, inevitable process of death. The Complexity Cliff: Error handling was a two-layer nightmare. A null in Java? A -1 return code in C? An errno? Debugging meant tracing through JNI’s opaque veil. Onboarding new engineers took weeks just for this subsystem. Performance Tax: The JNI call overhead itself was acceptable, but our workarounds—copying data to avoid pinning, intermediate buffers—added latency we could no longer afford. See also Testing Strategies: Unit, Integration, and Containerized Tests with TestcontainersWe knew there had to be a better way. When Project Panama moved from early-access to a preview feature, and finally to being a standard feature in JDK 22, we saw our path forward. Why Panama? The Promise of a Modern Bridge Project Panama’s goal is simple in ambition, profound in impact: reshape the interaction between Java and native code. Its Foreign Function & Memory API offers a pure-Java model for accessing native memory and calling foreign functions. Its core promises resonated deeply: No Native Glue Code: Define everything in Java. No more C stubs. Type Safety & Control: A rich API for describing native memory layouts (MemoryLayout) and symbol linkages (Linker). Memory Management: A deterministic, safe model via MemorySession (now Arena), eliminating leaks. Performance: Designed for low overhead, with a path to even better performance through jextract. The Migration: A Phased Approach We didn’t rip and replace. We adopted a careful, phased strategy over one development cycle. Phase 1: Exploration & Tooling Setup First, we used jextract, Panama’s powerhouse tool. We pointed it at our library’s main header file: jextract --source --output src/main/java --target-package com.ourlib.panama libours.h Like magic, it generated a mountain of Java source code: friendly, type-safe abstractions for every struct and function in our API. This wasn’t just convenience; it was a revelation. It gave us a working, compilable understanding of our own native API’s Panama shape. Phase 2: Piloting a Core Function We chose a foundational, mid-complexity function: encrypt_block. Our JNI version was a classic—a byte[] in, a byte[] out, with a secret key handle.The Panama rewrite was strikingly clean: // 1. Define the native symbol (using the jextract-generated header) static final Linker linker = Linker.nativeLinker(); static final SymbolLookup libLookup = SymbolLookup.libraryLookup("libours.so", Arena.global()); static final MethodHandle encrypt_block = linker.downcallHandle( libLookup.find("encrypt_block").orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, // input struct* ValueLayout.ADDRESS, // output struct* ValueLayout.JAVA_LONG // key handle )); // 2. Use it within an Arena (memory session) public void encrypt(OurDataBlock input, OurDataBlock output, long keyHandle) throws Throwable { try (Arena arena = Arena.ofConfined()) { MemorySegment nativeIn = input.toSegment(arena); // Our helper to copy to native MemorySegment nativeOut = arena.allocate(output.layout()); int status = (int) encrypt_block.invokeExact(nativeIn, nativeOut, keyHandle); if (status == 0) { output.fromSegment(nativeOut); // Our helper to copy back to Java } else { throw new EncryptionException("Native error: " + status); } } } The code was all Java. They Arena scoped the native memory lifetime perfectly. No native keyword in sight. The mental model shifted from “bridging two worlds” to “managing one world with two regions.” See also Java Meets Vector Databases: Building AI-Powered SearchPhase 3: Taming Memory: From Copying to Confidence Our initial helper methods (toSegment, fromSegment) copied data for safety. This was correct, but reintroduced the copy overhead we hated. Panama’s magic is its control. For large, long-lived matrices, we changed our Java class to be a wrapper around a native MemorySegment from the start, allocated in a shared Arena. This turned a memory-heavy operation from a copy-then-process into a direct, in-place process. The performance graph didn’t just improve; it cliff-jumped downward. Phase 4: Full Integration & De-risking We built a parallel Panama-powered service layer. For a full sprint, we ran both the JNI and Panama implementations in shadow mode, processing real traffic but discarding the Panama output, comparing logs and metrics. The results were identical, but the Panama subsystem showed ~15% lower latency and dramatically lower GC pressure because it bypassed the heap entirely for large data. Confidence grew. The Production Payoff: A Success Story in Numbers We cut over during a scheduled maintenance window. The rollout was, to our team’s collective surprise, boring. That was the first success. Performance: A sustained 12-18% throughput increase on our core transformation pipeline. The latency tail (p99) improved even more markedly, smoothing by over 25%. Reliability: No more gradual memory creep. Our memory charts became flat, predictable lines. The number of “mystery native crash” alerts went to zero. Developer Velocity: What used to take two days of cross-language plumbing to expose a new native function now takes an hour. jextract does the heavy lifting; we write clean, memory-safe Java adapters. Onboarding is now a matter of explaining Arena and MemorySegment, not the arcana of JNIEnv->GetXXXArrayElements. Codebase Health: We deleted over 5,000 lines of C glue code and JNI boilerplate. Our Java code became the single source of truth for the integration. The cognitive load on the team plummeted. See also Machine Learning in Pure Java: A Look at Tribuo and DJLLessons from the Trenches It wasn’t all automatic. We learned crucial lessons: jextract Is Your Friend, But Review its Output: The generated code is verbose. We started by using it as a reference, then hand-rolled more concise bindings for hot paths, using the generated code as a specification. Arena Discipline is Paramount: Choosing between confined, shared, and global arenas is critical. We established a clear policy: per-request operations use confined arenas; shared, long-lived caches use a dedicated shared arena with a clear lifecycle. This made memory management intuitive and leak-free. The JVM Still Shines: By moving the complexity into Java, we could leverage all our existing tools: unit tests (mocking MethodHandle), profiling with async-profiler (which now sees Panama calls clearly), and monitoring. Start with JDK 22+: We began on JDK 21 with preview features enabled. Moving to JDK 22 for the final cutover removed the warning flags and gave us the stable, final API. The Panama team’s work on performance and stability between these releases was noticeable. Conclusion: Not Just a New API, a New Paradigm Project Panama is more than a JNI replacement. It’s a paradigm shift that reclaims native interoperability as a first-class concern within the Java language. It brings it into the light of Java’s type safety, tooling, and memory management philosophies. Our success story is a testament to a bold Oracle and OpenJDK vision made real. We’ve escaped the JNI dark ages. Our service is faster, simpler, and more robust. The bridge between Java and native is no longer a rickety rope-and-plank affair; it’s a modern, high-speed data pipeline, and we’re confidently running our most critical workloads across it. If you are maintaining a complex JNI bridge or considering tapping into the vast ecosystem of native libraries, the future has arrived. It’s time to set sail with Panama. Java