jnbridge
.NET's Common Type System was supposed to be the neutral ground for dozens of languages. In practice, it had strong C# biases — try using unsigned integers from VB or F#'s discriminated unions from C#. The CLR "primitive types" were just as much a random collection as the WIT primitives are being described here.
The practical lesson from two decades of cross-runtime integration: stop trying to tunnel high-level types. The approaches that survive in production define a minimal shared surface (essentially: scalars, byte buffers, and handles) and let each side do its own marshaling. It's less elegant but it doesn't break every time one side's stdlib evolves.
WASM's linear memory model actually gets this right at the low level — the problem is everyone wants the convenience layer on top, and that's where the type-system politics start.
.NET 9's improvements to Native AOT, Dynamic PGO, and container support have made it a genuinely competitive backend choice on its own merits. But the strongest real-world argument for choosing .NET is when you already have Java in the mix and need both to coexist. The tooling for bridging the two ecosystems has matured significantly — shared memory bridges can do sub-millisecond cross-runtime calls, and both Java 21's virtual threads and .NET's async model handle high-concurrency integration well.
If you're truly greenfield with no constraints, pick whichever ecosystem your team knows best. The performance gap between .NET and JVM stacks is negligible in 2026. But if there's even a chance you'll need to integrate with Java libraries or services later, having .NET in the picture gives you options that Go/Rust/Node don't offer as smoothly.
A few patterns that have worked well in practice:
1. Idempotency keys at the API boundary — every side-effecting call gets a client-generated UUID, and the receiver deduplicates. Simple, but think carefully about the TTL of your dedup window.
2. Outbox pattern — instead of directly calling the external service, write the intent to a local "outbox" table in the same transaction as your state change. A separate process polls the outbox and delivers. Debezium + CDC makes this quite clean.
3. For cross-system workflows: treat the saga orchestrator as the single source of truth for step completion. Each step checks its completion status before executing, so steps must be idempotent OR the orchestrator tracks state.
In practice, designing for at-least-once delivery + idempotent receivers is more reliable than trying to achieve exactly-once through distributed coordination. Exactly-once across system boundaries is effectively a myth outside of systems that support two-phase commit (and even then it's fragile).
- Java went through it with java.util.stream (pull-based, lazy) vs Reactive Streams/Project Reactor (push-based, backpressure-aware). The result was two completely separate APIs that don't compose well.
- .NET actually handled this better with IAsyncEnumerable<T> in C# 8 — a single abstraction that's pull-based but async-aware. It composes naturally with LINQ and doesn't require a separate reactive library for most use cases.
- Go side-stepped the problem entirely with goroutines and channels, making the whole streams abstraction unnecessary for most cases.
What I find interesting about this proposal is it's trying to learn from that prior art. The biggest mistake Java made was bolting async streams on top of a synchronous abstraction and then needing a completely separate spec (Reactive Streams) for the async case. If JavaScript can get a single unified abstraction that handles both sync iteration and async backpressure, that would be a genuine improvement over what exists in most other runtimes.
The more interesting question might be: how well does your architecture handle having both? Teams that go all-in on one stack often find themselves 3-5 years later integrating with a Java service they inherited through an acquisition, or needing a .NET library that doesn't have a JVM equivalent.
To actually answer the question though: C# and .NET in 2026 are genuinely excellent. The language has evolved faster than Java in many ways (records, pattern matching, LINQ). But Java 21+ has closed the gap significantly with virtual threads, sealed classes, and the continued investment in GraalVM. For a greenfield project, the honest answer is both are fine choices — your team's familiarity matters more than language features at this point.
What I wouldn't do is pick a stack purely based on cloud vendor lock-in. That's the one decision that actually limits your future options.