DEV Community

Manish Boge
Manish Boge

Posted on • Originally published at manishboge.Medium on

Mastering Dependency Injection — Part 1: The Essentials Every Developer Must Know

Mastering Dependency Injection — Part 1: The Essentials Every Developer Must Know

Unlock the design principle that powers modern frameworks powerful, testable, and scalable.

Introduction

Before diving into any framework’s DI system, whether it’s Angular, Spring, or .NET Core, it’s essential to understand the core design principle that powers them all.

Dependency Injection (DI) isn’t just a buzzword; it’s the architectural glue that enables loose coupling, modularity, and testability in modern applications.

In this two-part series of Understanding DI Concepts, we’ll break down DI from the ground up:

  • Part 1 (this article): The core concepts, principles, and mental models
  • Part 2: Practical patterns, testing strategies, and framework integration

Since the main goal of this series is to fully understand Angular’s Dependency Injection system , we must first build a solid foundation by understanding DI fundamentals, independent of any framework.

Let us start with the essentials.

Why Dependency Injection Matters

Dependency Injection (DI) is a fundamental software design pattern that improves code quality by promoting l oose coupling, enhanced testability, and cleaner architecture.

Whether you’re building a small app or a large-scale enterprise system, understanding DI is key to writing maintainable and scalable code.

Every time your class creates its own dependencies, it quietly takes on responsibilities it shouldn’t have.

That leads to:

  • Rigid, tightly coupled code
  • Hard-to-test components
  • Painful refactors
  • Code that breaks when business logic changes

DI solves all of these problems by enforcing one simple principle:

Classes should not create their dependencies — they should receive them.

This unlocks flexibility, composability, and maintainability in real-world applications.

This article breaks down DI basics, explaining what it is , why it’s important , and how it changes the way dependencies are managed in software.

What Is Dependency Injection?

Let us begin with a simple definition:

Dependency Injection is a technique where a class receives the objects it depends on — its dependencies — instead of creating them internally.

Technical Definition

Dependency Injection (DI) is a software design pattern in which an object’s dependencies are supplied by an external entity rather than being constructed within the object itself.

This pattern is a practical implementation of the Inversion of Control (IoC) principle, where the responsibility of creating and managing dependencies is delegated to an external system (such as a framework, a container, or even custom code).

A Simple Way to Think About It

Instead of a class saying:

“I’ll create everything I need.”

It says:

“Give me what I need to do my job.”

This inversion of responsibility provides major advantages:

  • Better maintainability
  • Easier testing
  • More flexible and modular systems

Here is the important part:

You don’t need a framework to understand DI. The concept exists independently of Angular, Spring, .NET, or any other DI container.

Inversion of Control (IoC): The Principle Behind DI

Inversion of Control (IoC) is the broader principle that underpins DI.

It means that the control of object creation and lifecycle is inverted - moved away from the dependent class to an external entity (like an injector or framework).

  • Without IoC: The class controls its dependencies.
  • With IoC: An external system (like Angular, Spring, or your own injector) controls them.

Think of IoC as the philosophy, and Dependency Injection as one practical way to implement it.

Note: DI isn’t the only way to achieve IoC. Other patterns include Service Locator, Factory patterns, and Event-driven architectures . Each has different trade-offs in terms of explicitness and discoverability. DI is favored because it makes dependencies explicit and easier to track.

Practical Analogy

Think of dependencies as services in our daily life.

We need a ride to work. We could:

  • Buy a car
  • Handle fuel
  • Pay for maintenance
  • Replace parts
  • Take responsibility for everything

Or…

We can use Uber.

We delegate the responsibility to someone else, and we just use the service.

DI works the same way:

Instead of creating and managing dependencies, your class simply uses what’s provided to it.

So that’s how DI improves scalability and flexibility in your projects.

The Problem DI Solves: Tight Coupling

In traditional object-oriented code, classes often create their own dependencies:

Example:

// Logger Class
class Logger {
  log(message: string): void {
    console.log('Log:', message);
  }
}

// User Class (tightly coupled)
class User {
  private logger = new Logger(); // hard-coded dependency

