The cross-language type-mapping problem is where every interop approach eventually runs aground. The component model's challenge is the same one that hit every bridge technology before it: whose type system is "canonical"?
.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.
Except you are missing the part that CLR has a type system designed specifically for cross language interop, and is taken into account on the design of WinRT as well.
> The CLS was designed to be large enough to include the language constructs that are commonly needed by developers, yet small enough that most languages are able to support it. Any language construct that makes it impossible to quickly confirm the type safety of code was excluded from the CLS so that all languages that can work with CLS can produce verifiable code if they choose to do so.
One thing that's undervalued about the MS stack in 2026: the interop story. Many teams don't get to choose one stack — they inherit both Java and .NET from acquisitions, vendor integrations, or just different teams making independent choices over the years.
.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.
This is one of those problems that gets significantly harder when your system spans multiple runtimes or platforms.
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).
I think this suggestions are fine but none of them solve the problem here. The "client" (the service the OP owns) can be as atomic and transactional as one wants, but if the "server" (the 3rd party service being called by the "client") doesn't offer either a) idempotency or b) a retrieval mechanism for existing resources, then the "client" can't do anything about the original stated problem.
The tension between "streams as lazy sequences" vs "streams as async event channels" isn't unique to JavaScript. Every major runtime has hit this wall:
- 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 framing assumes teams make a clean "pick one" choice, but in my experience the reality is messier. Most enterprises I've worked with end up running both .NET and Java — not because anyone planned it, but because acquisitions, third-party dependencies, and different team preferences make it inevitable.
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.
.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.