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();