Architecture / IoC Container

The Service Container

Archery includes a lightweight, hierarchical Dependency Injection (DI) container that manages the lifecycle and resolution of your application's services. It allows you to build modular, testable, and loosely coupled applications.

Core Concepts

The container acts as a central registry for your application's parts. Instead of manually instantiating classes, you register "factories" or "instances" with the container and let it resolve dependencies for you.

Registration Types

Archery support three primary ways to bind services:

1. Transient Bindings (bind)

A new instance is created every time the service is requested.

container.bind<Logger>(factory: (c, _) => ConsoleLogger());

final logger1 = container.make<Logger>(); // New instance
final logger2 = container.make<Logger>(); // Another new instance

2. Singleton Bindings (singleton)

The same instance is returned for every request within the same scope.

container.singleton<Database>(factory: (c, _) => Database.connect());

final db1 = container.make<Database>();
final db2 = container.make<Database>(); // Same instance as db1
  • Lazy (Default): Instance is created only when first requested.
  • Eager: Instance is created immediately during container.initialize().
    container.singleton<Config>(factory: (c, _) => Config.load(), eager: true);
    

3. Instance Bindings (bindInstance)

Binds an already existing instance directly to the container.

final app = App();
container.bindInstance<App>(app);

Service Resolution

make<T>()

Resolves and returns an instance of type T. Throws an exception if not registered.

final router = container.make<Router>();

tryMake<T>()

Safely attempts to resolve a service. Returns null if not found.

final logger = container.tryMake<Logger>();

Named Registrations and Options

If you have multiple implementations of the same type, use Named Registrations.

// Registration
container.bind<Storage>(name: 's3', factory: (c, _) => S3Storage());
container.bind<Storage>(name: 'local', factory: (c, _) => LocalStorage());

// Resolution
final storage = container.make<Storage>(name: 's3');

You can also pass runtime options to factories:

container.bind<Client>(factory: (c, options) => Client(baseUrl: options?['url']));
final client = container.make<Client>(options: {'url': 'https://api.example.com'});

Scopes

Scopes allow you to isolate singletons and bindings for specific tasks, such as a single HTTP request. A child scope inherits all bindings from its parent but can have its own overrides.

final requestScope = container.newScope();
requestScope.bindInstance<Request>(currentRequest);

// Resolves from scope if present, otherwise falls back to parent
final req = requestScope.make<Request>(); 

Lifecycle Management

Initialization

If you use eager singletons, you must call initialize():

await container.initialize();

Disposal

The container can manage the cleanup of its services. Register a disposal callback:

container.onDispose(() async => await db.close());

// Triggers all registered disposers in reverse order
await container.dispose();