Skip to content

Dependency Injection vs Dependency Inversion

Published: at 03:57 PM

Table of contents

Open Table of contents

The Short Version

Use this mental model:

One is an architectural rule. The other is a construction technique.

Dependency injection can help you implement dependency inversion, but it does not guarantee it.

Dependency Inversion Principle

The Dependency Inversion Principle is the D in SOLID.

The classic statement is:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

That sounds abstract, so make it concrete.

Suppose you have business logic that sends a welcome email after a user signs up.

If your business service directly creates an SMTP client, then the policy of “send a welcome email” is tightly coupled to one delivery mechanism.

That is the wrong direction of dependency.

The business rule is the important part. The SMTP client is an implementation detail.

With dependency inversion, the business layer depends on an abstraction such as EmailSender, and the SMTP implementation depends on that abstraction by implementing it.

What DIP is really trying to protect

It protects your core logic from implementation details.

That means:

This is the heart of the principle.

Dependency Injection

Dependency Injection is a way of supplying collaborators from the outside instead of constructing them internally.

Instead of this:

service := UserService{
    sender: NewSMTPClient(...),
}

you do something like this:

service := UserService{
    sender: smtpSender,
}

The service receives what it needs. It does not create it itself.

Common forms of dependency injection are:

Constructor injection is usually the clearest default because it makes required dependencies explicit.

The Core Difference

The difference is easiest to see like this:

You can use injection without inversion.

For example, this uses dependency injection:

type UserService struct {
    db *sql.DB
}

func NewUserService(db *sql.DB) UserService {
    return UserService{db: db}
}

The dependency is injected from the outside, so yes, this is DI.

But the service still depends directly on a concrete database type. That may be totally fine in many codebases, but it is not a strong example of dependency inversion.

Now compare that with this:

type UserStore interface {
    CreateUser(ctx context.Context, user User) error
}

type UserService struct {
    store UserStore
}

func NewUserService(store UserStore) UserService {
    return UserService{store: store}
}

Now the service depends on an abstraction and receives that abstraction from the outside.

That uses both:

A Bad Example

Here is a design that violates dependency inversion:

type SMTPClient struct{}

func (c SMTPClient) Send(to, body string) error {
    return nil
}

type UserService struct{}

func (s UserService) Register(email string) error {
    client := SMTPClient{}
    return client.Send(email, "welcome")
}

Problems:

The high-level module depends directly on the low-level module.

A Better Example

Now invert the dependency and inject it:

package main

import "context"

type EmailSender interface {
    Send(ctx context.Context, to, body string) error
}

type UserService struct {
    sender EmailSender
}

func NewUserService(sender EmailSender) UserService {
    return UserService{sender: sender}
}

func (s UserService) Register(ctx context.Context, email string) error {
    // business logic here
    return s.sender.Send(ctx, email, "welcome")
}

type SMTPSender struct{}

func (s SMTPSender) Send(ctx context.Context, to, body string) error {
    return nil
}

What improved:

Why This Matters in Practice

The real value is not academic purity. It is changeability.

Today you may send email through SMTP. Tomorrow you may send through SES, SendGrid, or a background job queue.

If the business service is coupled directly to SMTP, the change spreads through the wrong layer. If the service depends on an abstraction, the change is localized to the adapter and wiring.

That is the payoff.

Dependency Injection Does Not Automatically Mean Good Design

A lot of teams overcorrect and inject everything.

That creates a different failure mode:

Good design is not “always use interfaces.” Good design is “separate policy from detail where the boundary is meaningful.”

For example, in Go it is often perfectly fine for a repository to depend directly on *sql.DB. You do not need an interface around everything. The important boundary is usually where business logic should be protected from infrastructure choices.

A Better Rule for Go

In Go, a practical rule is:

This is usually better than creating a giant “service layer interface” for every type on day one.

For example, if UserService only needs CreateUser, then this:

type UserStore interface {
    CreateUser(ctx context.Context, user User) error
}

is better than exposing a huge storage abstraction with methods the service does not actually need.

Constructor Injection vs DI Containers

Dependency injection does not require a framework or container.

In Go, explicit wiring in main is often the best choice:

func main() {
    store := PostgresUserStore{db: db}
    sender := SMTPSender{}
    service := NewUserService(sender)

    _ = store
    _ = service
}

This keeps object construction visible and debuggable.

DI containers can help in very large systems, but they also hide wiring and can make control flow harder to trace. Use them when the complexity is justified, not by default.

Common Interview Answer

If someone asks, “What is the difference between dependency injection and dependency inversion?”, a strong short answer is:

Dependency inversion is a design principle that says high-level modules should depend on abstractions, not concrete details. Dependency injection is a technique for providing those dependencies from the outside. Injection can help implement inversion, but they are not the same thing.

That answer is short, correct, and defensible.

Closing View

Dependency Inversion is about architecture. Dependency Injection is about wiring.

Use dependency inversion when you need to protect important business logic from infrastructure details. Use dependency injection to make those dependencies explicit and replaceable.

If you remember only one line, remember this:

DIP changes the direction of dependency. DI changes the way dependencies are supplied.