Volga
Home
API Docs
GitHub
  • English
  • Русский
Home
API Docs
GitHub
  • English
  • Русский
  • Home
  • Basics

    • Quick Start
    • Route Parameters
    • Query Parameters
    • Route Groups
    • Headers
    • Basic Middleware
  • Data Formats

    • Handling JSON
    • Handling Form Data
    • Working with Files
    • Server-Sent Events (SSE)
  • Protocols

    • HTTP/1 and HTTP/2
    • HTTPS
    • WebSockets & WebTransport
  • Advanced

    • Custom Middleware
    • Response Compression
    • Request Decompression
    • Global Error Handling
    • Dependency Injection
    • Tracing & Logging
    • Static Files
    • CORS (Cross-Origin Resource Sharing)
    • Cookies
    • Authentication and Authorization
    • Request cancellation
    • Custom Handling of HEAD, OPTIONS, and TRACE Methods

Dependency Injection

Volga supports robust dependency injection (DI) with three lifetimes: Singleton, Scoped, and Transient. These lifetimes allow you to manage the lifecycle of your dependencies effectively.

If you're not using the full feature set, ensure you enable the di feature in your Cargo.toml:

[dependencies]
volga = { version = "0.7.0", features = ["di"] }

Dependency Lifetimes

Singleton

A Singleton ensures a single instance of a dependency is created and shared for the entire lifetime of your web application. This instance is thread-safe and reused concurrently across threads.

Example: Singleton Dependency

use volga::{App, di::Dc, ok, not_found};
use std::{
    collections::HashMap,
    sync::{Arc, Mutex},
};

#[derive(Clone, Default)]
struct InMemoryCache {
    inner: Arc<Mutex<HashMap<String, String>>>,
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    // Register a singleton service globally
    app.add_singleton(InMemoryCache::default());

    // Inject the shared cache instance into the route handlers
    app.map_get("/user/{id}", |id: String, cache: Dc<InMemoryCache>| async move {
        let user = cache.inner.lock().unwrap().get(&id);
        match user {
            Some(user) => ok!(user),
            None => not_found!("User not found"),
        }
    });

    app.map_post("/user/{id}/{name}", |id: String, name: String, cache: Dc<InMemoryCache>| async move {
        cache.inner.lock().unwrap().insert(id, name);
        ok!()
    });

    app.run().await
}

In this example:

  • The add_singleton method registers an InMemoryCache instance as a singleton.
  • The Dc<T> extractor provides access to the dependency container, resolving the dependency as needed.
  • The Dc<T> behaves similarly to other Volga extractors, such as Json<T> or Query<T>.

Info

T must be Send and Sync.

Scoped

A Scoped dependency creates a new instance for each HTTP request. The instance persists for the duration of the request, ensuring isolation between requests.

Example: Scoped Dependency

use volga::{App, di::{Container, Dc, Error, Inject}, ok, not_found};
use std::{
    collections::HashMap,
    sync::{Arc, Mutex},
};

#[derive(Clone)]
struct InMemoryCache {
    inner: Arc<Mutex<HashMap<String, String>>>,
}

impl Inject for InMemoryCache {
    fn inject(_container: Container) -> Result<Self, Error> {
        Ok(Self { inner: Default::default() })
    }
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    // Register a scoped service
    app.add_scoped::<InMemoryCache>();

    // Inject a request-specific cache instance
    app.map_get("/user/{id}", |id: String, cache: Dc<InMemoryCache>| async move {
        let user = cache.inner.lock().unwrap().get(&id);
        match user {
            Some(user) => ok!(user),
            None => not_found!("User not found"),
        }
    });

    app.map_post("/user/{id}/{name}", |id: String, name: String, cache: Dc<InMemoryCache>| async move {
        cache.inner.lock().unwrap().insert(id, name);
        ok!()
    });

    app.run().await
}

Key differences from Singleton:

  • The add_scoped::<T>() method registers a dependency that is instantiated lazily for each request.
  • Each request gets its own, unique instance of InMemoryCache.

Registering with Default or a Factory

To use the add_scoped::<T>() method, the type must implement the Inject trait. This is a convenient and powerful approach when your type depends on other services registered in the DI container.

However, if the type has no dependencies, you can register it more directly using a factory:

#[derive(Clone)]
struct InMemoryCache {
    inner: Arc<Mutex<HashMap<String, String>>>,
}

// Register a scoped service using a factory
app.add_scoped_factory(|| InMemoryCache {
    inner: Default::default(),
});

Tips

You may use the Dc<T> or Container as a factory arguments for better control on dependency resolution.

If the type implements Default, you can simplify this further by using add_scoped_default::<T>():

#[derive(Default, Clone)]
struct InMemoryCache {
    inner: Arc<Mutex<HashMap<String, String>>>,
}

app.add_scoped_default::<InMemoryCache>();

Transient

A Transient dependency creates a new instance every time it is resolved, regardless of the request scope or context. You can register a transient service using one of:

  • add_transient::<T>()
  • add_transient_factory::<T>()
  • add_transient_default::<T>()

The behavior is similar to Scoped, with the key difference that a new instance is created for every injection, not once per request or scope.

DI in middleware

If you need to request/inject a dependency in middleware, if you're using method with(), you may leverage the Dc extractor similarly to request handlers. For the wrap() use either resolve::<T>() or resolve_shared::<T> methods of HttpContext. The main difference between them is that the first one requires to implement the Clone trait for T while the latter returns an Arc<T>.

// using .wrap()
app.wrap(|ctx: HttpContext, next: NextFn| async move {
    let cache = ctx.resolve::<InMemoryCache>()?;
    // do something....
    next(ctx).await
});

// using .with()
app.with(|cache: Dc<InMemoryCache>, next: Next| async move {
    // do something....
    next.await
});

Summary

  • Singleton: Shared instance across the entire application lifecycle.
  • Scoped: New instance for each HTTP request.
  • Transient: New instance for every injection request.

For more advanced examples, check out the this.

Last Updated: 11/15/25, 10:04 AM
Prev
Global Error Handling
Next
Tracing & Logging