DEV Community

Cover image for Java and .NET Without REST: Direct Interop Guide
JNBridge
JNBridge

Posted on Originally published at jnbridge.com

Java and .NET Without REST: Direct Interop Guide

REST is a good default for distributed systems, but it is not a law of nature. If a C# application needs to reuse an existing Java library—or Java needs to call a .NET assembly—turning every class operation into an HTTP endpoint can create more architecture than the problem requires.

This guide compares the main ways to connect Java and .NET without treating REST as the automatic answer. The key is to decide whether you actually need a service boundary, an asynchronous workflow, or a runtime interoperability boundary.

Start with the boundary, not the transport

When someone says “put a REST API around it,” they may be solving several different problems at once:

  • isolating deployment and failures;
  • making a capability available to many consumers;
  • supporting clients in several languages;
  • crossing a network boundary;
  • reusing code that happens to run on another runtime.

The first four are legitimate reasons to design a service. The last one may only require direct interoperability.

Suppose a .NET desktop application needs a mature Java calculation library. A REST wrapper means building and operating a Java service, inventing request and response contracts, mapping objects to JSON, handling versioning, securing an endpoint, and deploying another process. That can be worthwhile—but it should be an intentional architecture decision, not a reflex.

Where REST wrappers become awkward

REST works best with coarse-grained resources and operations. It becomes less natural when the real interface is object-oriented, internal, high-churn, or fine-grained.

Wrapper explosion

Every method that crosses the boundary needs an endpoint, payload schema, validation path, error contract, documentation, and tests. A team can accidentally build a second application whose main job is translating between two existing applications.

Lost type semantics

Rich Java and .NET types often become JSON documents plus handwritten mapping code. Overloads, enums, exceptions, object identity, callbacks, and references need new representations. The public contract may be clean, but it is not free.

Chattiness and latency

An object model designed for local calls may make dozens of small calls during one business operation. Mapping each call to HTTP adds serialization, network handling, and request-level observability overhead.

Operational surface area

A new service brings another deployment, health model, certificate path, log stream, monitoring target, and incident runbook. Those costs are justified for an intentional distributed boundary, but harder to defend for internal library reuse.

The main alternatives

Java and .NET can cooperate without REST in several ways. Each option solves a different class of problem.

Pattern Best fit Poor fit
Runtime bridge with generated proxies Typed, direct reuse of Java or .NET classes Systems that must be fully independent services
Message queue Asynchronous workflows and event processing Immediate object-level responses
Shared database Simple state exchange and reporting Shared business logic or strong ownership boundaries
Files or object storage Batch transfers and large artifacts Interactive operations
Custom sockets or binary protocol Specialized latency or protocol requirements Teams that do not want to own protocol infrastructure
REST/gRPC Coarse-grained, independently deployed services Thin wrappers around many internal library calls
Rewrite Retiring an obsolete platform completely Stable systems whose behavior is expensive to reproduce

The important distinction is that these patterns are not interchangeable. A queue is not a faster REST call. A shared database is not an API. A runtime bridge is not automatically a microservice.

Direct interoperability with generated proxies

A Java/.NET bridge can expose selected types from one runtime as generated proxy types in the other. The application uses local-looking classes and methods while the bridge handles communication, data conversion, object references, and exceptions.

The build flow is usually:

  1. Select the Java classes or .NET assemblies that should cross the boundary.
  2. Generate proxies for that deliberately small surface.
  3. Add the generated JAR or assembly to the consuming project.
  4. Configure where the JVM and CLR sides run.
  5. Call through the proxies using normal Java or C# syntax.

For example, a .NET application consuming a Java risk engine can work with generated .NET proxy classes:

// Generated proxies expose the selected Java API to C#.
var engine = new RiskEngine();

var request = new RiskRequest();
request.setAccountId(accountId);
request.setPositions(positions);

RiskResult result = engine.calculate(request);
Console.WriteLine($"Score: {result.getScore()}");
Enter fullscreen mode Exit fullscreen mode

The exact bootstrap and proxy names depend on the selected bridge configuration, but the architectural point is stable: the consuming code sees a typed API instead of constructing an HTTP request for every method.

The reverse direction works the same way. A Java application can consume selected .NET classes through generated Java proxies.

A bridge still needs a well-designed boundary

Direct calls do not make boundary design irrelevant. Cross-runtime calls are more expensive than ordinary in-process calls, and a large proxy surface can couple two codebases too tightly.

Use the same discipline you would apply to an internal API:

  • expose stable, meaningful operations rather than every implementation class;
  • send batches or domain objects instead of making one call per field;
  • keep object ownership and lifetimes explicit;
  • translate exceptions into failures the caller can act on;
  • benchmark realistic object graphs, arrays, callbacks, and concurrency;
  • regenerate proxies as part of a controlled upgrade workflow.

A useful pattern is a small facade on the provider side. The facade can turn a chatty library into a few coarse operations without forcing the team to build and operate a web service.

Shared memory or separate processes?

“Without REST” does not necessarily mean “everything in one process.” A bridge can support different runtime topologies.

Shared-memory or in-process-style deployment minimizes call overhead when both runtimes run on the same machine and share a lifecycle. It is useful for desktop software, local services, hardware-facing applications, and performance-sensitive library reuse.

TCP/binary deployment keeps the Java and .NET sides in separate processes or on separate machines. It adds isolation while retaining a proxy-based programming model. This is useful when operations require separate restart behavior, security controls, or deployment ownership.

Topology should be an explicit choice. Do not use the lowest-latency option if process isolation is the more important requirement.

When REST or gRPC is still the right answer

Avoiding REST should not become a goal by itself. A service boundary is the better design when:

  • several independent consumers need the capability;
  • provider and consumer must deploy independently;
  • the contract should be language-neutral and externally documented;
  • the call is naturally coarse-grained;
  • network isolation or organizational ownership is part of the architecture;
  • horizontal scaling and service-level observability matter more than object-level reuse.

gRPC is often a stronger option than REST when the team wants an explicit schema, generated clients, streaming, and efficient binary transport. It still creates a service boundary, which is exactly what you want in those scenarios.

A decision framework

Before adding another API, answer these questions:

  1. Who is the consumer? An internal Java/.NET application or a broad set of clients?
  2. What is being reused? A stable library or an independently owned business capability?
  3. How many operations are needed? A few coarse calls or dozens of thin wrappers?
  4. What is the call shape? Batched domain operations or fine-grained object interaction?
  5. Who owns both sides? One team or separate organizations with different release cycles?
  6. What failure isolation is required? Shared lifecycle or independently recoverable processes?
  7. What must be observable? Cross-runtime calls inside one application or a separately operated service?

Choose the smallest boundary that satisfies those requirements.

Production checklist

Whichever pattern you choose, validate more than the happy path:

  • version the boundary and document compatibility;
  • test arrays, collections, nulls, exceptions, and object lifetimes;
  • measure realistic payloads and call frequency;
  • define startup, shutdown, and restart behavior for both runtimes;
  • centralize logs and correlation identifiers;
  • test deployment upgrades and rollback;
  • limit the exposed surface to what consumers actually need.

Final takeaway

Java and .NET do not need REST merely because they use different runtimes.

Use REST or gRPC when you want a real distributed service. Use messaging for asynchronous workflows. Use files or shared storage for batch exchange. Use a generated-proxy bridge when the actual requirement is direct, typed reuse of existing Java or .NET code.

The best architecture is not the one with the most fashionable transport. It is the one whose boundary matches the ownership, deployment, latency, and maintenance model of the system.

The expanded source guide is available at Java .NET Without REST.

Top comments (0)