Introduction
In our last lesson, we delved into constructor-based dependency injection in Python, using Protocols to define abstractions and a "Composition Root" to assemble our application. This approach allowed us to decouple high-level components from the concrete details of their dependencies, fulfilling the Dependency Inversion Principle.
Today, we'll carry these exact same architectural principles into the Rust ecosystem. You will learn how to implement trait-based dependency injection to achieve the same powerful decoupling. We will see how Rust's trait system serves as the direct counterpart to Python's Protocols, providing a compile-time-enforced mechanism for defining abstractions. We'll explore the two primary methods for this in Rust: using generics for zero-cost, static dispatch and using trait objects for flexible, dynamic dispatch. This lesson will solidify your understanding of DI as a language-agnostic pattern and equip you with the idiomatic Rust techniques to build maintainable, testable applications.
The Foundation: Abstraction with Traits
Just as we began the last lesson, let's start with a clear example of the problem we're solving: tight coupling. In Rust, this often manifests as a struct directly containing or creating a concrete implementation of one of its dependencies.
SOLID Principles in Rust: A Practical Guide - 04 | 40tude
This article from 40tude provides a perfect "before" picture. It presents a simple OrderService that is tightly coupled to a concrete email::Sender.
Please read the first code example in the section The Problem. The extensive comments walk you through the issues: the high-level module (order) knows about and creates the low-level detail (email::Sender), making it impossible to swap implementations or test in isolation.
The solution, just as in Python, is to invert this dependency. The high-level OrderService should not depend on a concrete Sender; instead, both should depend on an abstraction. In Rust, this abstraction is defined using a trait.
A trait defines a contract of behavior that a type must implement. It is the Rust equivalent of an interface or, more accurately in our recent context, a Python Protocol.
SOLID Principles in Rust: A Practical Guide - 04 | 40tude
The same article immediately follows up with the refactored, decoupled solution.
Now, read the section The Solution. Pay close attention to these key changes: A Sender trait is defined in the domain module. This is our abstraction. OrderService is made generic: OrderService<S: Sender>. It no longer knows about Email but only that it has a dependency S that fulfills the Sender contract. The concrete Email struct, now in its own email module, implements the Sender trait. Notice how the dependency arrow has flipped: email now depends on domain. The main function becomes the Composition Root, creating the concrete Email service and injecting it into the OrderService.
This simple example encapsulates the entire pattern. The domain defines the "port" (the trait), and the infrastructure provides the "adapter" (the struct implementing the trait).
Constructor Injection with Generics (Static Dispatch)
The method shown above, using generics (struct OrderService<S: Sender>), is the most common and idiomatic way to perform dependency injection in Rust. It's a form of static dispatch.
When you compile this code, Rust creates a specialized version of OrderService for each concrete type you use it with (e.g., OrderService<Email>, OrderService<Sms>). This process is called monomorphization. The result is that at runtime, the method calls are direct function calls, just as if you had written the concrete type directly. This means the abstraction is "zero-cost"—it has no runtime performance overhead.
This is the recommended approach when performance is critical and you know all the dependency types at compile time.
Let's look at a more comprehensive example within a hexagonal architecture context.
rust-architecture-patterns - Skill | Smithery
This guide provides excellent, production-grade examples. We'll focus on the trait definitions, their implementations, and the generic service that uses them.
First, see how traits are defined as "Ports" in the section Ports: Trait Definitions. Notice UserRepository, EmailService, and Clock. Next, look at the concrete implementations, or "Adapters," in the section Adapters. See how PostgresUserRepository implements the UserRepository trait. Finally, study the Constructor Injection example. The UserService is generic over its three dependencies (R, E, C), each bounded by a trait. Its new function accepts these dependencies, making it perfectly testable.
The Payoff: Testability
The most significant architectural benefit of this pattern is testability. Because your services depend on traits, you can easily create mock implementations for your tests, allowing you to unit-test your business logic in complete isolation without needing a real database or network connection.
The example you just read includes a clear test case. The MockUserRepo struct is a simple, in-memory implementation of the UserRepository trait created solely for testing. In the test_register_user function, this mock repository is injected into the UserService, enabling a fast, reliable, and isolated test of the registration logic.
Constructor Injection with Trait Objects (Dynamic Dispatch)
While generics provide zero-cost abstractions, they have a trade-off: the final binary size can increase because code is duplicated for each concrete type. More importantly, they require the concrete type of the dependency to be known at compile time. What if you need more flexibility? For instance, what if you want to hold a collection of different objects that all implement the same trait, or decide at runtime which implementation to use?
This is where dynamic dispatch using trait objects comes in. A trait object, written as dyn Trait, is a way to refer to a type that implements a trait without knowing its concrete type at compile time. Because the size of the concrete type isn't known, trait objects must be used behind a pointer, such as &dyn Trait, Box<dyn Trait>, or Arc<dyn Trait>.
Box<dyn Trait>: A heap-allocated, owned pointer to a trait object.Arc<dyn Trait>: An atomically reference-counted, shared pointer. This is very common for dependencies that need to be shared across threads, as is typical in web services.
When you call a method on a trait object, the program performs a lookup in a virtual table (vtable) at runtime to find the correct function pointer. This adds a small performance overhead compared to static dispatch but provides significant runtime flexibility.
rust-architecture-patterns - Skill | Smithery
The "Rust Architecture Patterns" guide provides a clear comparison and an example of DI using trait objects.
First, review the decision table and flowchart in the section Generics vs. Trait Objects. This will help you decide which approach to use in a given situation. Next, study the code in the section Trait Object Based DI. Notice how the AppState struct holds dependencies as Arc<dyn UserRepository>. The UserService then receives a shared pointer to this AppState, allowing it to access the dependencies dynamically.
The video below also provides a clear walkthrough of this pattern, showing how traits are defined in the application layer and then injected as Arc<dyn ...> into a struct.
Practical Clean Architecture in Rust [with Axum Template]
This video on Clean Architecture in Rust demonstrates the practical use of trait objects for dependency injection.
Watch the explanation of how dependency inversion is achieved using traits (ports) here. Next, observe the code walkthrough where a UserUseCases struct is defined to take Arc<dyn UserPersistence> and Arc<dyn UserCredentialsHasher> as dependencies in this segment. Finally, see how this structure enables easy unit testing with mock implementations of the traits at this timestamp.
The Composition Root in Rust
Whether you use generics or trait objects, the fundamental principle remains: components do not create their own dependencies. This logic belongs in a single, well-defined location at the start of your application: the Composition Root. In a typical Rust application, this is your main.rs binary crate or a dedicated composition module.
This is where you instantiate your concrete infrastructure components (like database connection pools, HTTP clients) and your adapters (like PostgresUserRepository, StripePaymentGateway), and then inject them into your application services to assemble the final application state.
The following image provides a high-level visual map of this process. The ApplicationContext is analogous to our Composition Root. It's responsible for registering components, resolving their trait-based dependencies, and initializing them to get the system ready.

