Dependency injection without a framework in Rust means manually wiring up your application’s components, letting you precisely control how services and their dependencies are created and provided.

Here’s a look at a simple web server that uses dependency injection to manage its database connection and configuration:

use std::sync::Arc;
use tokio::net::TcpListener;
use axum::{
    routing::get,
    Router,
    extract::State,
};

// Our "service" that needs a database connection
struct UserService {
    db_pool: Arc<sqlx::PgPool>,
}

impl UserService {
    // Constructor takes the dependency
    fn new(db_pool: Arc<sqlx::PgPool>) -> Self {
        UserService { db_pool }
    }

    async fn get_user(State(db_pool): State<Arc<sqlx::PgPool>>, user_id: i32) -> String {
        // In a real app, you'd query the database here
        println!("Querying database for user {}", user_id);
        format!("User data for {}", user_id)
    }
}

// Application state that holds our services
struct AppState {
    user_service: UserService,
}

#[tokio::main]
async fn main() {
    // 1. Setup dependencies
    let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    let db_pool = sqlx::PgPool::connect(&database_url)
        .await
        .expect("Failed to create pool.");

    // Wrap in Arc for shared ownership across threads
    let db_pool_arc = Arc::new(db_pool);

    // 2. Instantiate services, injecting dependencies
    let user_service = UserService::new(db_pool_arc.clone()); // Injecting the pool

    // 3. Assemble application state
    let app_state = AppState {
        user_service,
    };

    // 4. Create the Axum router, passing the state
    let app = Router::new()
        .route("/users/:id", get(UserService::get_user))
        .with_state(app_state); // Passing the entire state

    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    println!("Server listening on http://127.0.0.1:3000");
    axum::serve(listener, app).await.unwrap();
}

The core problem dependency injection solves is the "hardcoded dependency." Imagine UserService directly creating its own sqlx::PgPool. If you wanted to test UserService with a mock database, or switch to a different database backend, you’d have to change UserService’s code. Dependency injection, by having UserService::new accept the PgPool, decouples the creation of the dependency from its usage.

In this example, the db_pool is the dependency. UserService needs access to it. We create the PgPool once in main, wrap it in an Arc (Atomic Reference Counted pointer) to allow safe sharing across multiple threads and copies, and then inject this Arc<PgPool> into UserService when we create it. The AppState struct then holds our UserService. Axum’s with_state mechanism allows us to pass this entire AppState to our handlers, where State(db_pool) extracts the shared database pool.

The key levers you control are:

  1. Instantiation Location: Where do you create the concrete instances of your dependencies? In this example, it’s main.
  2. Injection Method: How do you pass the dependency? Here, it’s constructor injection (UserService::new(db_pool)). Other methods include setter injection or passing via function arguments.
  3. Scope and Lifetime: How long does the dependency live, and how is it shared? Arc gives us shared ownership and a lifetime tied to the application’s execution.

The most surprising thing about manual dependency injection in Rust is how much of the "framework’s" work you can replicate with just Arc and careful struct design. The Arc is crucial because it allows multiple parts of your application (like different request handlers in a web server) to hold a reference to the same underlying resource (the database connection pool) without needing to clone the resource itself, which would be expensive and incorrect for shared state. It’s a thread-safe, reference-counted smart pointer that ensures the resource is only deallocated when the last Arc pointing to it goes out of scope.

The next step is often managing multiple, more complex, and potentially conditionally created dependencies, which leads to exploring patterns like builder patterns for your AppState or looking into crates like shaku or di for more structured approaches.

Want structured learning?

Take the full Rust course →