ivangeorgiev.dev

Introduction to Dependency Containers: Part 2

Improving our Dependency Container with support for singletons

March 10th, 2025 3 minute read

In the last article we learned about Dependency Containers. We started building our own Dependency Container with basic support to register and resolve dependencies.
In this short article we will improve our Dependency Container by adding support for singletons.

What are singletons?
Singletons are software components (e.g. classes) from which we can create at most one instance. These types of dependencies are useful when we don't need more than one instance and we want to share the same instance between other software components.

The opposite of singleton dependencies are transient dependencies. Software components that are transient are instantiated every time they are requested from the dependency container. As a result, there can be many instances of transient software components.

This is the code we wrote last time:

function createDependencyContainer() {
  const registrations = {};
                        
  function register(id, dependencies, factory) {
    registrations[id] = {
      id,
      dependencies,
      factory
    };
  }
                                        
  function resolve(id) {
    const registration = registrations[id];
                
    if (!registration) {
      throw new Error('No registration with ID ' + id + ' found.');
    }
        
    const { dependencies, factory } = registration;
        
    if (dependencies.length === 0) {
      return factory();
    }

    const resolvedDependencies = dependencies.map(dependency => resolve(dependency));

    return factory(resolvedDependencies);
  }
                                        
  return {
    register,
    resolve
  };
}

Now we are going to add an additional parameter to the "register" function. This parameter will specify whether the dependency is singleton or transient. It will be a boolean parameter and the value "true" will mark the dependency as singleton.

function register(id, dependencies, factory, singleton) {
  registrations[id] = {
    id,
    dependencies,
    factory,
    singleton,
    instance: undefined
  };
}

In addition, we add two properties to each registration object:

Now we have to modify our "resolve" function.
When we request something from the Dependency Container we can check whether the dependency is singleton and is already instantiated. If both conditions are met, we can resolve it right away.

function resolve(id) {
  const registration = registrations[id];

  if (!registration) {
    throw new Error('No registration with ID ' + id + ' found.');
  }

  const { singleton, instance } = registration;

  if (singleton && instance) {
    return instance;
  }
}

If the conditions are not met, we have to resolve the dependency. This part is the same as before. If the requested software component has no dependencies we can create an instance right away. If it has dependencies, we have to resolve them first and then create an instance of what was requested.

function resolve(id) {
  const registration = registrations[id];

  if (!registration) {
    throw new Error('No registration with ID ' + id + ' found.');
  }

  const { singleton, instance } = registration;

  if (singleton && instance) {
    return instance;
  }

  const { dependencies, factory } = registration;
  const resolvedDependencies =
    dependencies.length === 0
      ? []
      : dependencies.map((dependency) => resolve(dependency));

  const newInstance = factory(resolvedDependencies);
}

The final part before we return the created instance is to check whether the registration is singleton. If it is, we have to save the instance so that we can return the same instance for future requests.

function resolve(id) {
  const registration = registrations[id];

  if (!registration) {
    throw new Error('No registration with ID ' + id + ' found.');
  }

  const { singleton, instance } = registration;

  if (singleton && instance) {
    return instance;
  }

  const { dependencies, factory } = registration;
  const resolvedDependencies =
    dependencies.length === 0
      ? []
      : dependencies.map((dependency) => resolve(dependency));

  const newInstance = factory(resolvedDependencies);

  if (singleton) {
    registration.instance = newInstance;
  }

  return newInstance;
}

See the following link for the full code: https://github.com/ivan-georgiev-dev/introduction-to-dependency-containers/blob/part-2/script.js.
In the next part of this series we will add detection for cyclical dependencies.

If you enjoy my articles, please consider supporting me.