lilydocs
HTTP APIWebSocketConsumer
Fundamentals3 min read

Lifetimes and interfaces

Choose between Singleton, Scoped, and Transient, and resolve services through their interfaces.

A lifetime determines when a service instance is created, who shares it, and who cleans it up. Specify lifetimes explicitly in your examples.

Choose a lifetime#

Lifetime Created Shared with Cleaned up
Singleton During container build All work in the container At container shutdown
Scoped On first resolution in the scope The same request or unit of work At scope shutdown
Transient On every resolution Not shared By the resolving scope; by the container if there is no scope

If #[service] does not specify a lifetime, the default is Transient. Application resources such as connection pools can be Singleton, while a request's transaction context can be Scoped.

Dependency lifetimes#

A Singleton cannot retain a Scoped service. Inserting a Transient service in between does not remove the need for a scope: there is no active scope during startup. A direct lifetime error and an indirect resolution error do not necessarily arise in the same validation phase.

Singleton → Transient is allowed. However, that instance retained by the Singleton is tied to the root lifetime. It does not become a new instance on every use.

A scope is not an Arc counter. Moving a Scoped service's Arc outside the request does not extend the scope's lifetime. Open a separate scope for each independent unit of work.

Inject through an interface#

You can use a service through a behavioral contract instead of its concrete type. In the following snippet, Greeting is an application-defined trait, and GreetingService is its service implementing ServiceTrait:

Service registration and dependency field
#[service(interface = dyn Greeting, lifetime = "Singleton")]
// Use alongside #[derive(Injectable)] on GreetingService.

#[inject]
greeting: Arc<dyn Greeting>,

In an action, Service<dyn Greeting> resolves through the same interface registration. More than one active implementation of the same interface causes a build error. Concrete and interface resolutions follow the registered lifetime; for Transient, two resolutions do not share the same allocation.

Seed an instance in tests#

ApplicationContainer::builder().seed_singleton(value) replaces the initial instance of an already registered Singleton. It does not add a new service registration. An unknown type, a Scoped/Transient type, or repeated seeding of the same type is rejected.

Missing dependencies, cycles, duplicate interfaces, and lifetime errors are handled at startup. Use the application's existing provider instead of rebuilding the container inside an endpoint.

Sources for this page v0.1.0

This guide is based on the reviewed framework commit.

QUICK START
Documentation
Source code