Dependency Injection: Everything You Need to Know

Dependency injection is a technique in which you make the interactions between objects as minimal as possible through specific dependencies. Here’s what to know.  

Written by Nick Hodges
Dependency Injection: Everything You Need to Know
Image: Shutterstock / Built In
Brand Studio Logo
UPDATED BY
Matthew Urwin | Jun 10, 2024

Dependency injection (DI) is a software engineering technique that involves making the interactions between objects as thin as possible through specific dependencies. This allows for loosely coupled code — or code that only depends on the required portion of a separate class — to run. And it reduces hard-coded dependencies and allows for cleaner, more flexible code.

To illustrate this concept, let’s say you are ready to check out at the supermarket. Do you hand the cashier your wallet and have them dig around for what you owe? Of course not. You either give them some cash or swipe your credit card. You keep the interface between you and the clerk to a minimum. That’s the essence of dependency injection: If you need something in your code, ask for it to do that and nothing else.

Dependency injection is about injecting dependencies rather than creating them — a simple yet powerful concept that underpins better coding practices. It’s about coding against abstractions, asking for what you need and embracing loosely coupled code. Though it requires thoughtful planning and design, the effort pays off in making code maintenance significantly easier and more efficient in languages like Java, C#, C++ and PHP.

 

A tutorial on dependency injection. | Video: CodeAesthetics

What Is Dependency Injection?

A dependency is anything that a given class needs to do its job, such as fields, other classes, etc. If ClassA needs ClassB to be present to compile, then ClassA is dependent on ClassB. In other words: ClassB is a dependency of ClassA. Dependencies get created when, well, you create one. 

Here’s an example:

class ClassB {

}

class ClassA {
  private _ClassB: ClassB;

  constructor() {
    this._ClassB = new ClassB();
  }
}

In the code above, we have created a hard-coded dependency to ClassB in ClassA. It’s hard-wired into the class. ClassA is completely dependent on ClassB. ClassB is coupled to ClassA. Tightly coupled. About as tightly coupled as you can get. And tight coupling is bad. 

What Is Coupling?

Coupling is the notion of one thing being dependent on another. Tight coupling is when things really depend on each other and are strictly tied. You can’t compile ClassA without the complete definition of ClassB being present. B is permanently stuck to A. 

Tight coupling is bad because it creates inflexible code. Think how hard it would be to move around if you were handcuffed to another person. That’s how a class feels when you create hard-coded dependencies.

It’s best to keep the coupling between your classes and modules as “loose” as possible. That is, you want your dependencies to be as thin as you can make them. If you have a class that needs a customer name, pass in just the customer name. Don’t pass the entire customer. And as we shall see, if your class needs another class, pass in an abstraction, an interface usually of that class. An interface is like a wisp of smoke. There is something there, but you really can’t grab onto it. 

How to Inject a Dependency

To inject a dependency, do not create ClassB inside ClassA. Instead, inject the dependency via the constructor:

class ClassB {

}

class ClassA {
  private _ClassB: ClassB;

  constructor(aClassB: ClassB) {
    this._ClassB = aClassB;
  }
}

At this point, we have two types of coupling: one where the first class creates an instance of the second class and one where the first class needs the second class (i.e., where it is injected). The latter is preferable to the former in that it involves less (looser) coupling.

This is the essence of dependency injection. Inject your dependencies instead of creating them. If you understand the notion that you should code against abstractions and that you should ask for the functionality that you need, you are well on your way to understanding dependency injection and writing better code. Dependency injection is a means to an end, and that end is loosely coupled code.

More on Software DevelopmentHow to Write Pythonic Code

  

Components of Dependency Injection

Dependency injection involves four types of classes that each play a role in executing the technique: 

  • Client: The client is the class that defines an interface and what abilities or resources they need. As a result, clients don’t create dependencies.
  • Service: The service is the class that applies the interface with the specific abilities or resources requested by the client.  
  • Interface: The interface describes the abilities or resources the service needs to deliver. Interfaces enable loose coupling and flexible code since any service that applies the interface can collaborate with the client.
  • Injector: The injector is the class that inserts the service into the client. It regulates services, making sure clients receive the right abilities or resources at the right time. 

 

3 Types of Dependency Injection

There are three common types of dependency injection that you should be familiar with: 

  1. Constructor injection
  2. Property injection
  3. Method injection 

1. Constructor Injection

Constructor injection is the process of using the constructor to pass in the dependencies of a class. The dependencies are declared as parameters of the constructor. As a result, you cannot create a new instance of the class without passing in a variable of the type required by the constructor.