  createUser(name: string): void {
    this.logger.log(`User ${name} created.`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Consequence:

Here, User is tightly coupled to Logger.

If you want to replace Logger with a new one (DatabaseLogger or FileLogger), you must edit User’s source code.

This violates a core software design rule:

“High-level modules should not depend on low-level modules; both should depend on abstractions.” —  Dependency Inversion Principle, the “D” in SOLID

Important Clarification:

Not all uses of the new keyword indicates bad design. Value objects, DTOs, and data structures should be created directly.

For example:

const date = new Date();
const email = new EmailAddress(userInput);
Enter fullscreen mode Exit fullscreen mode

DI applies to behavioral dependencies such as services, repositories, and components that have side effects or external interactions like logging, database access, or API calls.

The Solution: Dependency Injection

With DI, dependencies are supplied (injected) from the outside — typically through the constructor:

Example:

// Logger (Dependency)
class Logger {
  log(message: string): void {
    console.log('Log:', message);
  }
}

// User (Dependent)
class User {
  constructor(private logger: Logger) {} // Constructor Injection (Recommended)

  createUser(name: string): void {
    this.logger.log(`User ${name} created.`);
  }
}

// Usage
const logger = new Logger();
const user = new User(logger);
user.createUser('Alice');
Enter fullscreen mode Exit fullscreen mode

Now User doesn’t know how logging is done — it only knows that something logs.

So Now,

  • User doesn’t create Logger
  • Any logger can be provided
  • Testing is trivial
  • Different implementations(FileLogger, MockLogger, or DatabaseLogger without touching User) can be plugged in without touching User

This is DI in its simplest form — clean, elegant, and extensible.

A dependency is any object another class relies on. Dependency Injection is the technique of supplying those dependencies, instead of creating them inside the class.

Using Abstractions with Interfaces

To fully leverage the Dependency Inversion Principle, it’s best to depend on abstractions instead of concrete classes.

Example:

// ILogger interface as abstraction
interface ILogger {
  log(message: string): void;
}

// ConsoleLogger implements ILogger
class ConsoleLogger implements ILogger {
  log(message: string): void {
    console.log('Log:', message);
  }
}

// User depends on the ILogger abstraction
class User {
  constructor(private logger: ILogger) {}

  createUser(name: string): void {
    this.logger.log(`User ${name} created.`);
  }
}

// Usage
const logger = new ConsoleLogger();
const user = new User(logger);
user.createUser('Alice');
Enter fullscreen mode Exit fullscreen mode

Now User depends only on the contract , never the concrete class.

Using an interface like ILogger decouples User from specific implementations, enabling seamless swapping and better scalability.

This is the essence of the Dependency Inversion Principle : both high-level and low-level modules depend on abstractions, not on each other.

A Preview: Types of Dependency Injection

There are several ways to inject dependencies into a class:

  1. Constructor Injection  — Dependencies passed through the constructor (most common and recommended)
  2. Setter Injection  — Dependencies set via setter methods (for optional dependencies)
  3. Method Injection  — Dependencies passed as method parameters (for transient needs)
  4. Interface-Based Injection  — Injection enforced through interface contracts

Each pattern serves different use cases, and we’ll explore all of them in detail in Part 2 of this series.

For now, just know that Constructor based Injection is the recommended default because it makes dependencies explicit and ensures objects are fully initialized.

When to Use Dependency Injection

DI is particularly valuable when:

  • Your application has complex dependencies across components or services
  • Unit testing and mocking are priorities
  • You want a modular, scalable architecture that supports future change
  • You expect multiple implementations of the same abstraction
  • Your team is working on a medium-to-large codebase

When NOT to Use Dependency Injection

DI isn’t always the right choice. Consider simpler alternatives when:

  • Building small scripts or utilities (< 3 dependencies)
  • Writing pure functions with no external dependencies
  • Working in performance-critical paths where indirection matters
  • Prototyping or writing throwaway code
  • The complexity of DI setup outweighs the benefits

Remember: Every pattern has trade-offs. DI adds indirection and configuration overhead. For simple scenarios, direct instantiation is perfectly fine.

The Mental Model: Thinking in Dependencies

The key mindset shift with DI is this:

Before DI thinking: “What do I need? Let me go create it.”

After DI thinking: “What do I need? Let me declare it, and someone else will provide it.”

This shift enables:

  • Flexibility  — Swap implementations without code changes
  • Testability  — Replace real dependencies with mocks
  • Maintainability  — Changes localized to one place
  • Scalability  — New features don’t break existing code

Once you internalize this mental model, writing loosely coupled code becomes natural.

Real-World Impact: Why This Matters

Let me share a quick example from a fintech project point of view.

We needed to support multiple payment gateways (Stripe, PayPal, internal processing). Initially, payment logic was scattered across 200+ transaction-handling classes, each directly instantiating the payment gateway they needed.

Without DI:

When business requirements changed and we needed to switch providers, it would have required modifying every single class.

The DI solution:

We created an IPaymentGateway interface and used Constructor Injection. Suddenly:

  • Switching providers became a single configuration change
  • Testing payment logic used mock gateways (no real transactions)
  • Adding new providers didn’t touch existing code

This is the power of DI: change becomes configuration, not code modification.

What’s Coming in Part 2

Now that you understand the core philosophy of Dependency Injection, you’re ready to see it in action.

In Part 2: Dependency Injection in Practice , we’ll explore:

  1. The 4 injection patterns in detail (Constructor, Setter, Method, Interface-based)
  2. Dependency lifecycles : Singleton vs Transient vs Scoped
  3. Testing strategies with real DI examples and mocks
  4. How frameworks automate DI (Angular, Spring, .NET Core)
  5. Common pitfalls that trip up even experienced developers
  6. Real-world case studies

Part 2 will be published next week — follow me so you don’t miss it!

Conclusion

Dependency Injection isn’t just a pattern, it’s a mindset that leads to cleaner, more robust code.

Understanding the essentials of DI is the first step toward mastering frameworks like Angular, where these principles are automated and extended.

These concepts form the mental model you need to understand how modern frameworks work.

But we’re just getting started.

Let’s Talk DI!

Dependency Injection isn’t just some fancy design pattern — it’s the backbone of clean, flexible, and testable code.

Now I’m curious…

  • What DI pattern do you usually rely on in your projects?
  • Have you ever struggled with tight coupling , and how did DI help you untangle it?

Drop your thoughts in the comments — I’d love to hear how other developers approach this!

And if you found these examples helpful, give it a clap and share it with your team.

Source Code & Resources

If you’d like to dive deeper or try the examples yourself, all source code including those discussed in this series is available on my GitHub repository: Angular DI Series

Don’t forget to give this Repository a star ⭐  — not only does it help you stay updated when code changes, but it also shows appreciation for the content and motivates continued development.

Connect with Me!

Hi there! I’m Manish , a Senior Engineer, passionate about building robust web applications and exploring the ever-evolving world of tech. I believe in learning and growing together.

If this article sparked your interest in modern Angular, software architecture, or just a tech discussion, I’d love to connect with you!

🔗 Let’s connect on LinkedInfor more tech insights and discussions.

📧 Follow me on Mediumto catch Part 2 when it drops next week!

💻 *Explore my work on * GitHub

Top comments (0)