A shared library across two languages needs a contract, not parity
I maintain a fleet of internal MCP servers at KnowBe4, most of them TypeScript, one in Rust because its workload (a high-throughput sync loader) needed Rust's performance characteristics. Both languages needed the same cross-cutting infrastructure: auth middleware, structured logging that correlates with distributed traces, health checks, OAuth discovery endpoints, OpenTelemetry wiring. Writing that twice, by hand, per service, was costing roughly 300 lines of boilerplate every time a new server came online. So I built a shared library, once per language, and the interesting design work wasn't the code inside either one. It was deciding what the two implementations were required to agree on and what they were free to do differently.
Naming the contract explicitly
If you don't draw that line on purpose, you end up drawing it by accident, and by accident means inconsistently. I named four things as the actual contract between the Node and Rust libraries: module boundaries (auth, middleware, logger, tracing, and the request proxy are separate concerns in both, not collapsed differently per language), configuration shape (the same environment variables mean the same thing in both), auth semantics (a token that's valid in one language's implementation is valid in the other's, checked the same way), and a documented version-parity note so nobody has to guess whether the two are still in step.
Everything else, internal structure, idiomatic patterns, how each language's ecosystem prefers to handle a given concern, is explicitly not part of the contract and is free to diverge. A Rust service and a Node service calling the shared library look nothing alike internally. They behave identically from the outside, which is the property that actually matters to a caller.
Why logger and tracer live in the same package
The one decision I'd point to as the most important is a small one: the structured logger and the distributed tracer are not separate libraries a service wires together itself. They're the same package, deliberately coupled, specifically so every log line a service emits automatically carries the current trace id.
The alternative, two independent libraries with a README telling engineers to wire them together correctly, sounds reasonable and fails silently. A service that forgets the wiring still logs fine and still traces fine. What it loses is the ability to jump from a trace to the logs that happened inside it, and nothing tells you that's missing until you're mid-incident trying to make that jump and it isn't there. Correlation is a property you get by structural coupling, not by good intentions in a README. The failure mode of skipping it is silent, so it has to be prevented structurally, not documented as a best practice.
The blind spot I chose to accept
One thing this design doesn't protect against, and I decided not to try to: two different libraries, or two different versions of the same library, both independently configuring OpenTelemetry inside one process. The shared library includes a double-registration guard for its own instance, but that guard has no way to see a second, unrelated OTEL setup someone else wired in. Closing that gap completely would mean either owning every telemetry decision a consuming service makes, which defeats the point of keeping the languages free to diverge everywhere else, or building a much heavier coordination mechanism for a failure mode that, in practice, only shows up when someone's actively fighting the library rather than using it as intended. I named the gap in the library's own docs instead of pretending it isn't there.
Where the two libraries will diverge next
The next time a Rust service and a Node service both need a capability neither library has today, the same question comes up again: does this belong in the four-item contract, or is it free to diverge. Nothing about the current split guarantees the answer stays the same shape twice in a row. The OTel double-registration gap already shows what happens when the honest answer is "neither, yet": it goes in the docs, not the code, until it's the thing that's actually breaking rather than the thing that might.
More in Infrastructure
All in Infrastructure →- Bedrock model access is three gates, not one permissionA Bedrock AccessDeniedException can mean three different things, so we built a nightly discovery job and a runbook that checks each gate in order.Infrastructure
- The credential nobody watchedA shared service-account token expired silently and took a production integration down for 40 hours. The fix that mattered wasn't rotation. It was making four causes of the same error distinguishable.Infrastructure
- Your infrastructure has a new client, and it pushes changesThe architecture diagram for our shared AWS platform names two human actors and lists an AI coding agent's own cloud egress as a third. That's a trust-model decision, not a diagram detail.Infrastructure