That last point is key. When you declare a dependency as a parameter on the constructor, you are saying, “I’m sorry, folks, but if you want to create this class, you must pass in this parameter.” Thus, a class is able to specify the dependencies that it requires and be guaranteed that it will get them. You can’t create the class without them. If you have this code: 

class BankingService {}

class PayrollSystem {
  private _BankingService: BankingService;

  constructor(aBankingService: BankingService) {
    this._BankingService = aBankingService;
  }
}

You can’t create a PayrollSystem without passing it an instance of BankingService. Well, sadly, you can pass null, but we’ll deal with that in a minute. PayrollSystem very clearly declares that it requires a BankingService, and users of the class must supply one.

Never Accept Null

As I mentioned, it’s unfortunate that the above class can and will take null as a parameter. I say “take” because, while a user of the class can pass in null, the class itself doesn’t have to accept null.

In fact, I argue that all methods should explicitly reject null as a value for any reference parameter at any time, including constructors and regular methods. At no time should a parameter be allowed to be null without the method raising an exception. If you pass null to the PayrollSystem above and the class tries to use it, an error will occur. And errors are bad. They should — and can — be avoided.

The above code really should look something like this:

class BankingService {}

class PayrollSystem {
  private _BankingService: BankingService;

  constructor(aBankingService: BankingService) {
    if (!aBankingService) {
      throw new Error('How dare you pass me a null parameter!!!')
    }
    this._BankingService = aBankingService;
  }
}

This code will never allow the internal field to be null. It will raise an exception if someone dares pass in null as the value for the constructor’s parameter. This is how it should be. You could accept null, but then you’d have to check for it everywhere in your code.  

Checking for null is boilerplate code. Protecting against null being passed as a parameter is called the guard pattern, and you can write code to guard against null being passed to your methods. Here’s a basic implementation of a guard function:

function CheckNeitherNullNorUndefined<T>(value: T | undefined | null): void {
  if (<T>value == undefined || <T>value == null) {
    throw new Error('Guard Failure: null was passed.');
  }
}

The guard pattern actually is defined as any Boolean expression that must evaluate to true before the program execution can continue. It is usually used to ensure that certain preconditions are met before a method can continue, ensuring that the code that follows can properly execute. Checking that a reference isn’t null is probably the most common, but not the only, use for the guard pattern.

In the case at hand, we are using the guard pattern to protect against a parameter being null, so we can use the guard pattern to simplify our code:

class PayrollSystem {
  private _BankingService: BankingService;

  constructor(aBankingService: BankingService) {
    CheckNeitherNullNorUndefined(aBankingService);
    this._BankingService = aBankingService;
  }
}

When to Use Constructor Injection

You should use constructor injection when your class has a dependency that the class requires in order to work properly. If your class cannot work without a dependency, then inject it via the constructor. If your class needs three dependencies, then demand all three in the constructor.

Additionally, you should use constructor injection when the dependency in question has a lifetime longer than a single method. Dependencies passed into the constructor should be useful to the class in a general way, with its use spanning multiple methods in the class. If a dependency is used in only one spot, method injection (covered below) might be a better choice.

Constructor injection should be the main way that you do dependency injection. It’s simple: A class needs something and thus asks for it before it can even be constructed. By using the guard pattern, you can use the class with confidence, knowing that the field variable storing that dependency will be a valid instance. Plus, it’s really simple and clear to do.

Constructor injection should be your go-to dependency injection technique for clear, decoupled code. But it shouldn’t be the only tool in the toolbox. 

2. Property Injection

Sometimes a class has a dependency that isn’t strictly required but is indeed used by the class. An example might be a document class that may or may not have a grammar checker installed. If there is one, great, the class can use it. If there isn’t one, great, the class can include a default implementation as a placeholder.

The solution here is property injection (also known as setter injection). You add a property to your class that can be set to a valid instance of the class in question. Since the dependency is a property, you can set it as desired. If the dependency is not wanted or needed, you can leave the property as is.

Your code should act as if the dependency is there, so you should provide a do-nothing default implementation so that the code can still be run with or without a real dependency. Remember, we never want anything to be null, so that default implementation should be valid. If the user of the class wants to provide a working implementation, they can. If not, there is a working default that will allow the containing class to still function. 

When to Use Property Injection

Use property injection when a dependency is optional and/or when a dependency can be changed after the class is instantiated. Use it when you want users of the containing class to be able to provide their own implementation of the interface in question. You should only use property injection when you can provide a default implementation of the interface in question. 

Any default implementation will likely be a non-functional implementation. But it doesn’t have to be. If you want to provide a working default implementation, that’s fine. However, be aware that by using property injection and creating that class in the containing object’s constructor, you are coupling yourself to that implementation. 