Here is a concrete code example of what a Composition Root looks like in main.rs.
rust-architecture-patterns - Skill | Smithery
This final section from the patterns guide shows a complete main.rs that wires up an entire application.
Read through the code in the section Composition Root Pattern. Observe how it follows a clear sequence: Concrete infrastructure (e.g., PgPool, reqwest::Client) is created. Adapters (e.g., PostgresUserRepository) are instantiated using the infrastructure components. Application services (e.g., UserService) are instantiated, with the adapters injected into them via their new functions. The final App struct, containing all the fully constructed services, is returned.
This centralization makes your application's entire dependency graph explicit and manageable in one place, which is a cornerstone of maintainable architecture.
Conclusion
In this lesson, we translated the universal pattern of Dependency Injection into idiomatic Rust. We've seen that traits are the key to defining the abstractions required by the Dependency Inversion Principle, just as Protocols are in Python.
Key Takeaways:
- Traits as Abstractions: Rust
traits define the contracts for your dependencies, allowing high-level logic to be decoupled from low-level implementation details. - Constructor Injection: Passing dependencies via a
newfunction is the most explicit and common DI method in Rust. - Static vs. Dynamic Dispatch:
- Generics (
<T: Trait>) provide static dispatch, offering zero-cost abstractions at the expense of compile-time knowledge and potentially larger binaries. This is the default choice. - Trait Objects (
dyn Trait) provide dynamic dispatch, offering runtime flexibility at the cost of a small performance overhead. This is useful for sharing dependencies across threads or when implementations aren't known at compile time.
- Generics (
- Composition Root: Assembling your application's object graph in a single, dedicated place (like
main.rs) is crucial for managing complexity and making dependencies explicit.
With a firm grasp of managing dependencies within a single codebase in both Python and Rust, you are now well-prepared for our next module. We will move up a level of abstraction to explore Codebase Structural Patterns, such as Layered, Hexagonal, and Clean Architecture. These patterns leverage the DI techniques you've just learned to organize entire applications into maintainable and scalable structures.