ivangeorgiev.dev

Introduction to Dependency Containers: Part 1

Learn about Dependency Containers in an intuitive way

February 21th, 2025 5 minute read

In the last article we learned about Dependency Injection (DI for short). We said that DI is a mechanism that removes the burden of dependency creation from software components (e.g. classes). As the burden is removed from one place it has to be placed in another. That place are the Dependency Containers.
In this article we will learn about Dependency Containers and we will build one ourselves!

Dependency Containers usually provide a way to:

  1. Register a software component with the Dependency Container. This allows the Dependency Container to provide the software component as a dependency to other software components.
  2. Request a software component from the Dependency Container.

The methods that correspond to these features are usually called:

  1. register
  2. resolve

Let's start building our Dependency Container. We will be doing so in the JavaScript language.

We define a function that returns an object with two properties. These two properties are functions that allow us to register and resolve software components.

function createDependencyContainer() {
  function register() {}

  function resolve() {}

  return {
    register,
    resolve
  };
}

When we register a software component with the Dependency Container we need to specify a unique identifier for the software component. We will use string values for the identifiers.
We also need to specify the dependencies of the software component we want to register. We will use string arrays to specify the dependencies. The strings will correspond to the identifiers of other software components registered in the Dependency Container.
Finally, we need to tell the Dependency Container how to construct an instance of the software component. We will use a function for this. The function will have a single parameter and its value will be an array of the resolved dependencies of the software component.
The code with the modified signature of the "register" function becomes:

function createDependencyContainer() {
  function register(id, dependencies, factory) {}
        
  function resolve() {}
        
    return {
      register,
      resolve
    };
}

Let's add an object that will keep track of the software components registered in the Dependency Container. The object keeps track of the registrations by mapping the identifiers to objects that contain information about the registered software component.

function createDependencyContainer() {
  const registrations = {};

  function register(id, dependencies, factory) {
    registrations[id] = {
      id,
      dependencies,
      factory
    };
  }
                
  function resolve() {}
                
  return {
    register,
    resolve
  };
}

Let's move on to the "resolve" function. It will accept a single parameter related to the identifier of the software component that we want to resolve. We will throw an error if the Dependency Container has no registration with that identifier.

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.');
    }
  }
                        
    return {
      register,
      resolve
    };
}

If the Dependency Container has a registration with the specified identifier we have to create an instance of the software component. There are two possibilities:

  1. The requested software component has no dependencies.
  2. The requested software component has dependencies.

In case there are no dependencies, we simply call the factory function to create an instance of the software component.

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();
    }
  }
                                
  return {
    register,
    resolve
  };
}

In case there are dependencies, we have to resolve them. We do this by calling the "resolve" function for every dependency. After we resolve the dependencies, we call the factory function with an array of the resolved dependencies.

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
  };
}

See the following link for an example of how our Dependency Container is used: https://github.com/ivan-georgiev-dev/introduction-to-dependency-containers/blob/part-1/script.js

This Dependency Container can be improved. For example, it always creates new instances of the dependencies and we could improve it by allowing the option to reuse already created instances.
We could improve it by allowing the option to manage singleton dependencies (i.e. limit to one instance).
We could improve it by adding guards against cyclical dependencies (e.g. A depends on B and B depends on A).
We will tackle these improvements in the next parts of this series.

If you enjoy my articles, please consider supporting me.