Property Injection Example

Let’s take a look at code that does what I described above — a document class that has an optional grammar checker.

First, we’ll start with an interface:

interface GrammarChecker {
  CheckGrammar(): void;
}

Now, we’ll implement it twice: once as a do-nothing default and again as a real grammar checker.

  class defaultGrammarChecker implements IGrammarChecker {
    public CheckGrammar() {
      console.log('Do Nothing');
    }
  }

  class realGrammarChecker implements IGrammarChecker {
    CheckGrammar() {
      console.log('Grammar has been checked');
    }
  }

Both of the implementations just log to the console, even the non-op implementation. I merely wanted to make sure that things were all working properly. Again, defaultGrammarChecker is meant to be a non-operational, default implementation that will keep us from having to check for null all the time.

Now, we need a class that has a property on it for the grammar checker.

class Document {
    private _text: string;

    private _grammarChecker: IGrammarChecker;

    set grammarChecker(value: IGrammarChecker) {
      CheckNeitherNullNorUndefined(value);
      this._grammarChecker = value;
    }

    public constructor(aText: string) {
      this._text = aText;
      this._grammarChecker = new defaultGrammarChecker();
    }

    set text(value: string) {
      this._text = value;
    }

    get text(): string {
      return this._text;
    }

    public CheckGrammar() {
      this._grammarChecker.CheckGrammar();
    }
  }

Here are some things to note about this code:

  • Its constructor takes the document text as a parameter. It’s then exposed as a read/write property, so you can access and change it if you want.
  • The constructor also creates an instance of the default grammar checker. Note again that this creates a hard-coded dependency — one of the perils of property injection. But the dependency is a do-nothing default and prevents us from having to constantly check for null.
  • The setter for the grammarChecker property contains a guard call, ensuring that the internal value _grammarChecker can never be null.
  • One further thing to note is that the grammarChecker property is a write-only property. That is, you can only set the value and never actually read it externally. You might create a write-only property when the value being written is only used internally and will never be called by code outside of the class.

Here’s some code that exercises everything and shows property injection in action:

  var document: Document = new Document('This is the document text.');
  
  console.log(document.text);
  // Use the default, no-op grammar checker
  document.CheckGrammar();
  // Change the dependency to use the "real" grammar checker
  document.grammarChecker = new realGrammarChecker();
  // Now the grammar checker is a "real" one
  document.CheckGrammar();

Here are things to note about the above code:

  • It creates a document, taking some text as a constructor parameter.
  • It calls CheckGrammar, but the default grammar checker doesn’t do anything, so it says so in the console.
  • But then we use property injection to inject a real grammar checker, and when we call CheckGrammar, the grammar gets checked for real.

Property injection allows you to provide optional dependencies. It also allows you to change a dependency if required. For instance, your document class may take texts from different languages and require that the grammar checker changes as the document’s language changes. Property injection will allow for this. 

3. Method Injection

What if the dependency that your class needs is going to be different much of the time? What if the dependency is an interface, and you have several implementations that you may want to pass into the class? You could use property injection, but then you’d be setting the property all the time before calling the method that utilized the frequently changing dependency, setting up the possibility of temporal coupling.

Temporal coupling is the idea that the order of execution must occur in a specific way for things to work correctly.

Constructor and property injection are generally used when you have one dependency that isn’t going to change often, so they aren’t appropriate for use when your dependency may be one of many implementations.

This is where the third type of dependency injection — method injection — comes in.

Method injection allows you to inject a dependency right at the point of use so that you can pass any implementation you want without having to worry about storing it for later use. It’s often used when you pass in other information that needs special handling. For example:

interface IFoodPreparer {
    prepareFood(aRecipe: Recipe);
  }

  class Baker implements IFoodPreparer {
    prepareFood(aRecipe: Recipe) {
      console.log('Use baking skills to do the following: ' + aRecipe.Text);
    }
  }

  class ShortOrderCook implements IFoodPreparer {
    prepareFood(aRecipe: Recipe) {
      console.log('Use the grill to do the following: ' + aRecipe.Text);
    }
  }

  class Chef implements IFoodPreparer {
    prepareFood(aRecipe: Recipe) {
      'Use well-trained culinary skills to prepare the following: ' +
        aRecipe.Text;
    }
  }

  class Restaurant {
    private _name: string;
    constructor(aName: string) {
      this._name = aName;
    }

    set Name(value: string) {
      this._name = value;
    }

    get Name(): string {
      return this._name;
    }

    MakeFood(aRecipe: Recipe, aPreparer: IFoodPreparer) {
      aPreparer.prepareFood(aRecipe);
    }
  }

