Dependency Injection ContainerLoading saved progress…

Dependency Injection Container

A dependency-injection (DI) container is a registry that knows how to build your objects: you tell it how each service is made and what it depends on, and it wires the whole graph together on demand. It is the core idea behind frameworks like Angular, NestJS, InversifyJS, and Awilix. You will build a small, explicit one — no decorators, no reflection — exposed as a factory diContainer().

Signature

const container = diContainer();

container.register(name, factory, options?);
//   factory: (...deps) => instance   receives its resolved dependencies
//   options.dependencies: string[]   names to resolve and pass, in order (default [])
//   options.singleton: boolean       cache one instance? (default true)

container.registerValue(name, value); // register a constant / already-built value
container.resolve(name);              // build (or return the cached) instance
container.has(name);                  // → boolean: is `name` registered?

Examples

const c = diContainer();

c.registerValue('apiUrl', 'https://api.example.com');
c.register('http', (url) => ({ get: (path) => url + path }), {
  dependencies: ['apiUrl'],
});

c.resolve('http').get('/users'); // 'https://api.example.com/users'
c.resolve('http') === c.resolve('http'); // true — the same cached singleton
// A transient rebuilds on every resolve; a cycle is rejected, not recursed forever.
c.register('ticket', () => ({ n: Math.random() }), { singleton: false });
c.resolve('ticket') === c.resolve('ticket'); // false — a fresh object each time

c.register('a', (b) => ({ b }), { dependencies: ['b'] });
c.register('b', (a) => ({ a }), { dependencies: ['a'] });
c.resolve('a'); // throws: Circular dependency detected: a -> b -> a

Notes

  • Explicit dependencies — a factory never reaches into the container itself. It declares what it needs by name in options.dependencies, and the container passes those instances as arguments, in the order listed. This keeps every factory a pure function of its inputs.
  • Singletons are the defaultresolve caches a singleton and returns the same instance every time, including when two services depend on it (a diamond shares one instance). Pass singleton: false for a transient that is rebuilt on each resolve.
  • Lazy — nothing is constructed at register time. A factory runs only when its service is resolved, and only once for a singleton.
  • Errors are explicit — resolving an unregistered name throws a clear error, and a circular dependency throws an error naming the cycle rather than overflowing the call stack.
  • Do not worry about — async factories, child/scoped containers, auto-wiring by type, or unregistering. The four methods above are the whole surface.

FAQ

What is the difference between a singleton and a transient registration?
A singleton is built once and cached, so every resolve and every dependent shares the exact same instance. A transient is built fresh on each resolve, so two dependents each get their own instance.
How does the container detect a circular dependency?
It keeps a set of the names currently being built. Before resolving a service it checks that set; if the name is already in it, the graph has looped back on itself, so it throws instead of recursing forever.
Why does the factory receive its dependencies as arguments instead of looking them up itself?
Passing dependencies in keeps each factory a pure function of its inputs, which makes it deterministic and trivial to test in isolation. It also lets the container control lifecycle and ordering, and it avoids the reflection magic that makes some DI frameworks hard to follow.
Loading editor…