Skip to main content
๐Ÿ“œ WAYPOINT LESSON

A Tiny Container: Lifetimes and the Composition Root

โญโญ intermediateโณ 18 min read๐Ÿ“ Lesson 121 of 265

Registration, resolution, singleton vs transient, and why the app wires itself in exactly one place.

Service lifetimes

  • SINGLETON โ€” one instance for the whole process (stateless services, caches). Thread-safety is now the implementation's problem.
  • TRANSIENT โ€” a new instance per resolution (stateful per-operation services, fakes). Cheap to create.
  • (Scoped โ€” one instance per logical unit of work โ€” matters with a web framework and a request pipeline; revisit it in Advanced/API work.)

The hand-rolled container

With no NuGet, the pattern behind every DI container is small enough to build โ€” a registry of factory functions:

public sealed class Container
{
    private readonly System.Collections.Generic.Dictionary<Type, Func<object>> _factories = new();
    private readonly System.Collections.Generic.Dictionary<Type, object> _singletons = new();

    public void Register<TService>(Func<TService> factory) =>
        _factories[typeof(TService)] = () => factory();

    public void RegisterSingleton<TService>(Func<TService> factory)
    {
        object? instance = null;
        _factories[typeof(TService)] = () =>
        {
            if (instance is null) instance = factory();   // lazy, once
            return instance;
        };
    }

    public TService Resolve<TService>() => (TService)_factories[typeof(TService)]();
}

Real containers add constructor discovery via reflection and lifetime scopes โ€” but registration/resolution/lifetimes are the concepts.

The composition root

Wiring lives in ONE place, at the application's entry point:

var container = new Container();
container.RegisterSingleton<IEmailer>(() => new SmtpEmailer(config));
container.RegisterTransient<IOrderService>(() => new OrderService(
    container.Resolve<IEmailer>(), container.Resolve<IClock>()));
var svc = container.Resolve<IOrderService>();   // app runs from here down

Business classes never touch the container; they receive what they need. When the provider changes, exactly one file changes.

Check your understanding

  • Transient or singleton for a stateful per-request service? (Transient.)
  • Where does new SmtpEmailer() belong? (The composition root โ€” nowhere else.)

โšก Now practice

Ready to Code
DI Practice: Fakes, Lifetimes, WiringBreak a hardwired dependency with an interface, prove container lifetimes, and build the composition root.
3 challenges ยท ยท ~30 min