Shaku builds a dependency graph in Rust before the program runs
Compile Time Dependency lnjection Library for Rust
At a glance
- What is it?
- Shaku is a Rust dependency injection library that turns modules, components and providers into types checked at compile time, with derive macros for wiring and separate crates for Rocket, Axum and Actix. It is a good fit for a service that wants DI structure without a runtime container, and a poor fit for a codebase that cannot move to a recent Rust toolchain.
- Who is it for?
- Shaku fits Rust services that want a single place to describe their object graph, want missing dependencies to fail the build rather than a request, and are willing to accept a single maintainer who says no major changes are planned. It does not fit a project pinned to an older toolchain, since the minimum supported Rust version is 1.88.0, nor one that expects runtime registration of arbitrary implementations.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 75 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A dependency graph that the type checker has already read
Shaku is a compile time dependency injection library for Rust. The problem it solves is familiar: a service grows until the constructors of your types are a web of plumbing, and every test needs a hand built set of doubles. A runtime container fixes the plumbing but pays for it at startup, and it moves wiring mistakes from the compiler to the first request.
Shaku takes the opposite route. You describe the graph with a module, and the graph is checked while the crate compiles. A missing implementation, a component wired to an interface nothing provides, or a parameter that cannot be satisfied is a compile error rather than a runtime surprise. For a library author who publishes a public API, that difference decides whether a consumer's integration test fails in their CI or in production.
The API surface is built around four ideas: a module, a component, a provider, and an interface. Those four carry the whole design, and everything else in the crate is detail about how they connect.
Components are singletons, providers are factories
The distinction between a component and a provider is the core semantic of the library, and it is a runtime distinction, not a naming one.
A Component represents a single instance of a service, in other words a singleton. Each time that component is resolved you get the same instance back. A Provider behaves like a factory. Each time it is resolved you get a new instance.
That difference decides how you model services. A connection pool, a frozen configuration value, a logger that carries accumulated state: all of these want component semantics, because sharing one instance is the point. A request handler that captures per-request state, or a short lived value object, wants provider semantics, because a fresh instance per resolution is the point.
Both live in the same module declaration, which is why the module macro lists them separately:
module! {
MyModule {
components = [LoggerImpl, DateLoggerImpl],
providers = []
}
}Note that in the shipped example the providers list is empty. Shaku gives you the mechanism for transient services, but the reference usage is entirely singletons, which tells you where the library's centre of gravity is.
Deriving a component and injecting an interface
A service is declared by deriving Component on its struct and naming the interface it satisfies in a shaku attribute.
#[derive(Component)]
#[shaku(interface = Logger)]
struct LoggerImpl;A component that itself needs a dependency uses an inject attribute on the field holding it, and the field is an Arc of the interface trait rather than of the concrete type. That is what lets a module swap the implementation without touching the consumer.
#[derive(Component)]
#[shaku(interface = DateLogger)]
struct DateLoggerImpl {
#[shaku(inject)]
logger: Arc<dyn Logger>,
today: String,
year: usize,
}The fields with no inject attribute, today and year here, are parameters. Parameters are the values you supply when you build the module, which is how Shaku keeps runtime configuration out of the global scope. A component that needs different configuration in a test and in production gets a different built module rather than a different global.
The traits themselves are declared with the Interface marker, which tells the derive machinery that the type is meant to be resolved as a trait object.
Building the module and resolving through the interface
Construction goes through a builder on the module type. You attach parameter values for the components that need them, then build, then resolve by trait.
fn main() {
let module = MyModule::builder()
.with_component_parameters::<DateLoggerImpl>(DateLoggerImplParameters {
today: "Jan 26".to_string(),
year: 2020
})
.build();
let date_logger: &dyn DateLogger = module.resolve_ref();
date_logger.log_date();
}The resolution is typed: the local is annotated as &dyn DateLogger, and the compiler checks that the module can produce one. That is the payoff of the compile time claim. If the module does not contain a DateLogger implementation, or contains one that cannot satisfy its dependencies, the build fails here rather than at the call site.
Resolve returns a reference, which suits the singleton case where the module owns the instance for its whole life. For a provider, the natural result is a new owned instance, and the documentation's provider guide covers that path separately.
The example also shows the shape of the parameters struct, DateLoggerImplParameters here, which the derive macro generates to match the non injected fields of the component.
Framework crates for Rocket, Axum and Actix
Dependency injection in a web service is where the design either pays off or becomes friction, and Shaku ships three integration crates for it: shaku_rocket, shaku_axum and shaku_actix, each with a linked example under the repository.
The value of these crates is that they wrap a Shaku module into whatever request scoped container the framework uses. A handler can then ask for an interface rather than receiving a wide struct of unrelated services, and the framework's own lifetime rules decide when a resolved value is dropped.
There is also a submodules guide, which covers splitting a module into smaller modules. That matters as a codebase grows, because a single module macro listing every component becomes its own maintenance problem, and the ability to compose modules keeps the declaration local to a feature.
The workspace layout shows how the pieces separate. The root Cargo.toml lists five members.
[workspace]
resolver = "3"
members = [
"shaku",
"shaku_derive",
"shaku_actix",
"shaku_axum",
"shaku_rocket"
]The core crate holds the runtime, shaku_derive holds the procedural macros, and the three integration crates are optional dependencies you take only if you use that framework.
Hand wiring is the alternative worth weighing
The obvious alternative to a DI library is no DI library. If your service has eight types and two implementations, a set of plain constructors that take each other as arguments is often clearer than a module declaration and a resolve call. The difference in approach is that Shaku moves the wiring into a single declarative place and lets the compiler check it, while manual wiring spreads the same information across constructor signatures.
The gap widens with framework code. In a Rocket, Axum or Actix handler, constructing dependencies by hand means either a wide argument list per handler or a global that every handler reaches into. A container turns that into one module and a resolve at the edge of the request, at the cost of a layer of indirection between the handler and the type it wants.
Shaku is also not the only Rust DI option, so the honest comparison is between libraries of this shape rather than between DI and no DI. What distinguishes Shaku in particular is that the graph is checked at compile time, the derive macros do most of the wiring, and the framework integrations are first class crates in the same workspace rather than separate projects. If compile time checking of your object graph is not a goal for your team, a simpler manual wiring scheme will be cheaper to live with, and the module macro will not make that decision for you.
One maintainer, an open changelog, and a 1.88.0 floor
The project's own status note is unusually blunt. The foundation of the API is in place, the stated focus is to mature the project based on user feedback, and the maintainer says he is active but does not have many major changes of his own planned. Most future changes are expected to come from user feedback.
The dates back that up. Releases are v0.6.1 on 2021-02-05, v0.6.2 on 2024-08-31 and v0.6.3 on 2026-08-02, and the last push was on 2026-08-09. The gap between the 2021 and 2024 tags is long, so the 0.6 line moves in bursts rather than on a schedule.
Two upgrade constraints follow. The minimum supported Rust version is 1.88.0, and the policy is to support the latest stable release plus the previous two versions, with changes to the minimum noted in the changelog. Pinning your toolchain file matters here. The repository also ships a Cargo.lock.msrv alongside Cargo.toml, which is the kind of file that exists precisely to make a minimum version build reproducible.
Licensing is permissive and dual: the tree carries LICENSE-APACHE and LICENSE-MIT files, with the project declared as Apache-2.0. Either licence lets you use Shaku in a proprietary product without copyleft obligations, which is what most teams adopting a DI library need to know. The documentation lives on docs.rs, with the getting started, provider and submodules guides published there.
Editorial conclusion
Shaku fits Rust services that want a single place to describe their object graph, want missing dependencies to fail the build rather than a request, and are willing to accept a single maintainer who says no major changes are planned. It does not fit a project pinned to an older toolchain, since the minimum supported Rust version is 1.88.0, nor one that expects runtime registration of arbitrary implementations. Before adopting it, check the shaku_derive macro expansion against the trait shapes you use, confirm your framework integration crate exists for your server, and read the CHANGELOG.md for the break in release cadence between v0.6.1 and v0.6.3.
Frequently asked questions
What is the difference between a Shaku component and a provider?
A Component is a singleton, so resolving it returns the same instance every time. A Provider is more like a factory, so each resolution produces a new instance.
Does Shaku need a runtime container lookup at request time?
The library describes itself as compile time dependency injection, and the module is checked while your crate builds. The integration crates for Rocket, Axum and Actix wrap a module for those frameworks.
What Rust version does Shaku require?
The minimum supported Rust version is 1.88.0, and the policy supports the latest stable release plus the previous two versions at minimum. Changes to that floor are noted in the changelog.
How do I pass configuration values into a Shaku component?
Fields without an inject attribute become parameters, and you supply them through the module builder with with_component_parameters, passing a generated parameters struct. The documentation example does this for a DateLoggerImpl with today and year.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/azuremarker-shaku)Was this useful?