At some scale, composition becomes the harder problem in Rust applications, not memory safety or performance.
In ecosystems like Java, this is typically addressed with dependency injection frameworks, but those rely heavily on runtime mechanisms (reflection, containers, dynamic resolution), which don’t map well to Rust.
I’ve been exploring a different direction:
Can dependency injection be done entirely at compile time?
The approach I ended up with is based on:
traits as capability contracts
“use-traits” to express dependencies
generics + monomorphization for wiring
macros to reduce boilerplate
The goal is to keep everything:
fully type-checked at compile time
no runtime container
no dyn in the core path
no Arc/Rc required for composition
The code is a bit too large to include here, so I’ve put everything into a small repo:
I’d really appreciate feedback on a couple of things:
Does this approach make sense in Rust, or does it feel like it goes against idiomatic patterns?
Am I solving a real problem here, or is this overengineering?
Are there existing patterns/crates that address this in a simpler or more “Rusty” way?
Also, since I’ve already started working in this direction, I’m curious if there’s a meaningful way this could be useful for the community (as a library, pattern, or something else).
It's certainly awesome, however I am more interested in if a web server can dynamically link a Rust program by an HTTP request. Do you do something in the direction? Generally my crate can implement some trait, and the server just dynamically calls the crate. I can do that at compile time as you proposed, however it requires each time calling compiler. Maybe it isn't a big deal, because I did it on IBM370 sometimes ago, dynamically pulling PL/I compiler and then executing the result. Pulling rustc isn't more complex.
If we’re speaking about Rust, compile-time binding as the baseline is still the more idiomatic and “Rust-friendly” approach for several reasons: performance, type safety, interoperability, predictability, and security.
That said, there are situations where dynamic binding is not a design choice, but a functional requirement. A good example is NGINX, which is intentionally designed as middleware that supports runtime plugins.
In such cases, the goal is not to replace compile-time DI, but to layer dynamic binding on top of a static foundation.
This leads to an important asymmetry:
If your DI system supports compile-time binding, you can extend it with dynamic behavior when needed.
But if your system is fundamentally dynamic, you cannot realistically move it to compile-time without reworking the architecture.
In my article, I explore a related example with brokers, where components are aggregated from multiple sources. This connects directly to your question about HTTP frameworks, where controllers may come from:
The implementation of DynamicLookup is intentionally left out here, since it is a separate concern. If you’re curious, something like libloading can be used to load .so / .dll at runtime.
At this point, everything depends on how DynamicLookup is implemented. This is where you can introduce logic to load dynamic libraries and retrieve trait objects from them.
One important caveat: runtime-loaded Rust code introduces ABI challenges.
dyn Trait is not ABI-stable
different compiler versions and optimization settings can break compatibility
this typically pushes you into unsafe territory
To make this reliable, it is better to rely on established FFI patterns or solutions specifically designed for ABI stability.
If we step back, dynamic binding is often overused when it is not actually required.
In backend systems (microservices, Docker containers), you already rebuild the application (cargo build) for every new version of deployment. Runtime binding adds little value.
In embedded systems, compile-time binding is even more important due to size and platform constraints.
Dynamic plugins make the most sense in desktop applications, but even there:
scripting languages (e.g., Lua) are often preferred for compatibility
alternatively, you can recompile on plugin changes, trading UX for performance and safety, somewhat similar in spirit to how Java Virtual Machine JIT compilation works
So the general idea is:
Use compile-time DI as the foundation. Add dynamic behavior only where it is truly required by the problem.
Everything else tends to introduce unnecessary complexity.