Here we have the notion of a recipe, which may require a different preparer depending on that recipe. Only the calling entity will know what the proper preparer type will be for a given recipe. For instance, one recipe might require a short-order cook, and another recipe might require a baker or a chef. We don’t know when writing the code what kind of IFoodPreparer will be needed, so we can’t really pass the dependency in the constructor and be stuck with that one implementation.

It is also clumsy to set a property every time a new or different IFoodPreparer is required. And setting the property in such a way induces temporal coupling and will suffer from thread-safety issues because it would require a lock around the code in a threaded environment.

The best solution is to just pass the IFoodPreparer into the method at the point of use.

Method injection should be used when the dependency could change with every use, or at least when you can’t be sure which dependency will be needed at the point of use.

Here’s an example of using method injection when the dependency needs to change every time it is used. Imagine a situation where a car-painting robot requires a new paint-gun tip after every car it paints. You might start out like this, using constructor injection:

interface IPaintGunTip {
    SprayCar(aColor: string);
  }

  class PaintGunTip implements IPaintGunTip {
    SprayCar(aColor: string) {
      console.log('Spray the car with ', aColor);
    }
  }

  class CarPaintingRobot {
    private _paintGunTip: IPaintGunTip;

    constructor(aPaintGunTip: IPaintGunTip) {
      CheckNeitherNullNorUndefined(aPaintGunTip);
      this._paintGunTip = aPaintGunTip;
    }

    paintCar(aColor: string) {
      this._paintGunTip.SprayCar(aColor);

      // what now?  The tip is no good, how do we get a new one?
    }
  }

When we paint the car, we have to get a new paint gun tip. But how? When we paint the car, the tip is no good anymore, but it’s an interface, and we have no way to manually free it. Even if we did, what would we do the next time we need to paint a car? We don’t know what kind of tip is needed for a given car, and if we have properly separated our concerns, we don’t even know anything about creating a new tip. What to do? Use method injection instead:

  class CarPaintingRobotWithMethodInjection {
    public PaintCar(aColor: string, aPaintGunTip: IPaintGunTip) {
      CheckNeitherNullNorUndefined(aPaintGunTip);
      aPaintGunTip.SprayCar(aColor);
    }
  }

When implementing a method using method injection, you must include a guard clause. The dependency will be immediately used, and if you try to use it when it’s null, you’ll get an immediate error. This should obviously be avoided.

Now, when we pass the dependency directly to the method, the interface goes out of scope when we are done painting and the paint gun tip is destroyed. In addition, the next time a car needs to be painted, the consumer will pass in a new tip, which will be freed upon use. 

When to Use Method Injection

Method injection is useful in two scenarios: 

  1. When the implementation of a dependency will vary. 
  2. When the dependency needs to be renewed after each use. 

In both cases, it’s up to the caller to decide what implementation to pass to the method.

 

5 Dependency Injection Principles to Follow

There are five basic principles that you should follow for dependency injection:

1. Code Against Abstractions, Not Implementations

Erich Gamma of the “Gang of Four”, the authors of the book Design Patterns, is credited with coining the phrase, “Code against abstractions, not implementations.” And it’s a powerful idea. 

Abstractions, usually interfaces but not always, are flexible and can be implemented in many ways. Coding against abstractions, rather than implementations, can prevent tight coupling and result in a more versatile system. Avoid locking into a single implementation; use abstractions for a more reusable and adaptable code. 

2. Never Create Things That Shouldn’t Be Created

During dependency injection, your classes should follow the single responsibility principle, the idea that a class should only do one thing. If they do that, then they shouldn’t be creating things because that makes two things they’re doing. Instead, they should ask for the functionality they need and let something else create and provide that functionality. 

There are two different kinds of objects that we should consider creating: creatables and injectables.

Creatables vs. Injectables

Creatables are classes you should go ahead and create. Generally, classes in the run-time library should be considered creatables. They typically have short lifetimes, living no longer than the span of a single method. If they’re required by the class as a whole, they can be created in the constructor. Only other creatables should be passed to a creatable’s constructor.

Injectables, on the other hand, are classes we never want to create directly or hard-code a dependency to. Instead, they should always be passed via dependency injection. They will normally be asked for as dependencies in a constructor, and should be referenced via interfaces — not direct references to an instance. 

Injectables will most often be classes you write as part of your business logic. They should always be hidden behind an abstraction, usually an interface. Note, too, that injectables can ask for other injectables in their constructor. 

3. Keep Constructors Simple

