I've been reading quite a lot on the concept of Inversion of Control (IoC) and Dependency Injection (DI), and I've seen that DI often relies on an IoC container, which among other roles has to

Register: The container must know which dependency to instantiate when it encounters a particular type. This process is called registration. Basically, it must include some way to register type-mapping.
Resolve: When using the IoC container, we don't need to create objects manually. The container does it for us. This is called resolution.

The above quote comes from: https://www.tutorialsteacher.com/ioc/ioc-container#:~:text=IoC%20Container%20. While I understand "Resolve", which is basically letting the IoC container create the dependency to be injected, I really cannot understand "Register". I tried my hardest to look for a language-independent explanation of registration (which is what I want, considering my limited programming experience), but I never understood the "registration" role of the container. The link above is to me one of the best links that has a language-independent overview of the IoC container, but I cannot understand it at all.

I thought the IoC container is an under-the-hood mechanism that basically creates the dependency for us whenever we include it in the constructor of the dependent class (in the constructor if we are using constructor injection).

In other words, instead of having a line of code instantiating an instance of the MsSqlConnection, as below on 'line X':

class User {
    constructor(connection: MsSqlConnection) {}

class MsSqlConnection {
    constructor() {}

const connection = new MsSqlConnection() // notice how we create the connection here; call this line X
const user = new User(connection) // line Y

we would have the IoC container do the job of creating the MsSqlConnection instance on 'line X', so that we can directly inject the connection, prepared by the IoC container, into the constructor of User on 'line Y'. From the above example, the role of the IoC container to instantiate the object, called 'resolution', is pretty clear to me. But what about 'registration'? In this simple hard-coded example where I create the MsSqlConnection myself on 'line X', I'm not doing anything remotely related to "type-mapping" and "registration" in general. At least, not that I see it.

I'd be more than grateful if someone could provide a language-independent explanation of this registration process, or one in NestJS (which I'm somewhat accustomed to).

2 Answers 2


In order to resolve a dependency, the DI engine should know how to resolve it.

Imagine you have IConnection interface, which has three implementations: MsSqlConnection, OracleDBConnection, and MockConnection.

A given DemoService needs IConnection in order to do its job. At runtime, a DI engine is asked to provide an instance of DemoService class. In order to do that, it needs to know what class should it use. Is it MsSqlConnection, OracleSqlConnection, or MockConnection?

In order to resolve the ambiguity, you should register the dependency.

In some cases, such registration is straightforward—so straightforward, actually, that some DI engines would do such registration automatically. For instance, when there is only one class which implements a given interface, a DI engine may decide that it would use this exact class every time a dependency corresponding to the specific interface is required.

In other cases, registration rules could be pretty complicated. You may, for instance, register MsSqlConnection or OracleSqlConnection based on the application configuration, and leave MockConnection for tests.

That's pretty all about registration. In essence, a bunch of classes have a dependency. Instead of hardcoding the dependency for every class, specifying which concrete class will be used, you move this logic to a central location, and leave DI to handle the actual injection based on those rules. The fact of telling the DI about this logic is the actual registration.


The missing cog

The underlying issue in this question is the same as another question of yours I answered yesterday on the same topic of DI: you are not thinking about polymorphism here, and polymorphism is a huge part of what makes DI so powerful.

To very quickly repeat the example of a barista who is only able to make coffee in a cardboard cup, you're thinking of injecting a concrete type:

For the example, ignore the distinction between constructor DI and method DI, this is irrelevant for the example at hand. For DI containers, it centers around constructor DI specifically, but this example is kept much easier to read when using method DI.

class BadBarista {
    makeEspresso(cup: CardboardCup) {
        let coffee = new Coffee();


        return cup;

But you want your barista to make a coffee in any cup that is handed to them, because customers like choosing their own cup. Therefore, you cannot use the concrete CardboardCup type as the dependency type. You will have to use something more generalized, e.g. an ICup interface or a BaseCup base class. I'm using ICup here for the example.

class GoodBarista {
    makeEspresso(cup: ICup) {
        let coffee = new Coffee();


        return cup;

For your specific example, this means that your injected dependency should be an interface, e.g. IDatabaseConnection, instead of a concrete type such as MsSqlConnection

class User {
    constructor(connection: IDatabaseConnection) {}

interface IDatabaseConnection {
    // ...

class MsSqlConnection implements IDatabaseConnection {
    // ...

class MongoDbConnection implements IDatabaseConnection {
    // ...

Most of this so far has been repetition of the answer I wrote yesterday, but I'm adding it here for completeness' sake.

This part is what you're missing to really understand the role DI plays, and how you work with it. Now, with this new information in mind, let's look back at your confusion about "registration".

So what is registration?

we would have the IoC container do the job of creating the MsSqlConnection instance

When you don't register anything in the IOC container, how does the IOC know what dependencies your User class needs? The only information it can find is by looking in the User constructor and figuring out the type of the constructor arguments.

So the IOC container knows that User needs an IDatabaseConnection. But which implementation of IDatabaseConnection? MsSqlConnection? MongoDbConnection? How does the IOC container know which one to choose?

This is where registration comes into play. At its very core, registration means mapping a general constructor parameter type (IDatabaseConnection) to a concrete type (e.g. MsSqlConnection). A simple example (with C# syntax):

services.AddTransient<IDatabaseConnection, MsSqlConnection>();

This registration is the coding equivalent of telling the IOC container:

"Hey, whenever you're instantiating an object, and it needs a dependency of type IDatabaseConnection, I want you to very specifically use a MsSqlConnection for that."

If, for example, tomorrow we want to start using MongoDbConnection instead, all we have to do is edit that one registration:

services.AddTransient<IDatabaseConnection, MongoDbConnection>();

And with that one change, this means that all classes with an IDatabaseConnection dependency will get a MongoDbConnection injected instead of the old MsSqlConnection. This makes it very easy for even large enterprise application to very quickly switch between different components without needing to revisit all of the existing code.

That is all there is to it.

Of course, there are a lot of ways to further configure the registration when you need to, but the vast majority of IOC registrations tends to be as simple as the example I just gave. It's a simple base-to-concrete-type mapping that the container needs to know in order to always inject the dependency you want to inject.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.