Service Container

Service Container

On this page

The container is a registry that knows how to build things, and how to hand you the same one twice when that is what you want.

What matters is the key. A reflection-based container keys on a string or a class name and resolves at runtime. Rust has no reflection, so Rainier keys on the type itself — TypeId. That is faster, and it makes an unbound service a compile-time-shaped error rather than a stringly-typed one.

use rainier_framework::prelude::*;

// Bind
app.instance(Config::new());
app.singleton(|c: &Container| Ok(PostService::new(c.resolve::<Database>()?)));

// Resolve
let service: Arc<PostService> = app.resolve::<PostService>()?;

Binding

MethodLifetime
instance(value)one value, given now
instance_arc(arc)one value you already share
singleton(factory)built once, on first resolve
scoped(factory)like a singleton, until flush_scoped()
bind(factory)rebuilt on every resolve
// A value you have.
app.instance(Argon2Hasher::new());

// Built lazily, once, and shared. The usual choice.
app.singleton(|c: &Container| {
    let db = c.resolve::<Database>()?;
    Ok(EntityRepository::<Post>::new((*db).clone()))
});

// Rebuilt every time — for something that must not be shared.
app.bind(|_: &Container| Ok(RequestScopedThing::new()));

The factory receives &Container, so a binding can resolve its own dependencies. That is constructor injection, done by hand — which in Rust is the honest version, because there is nothing to reflect on.

Binding a trait object

There is no bind(Interface::class, Implementation::class), because a trait is not a type you can key on. Bind the Arc<dyn Trait> instead:

let repository: Arc<dyn Repository<Post>> = Arc::new(EntityRepository::new(db));
app.instance(repository);

// …and resolve it as exactly that type:
let posts = app.resolve::<Arc<dyn Repository<Post>>>()?;

This is the idiom the sample project's repository provider uses. The double Arc is real — resolve always returns Arc<T>, and here T is itself Arc<dyn Repository<Post>> — but it costs one pointer hop and keeps the container's signature uniform.

Resolving

MethodOn failure
resolve::<T>()Err naming the type
try_resolve::<T>()None
expect_resolve::<T>()panics, naming the type
bound::<T>()false
let db = app.resolve::<Database>()?;             // usual
let mailer = app.try_resolve::<Mailer>();        // optional feature

The error message names the type, which is the whole point of keying on it:

Error: nothing is bound for `app::services::PostService`

The application

Application is Foundation\Application: a container plus the things a running app needs around it. It Derefs to Container, so every method above works directly on it.

let app = Application::new(".").with_environment("local");

app.instance(Config::new());
app.register(AppServiceProvider)?;
app.boot().await?;

Environment

app.environment();                              // "local"
app.environment_is(&["local", "testing"]);      // true
app.is_local();                                 // true
app.is_testing();
app.is_production();

Set from APP_ENV by the builder.

Paths

Where the application lives on disk:

app.base_path();                 // Path
app.path("src/routes");          // base + relative
app.config_path();               // base/config
app.storage_path();              // base/storage
app.resource_path();             // base/resources
app.database_path();             // base/database

Lifecycle hooks

app.booting(|app| { /* before providers boot */ });
app.booted(|app| { /* after they all have */ });
app.terminating(|app| { /* on shutdown */ });

app.terminate();                 // runs the terminating hooks
app.is_booted();                 // has boot() completed

Cycles do not deadlock

The interesting failure mode in any container is a dependency cycle. A naive implementation takes the memoization lock, calls the factory, the factory resolves the same type, and the whole thing deadlocks — a hang with no diagnostic, which is the worst possible outcome.

Rainier tracks an in-progress resolution set per thread and checks it before touching any lock:

app.singleton(|c: &Container| Ok(A(c.resolve::<B>()?)));
app.singleton(|c: &Container| Ok(B(c.resolve::<A>()?)));

app.resolve::<A>();
// Err: circular dependency while resolving `A`
flowchart TD
    A[resolve::&lt;T&gt;] --> B{T already<br/>being resolved<br/>on this thread?}
    B -->|yes| C[Err: circular dependency]
    B -->|no| D[mark T in progress]
    D --> E{shared and<br/>already built?}
    E -->|yes| F[return the Arc]
    E -->|no| G[run the factory]
    G --> H[memoize if shared]
    H --> F
    F --> I[unmark T<br/>on drop]
    C --> I

    style C fill:#633,stroke:#a66,color:#fff

The unmarking happens in a Drop guard, so a panicking factory does not poison later resolutions either. Both properties have tests: dependency_cycles_error_instead_of_deadlocking and a_panicking_factory_does_not_poison_later_resolutions.

A singleton that resolves itself is caught by the same mechanism, rather than self-deadlocking.

Scoped bindings

scoped behaves like singleton until someone calls flush_scoped(), which drops the memoized values but leaves the bindings and the true singletons alone. That is the seam for per-request or per-job state in a long-running process.

app.scoped(|_: &Container| Ok(RequestContext::new()));
// … later, at the end of the unit of work:
app.flush_scoped();

Housekeeping

app.forget::<T>();      // remove the binding entirely
app.len();              // how many bindings
app.is_empty();

Thread safety

Container is Send + Sync and every method takes &self, so an Arc<Application> is shared across every worker thread with no wrapping. Bound values must be Send + Sync + 'static — which is the compiler telling you something true about a multi-threaded server, not a limitation of the container.

When not to use it

The container is the right home for application-wide services: the database, repositories, the mailer, a client for some upstream API.

It is the wrong home for values that belong to one request. Those go in request extensions — typed, per-request, and gone when the request is.

And a service that a piece of code needs should usually arrive as a constructor argument, not be fetched from the container inside a method. A struct that resolves its own dependencies hides them from its own signature, which is the same objection as facades — for the same reason.

This page is the framework’s docs/container.md at 7b8cb15. Found a mistake? Edit it on GitHub.

Type to search 2.0.0 docs.