Disclosure first: I work at JNBridge. We've been solving Java/.NET integration for two decades, so this article makes a case for our approach. But every claim is backed by numbers and code, and I'll tell you exactly when our tool is overkill.
Why You're Even Reading This
Your Java application needs functionality that lives in .NET. The usual suspects:
- A C# pricing engine, rules engine, or analytics library the company has invested years in and isn't going to rewrite
- .NET-specific libraries (Office automation, vendor SDKs, certain finance and scientific libraries) with no Java equivalent
- Windows APIs your Java app suddenly needs to touch
- A .NET service that's too entangled with its dependencies to wrap behind a clean network API
The instinct is "why not just port it to Java?" The honest answer is money, time, and risk. So you integrate. The question is how — and most teams quietly pick the most expensive option without realizing it.
Option 1: REST APIs — Easy to Build, Expensive to Use
Wrap the .NET code in ASP.NET Core, call it from Java.
[ApiController]
[Route("api/[controller]")]
public class PricingController : ControllerBase
{
[HttpPost("calculate")]
public ActionResult<PriceResult> Calculate([FromBody] PriceRequest request)
{
var engine = new PricingEngine();
return Ok(engine.CalculatePrice(request));
}
}
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:5000/api/pricing/calculate"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
The hidden tax: 20–50ms per call, plus serialization overhead, plus a separate service to deploy, secure, monitor, and own.
That's fine when you make a handful of calls per user request. It's brutal when a single business operation needs 10–15 cross-runtime calls — and that's the more common pattern than teams admit. 300–750ms of pure network and serialization overhead before any computation runs.
You're also signing up for JSON contract drift, auth, retries, two deployment pipelines, two on-call rotations, two monitoring stacks. REST is "simple" in the same way wheeling groceries home in a wheelbarrow is simple.
Use REST when: Java and .NET are genuinely separate services with coarse-grained communication. Don't use REST when: the .NET code is effectively a library your Java application depends on.
Option 2: gRPC — A Smaller Tax
Binary serialization over HTTP/2, typed contracts via Protocol Buffers.
ManagedChannel channel = ManagedChannelBuilder
.forAddress("localhost", 5001)
.usePlaintext()
.build();
PricingServiceGrpc.PricingServiceBlockingStub stub =
PricingServiceGrpc.newBlockingStub(channel);
PriceResult result = stub.calculatePrice(
PriceRequest.newBuilder()
.setProductId("WIDGET-123")
.build());
5–15ms per call instead of 25–75ms. Typed contracts catch breaking changes at compile time. In return, you maintain .proto files in sync across two codebases that drift the moment no one's looking, and you still pay network latency on every call.
Better than REST for fine-grained calls. Still a separate service. Still network-bound.
Option 3: JNI Bridge — The Path of Pain
Write C++ that hosts the .NET CLR and exposes it to Java via JNI.
Teams attempt this. Teams abandon it. JNI plus C++ memory management plus .NET hosting APIs is a debugging nightmare — one mismanaged reference takes down the JVM with minimal diagnostics. There are niche embedded cases where it makes sense. For typical enterprise integration, the engineering cost almost never pays back.
Option 4: Subprocess Execution — Batch Jobs Only
ProcessBuilder pb = new ProcessBuilder(
"dotnet", "run", "--project", "PricingEngine",
"--", "WIDGET-123", "100"
);
Process process = pb.start();
String result = new BufferedReader(
new InputStreamReader(process.getInputStream())).readLine();
.NET startup is 200ms+ per invocation. Acceptable for nightly batch. A non-starter for interactive or stateful workloads.
Option 5: In-Process Bridging — How JNBridgePro Works
Disclosure-first version: this is our product. Architecture-first version: instead of running Java and .NET as separate processes that chat over a socket, JNBridgePro runs the JVM and the CLR inside the same process. We generate strongly-typed Java proxy classes from your .NET APIs at build time. Your Java code calls .NET like it would any other Java library:
// Real .NET PricingEngine, called from Java, no network in between
PricingEngine engine = new PricingEngine();
PriceResult result = engine.calculatePrice("WIDGET-123", 100);
That's not a hidden HTTP call wearing a Java costume. It's a real method invocation against a real .NET object living in the same process as your Java code.
What that buys you:
- Sub-millisecond call latency. Microseconds to cross the boundary, not milliseconds. A 15-call operation drops from ~300ms (REST) to ~15ms (in-process).
- Native Java ergonomics. Your IDE understands the .NET types. Type safety, exception propagation across runtimes, and a debugger that steps from Java into C# and back.
- Bidirectional integration. Java calls .NET, .NET calls Java, callbacks work, events work. You can subclass a .NET class in Java and hand the instance back to .NET code that uses it polymorphically.
- Either direction supported. Embed the CLR inside a JVM, or the JVM inside a .NET process. Same product, both configurations — useful when one side of your stack shifts.
- One deployment. No service to operate, no network to secure, no JSON schema to keep in sync between teams.
The pattern we see most often: a Java application calling a .NET pricing engine, rules engine, or analytics library through a REST service. The hot path makes a dozen-plus calls per request, latency is in the seconds, the user experience suffers. They move the same Java and .NET code to in-process bridging — no rewrites, just a different transport — and per-request latency collapses by an order of magnitude. Same library, same client code, different bridge.
The Other Tools You'll Find
- jni4net — Open source, effectively abandoned since around 2015. Doesn't support modern .NET. You'll see it in old Stack Overflow answers; ignore it.
- IKVM — Cross-compiles between the runtimes. Clever, but struggles with libraries that use reflection, P/Invoke, or recent runtime features. You can hit the wall well into a project.
- Javonet — Commercial alternative with a different architectural approach. Worth evaluating alongside us; we're confident in how JNBridgePro stacks up for tight, bidirectional integration with large enterprise .NET stacks.
Performance, Side by Side
| Approach | Per-call latency | 15-call operation | Best for |
|---|---|---|---|
| REST | 20–50ms | 300–750ms | Loosely-coupled services |
| gRPC | 5–15ms | 75–225ms | Moderate-frequency RPC |
| Subprocess | 200ms+ startup | N/A | Batch / scheduled jobs |
| JNBridgePro (in-process) | <1ms | ~10–15ms | Tight, library-style integration |
For one or two calls per request, the row you pick barely matters. For real integration workloads, the row you pick is the difference between a UI that feels fluid and one that doesn't.
How to Choose, Honestly
Three questions:
- How many cross-runtime calls per user request? A handful → REST or gRPC. Dozens → in-process is the right architecture.
- What's your latency budget? Seconds → REST. Hundreds of ms → gRPC. Tens of ms → in-process.
- Is the .NET code part of your application, or a separate service? If it's a separate service, talk to it like one. If it's effectively a library, call it like one. Most teams answer "part of my application" and then build a service anyway, because building services is what they know how to do.
If REST genuinely fits, use it — it's free and simple. But if you're staring at an HttpClient call to a .NET service that's effectively a library your Java app depends on, you're paying for an architecture you don't actually need.
Try It
JNBridgePro ships with a free evaluation, full documentation, and worked examples covering Office automation, WCF, ADO.NET, and most of the enterprise .NET surface area. If REST has been your default and you've never benchmarked the alternative, the eval is the fastest way to see what your application's actual latency floor looks like.
Over to You
What does your Java/.NET integration look like in production today? If you went with REST, was it the right call in retrospect — or the path of least resistance? Curious to hear the war stories in the comments.
Disclosure: I work at JNBridge, which makes JNBridgePro. Latency ranges above reflect typical observed numbers from internal benchmarks and customer deployments; your specifics will vary with payload size, call frequency, and serialization complexity.
Top comments (0)