The constructor of a class shouldn’t be doing any work. They shouldn’t be doing anything other than checking for null, creating creatables and storing dependencies for later use. They shouldn’t include any coding logic. An if clause in a class’ constructor that isn’t checking for null is a cry for that class to be split into two classes. There are ways to check for nil-value parameters that don’t involve an if statement.

A complex constructor is a clear sign that your class is doing too much. Keep constructors short, simple, and free of any logic during dependency injection. 

4. Don’t Assume Anything About the Implementation

Interfaces are useless without an implementation. But never make any assumptions about what that implementation is.

You should only code against the contract made by the interface. You may have written the implementation, but you shouldn’t code against the interface with that implementation in mind. In other words: Code against your interface as if a radically new and better implementation of that interface is right around the corner.

well-designed interface will tell you what you need to do and how it’s to be used. The implementation of that interface should be immaterial to your usage of the interface. 

5. Don’t Assume an Interface Is an Abstraction

Not every interface is an abstraction. For instance, if your interface is an exact representation of the public portion of your class, you really aren’t abstracting anything. Such interfaces are called header interfaces because they resemble C++ header files. Interfaces extracted from classes can easily be tightly coupled to that class alone, making the interface useless as an abstraction.

Finally, abstractions can be leaky. They can reveal specific implementation details about their implementation. Leaky abstractions are also normally tied to a specific implementation. 

 

Benefits of Dependency Injection

Why go to all the trouble to arrange our code in the particular way called for by the principles of dependency injection? Because dependency injection offers many benefits.  

1. Code Using Dependency Injection is Maintainable

One of the main benefits of dependency injection is maintainability. If your classes are loosely coupled and follow the single responsibility principle, your code will be easier to maintain.

Simple, stand-alone classes are easier to fix than complicated, tightly coupled classes.

Maintainable code has a lower total cost of ownership. Maintenance costs often exceed the cost of building the code in the first place, so anything that improves the maintainability of your code is a good thing. 

2. Code Using Dependency Injection Is Testable

Code that is easy to test is tested more often. More testing means higher quality.

Loosely coupled classes that only do one thing are very easy to unit test. By using dependency injection, you make creating test doubles, commonly called “mocks,” much more straightforward.

If you pass dependencies to classes, it’s easy to pass in a test double implementation. If dependencies are hard-coded, it’s impossible to create test doubles for those dependencies.

Testable code that actually is tested is quality code. Or at least, it’s of higher quality than untested code. 

3. Code Using Dependency Injection Is Readable

Code that uses dependency injection is more straightforward. It follows the single responsibility principle, resulting in smaller, more compact, and to-the-point classes.

Constructors aren’t as cluttered and filled with logic. Classes are more clearly defined, openly declaring what they need. Because of all this, dependency injection-based code is more readable. And more readable code is more maintainable.

More on Software DevelopmentA Guide to React Hooks With Examples

4. Code Using Dependency Injection Is Flexible

Loosely coupled code is more flexible and usable in different ways. Small classes that do one thing can more easily be reassembled and reused in different situations.

Small classes are like Lego blocks — they can easily be pieced together to make a multitude of things, as opposed to Duplo blocks, which are bulkier and less flexible. Being able to reuse code saves time and money.

All software needs to be able to change and adapt to new requirements. Loosely coupled code that uses dependency injection is flexible and able to adapt to those changes. 

5. Code Using Dependency Injection Is Extensible

Code that uses dependency injection results in a more extendable class structure. By relying on abstractions instead of implementations, code can easily vary a given implementation.

When you code against abstractions, you can code with the notion that a radically better implementation of what you are doing is just around the corner.

Small, flexible classes can be extended easily, either by inheritance or composition.

An application’s codebase never remains static, and you will very likely need to add new features as your codebase grows and new requirements arise. Extensible code is up to that challenge. 

6. Code Using Dependency Injection Is Shareable

If you are working on a team project, then dependency injection will facilitate team development. Even if you are working alone, your work will likely be passed on to someone in the future.

Dependency injection calls for you to code against abstractions and not implementations.

If you have two teams working together, each needing the other’s work, you can define the abstractions before doing the implementations. Then each team can write their code using the abstractions, even before the implementations are written.

Also, because code is loosely coupled, those implementations won’t rely on each other, so they can be more easily split between teams.

Frequently Asked Questions

Dependency injection is a programming technique that involves inserting a software component known as an “object” instead of creating the component. This results in a piece of largely independent code that is only dependent on specific components, allowing developers to work with cleaner and more flexible code.

The three types of dependency injection are constructor injection, property injection and method injection. 

Hiring Now
SOPHiA GENETICS
Artificial Intelligence • Big Data • Healthtech • Software • Biotech
SHARE