Olivier Lowe

This Engineering Note is written in English.

Frontend Architecture · Angular · Composition

Dependency Injection Is Not Dependency Inversion

Angular can inject a dependency correctly while higher-level code still depends on the wrong thing.

The Confusion

Dependency Injection and Dependency Inversion often appear next to each other in the same code.

That makes them easy to collapse into one idea.

In NovaTrade, Angular was already capable of injecting services long before runtime composition became architecturally interesting.

But the existence of a DI mechanism did not tell us whether the dependency direction was right.

The important distinction:

Dependency Injection can supply a dependency perfectly even when higher-level code depends directly on the wrong implementation.

The Problem Was the Dependency Direction

Imagine the Orders feature depending directly on the production REST adapter:

OrderPage
    ↓
RestOrderApi

Angular could inject that object.

The application would run.

But the feature would know that its backend capability was implemented through REST.

That is the architectural coupling that mattered.

The Dependency Changed

The higher-level feature stopped depending directly on the concrete adapter.

Instead, it depended on the capability it required:

OrderPage
    ↓
ORDER_API

Now the Orders feature expressed what it needed without choosing how that need would be implemented in production.

Dependency Inversion asks:

What should higher-level code depend on?

The important architectural change was not that Angular injected something.

It was that higher-level code now depended on a capability boundary instead of a concrete transport implementation.

But Production Still Needs Something Concrete

Removing the concrete adapter from the feature does not remove the adapter from the running application.

Production still needs an implementation.

So the runtime graph still has to connect:

ORDER_API
    ↓
RestOrderApi

This is where Angular Dependency Injection becomes useful.

Angular knows how to supply a concrete implementation when application code asks for the capability token.

Dependency Injection asks:

How is the requested dependency supplied?

There Is Still a Third Question

Even after separating the capability from its concrete implementation, somebody still has to decide which implementation the running application receives.

In NovaTrade, that decision already had a natural home.

src/main.ts
    ↓
src/app/app.config.ts

The Angular application configuration could see both sides of the boundary:

required capability
    ↓
ORDER_API

production choice
    ↓
RestOrderApi

That runtime choice is a different responsibility again.

Composition asks:

Which concrete implementation should the running application use?

Three Questions, Not One

Dependency Inversion
What should
higher-level code
depend on?
Dependency Injection
How is that
dependency
supplied?
Composition
Which implementation
does the running
application receive?
Dependency Inversion
        ≠
Dependency Injection
        ≠
Composition

They participate in the same runtime graph.

They do not answer the same architectural question.

The Complete Runtime Picture

Once the responsibilities were separated, NovaTrade's production relationship could be described clearly:

OrderPage
    ↓
ORDER_API
    ↑
RestOrderApi

binding selected by
app.config.ts

The feature knows the capability it needs.

The concrete adapter knows how to communicate with the backend.

The composition boundary decides which implementation production receives.

A Composition Root Is a Responsibility

Once the runtime choice became important enough to name, the architectural responsibility was a Composition Root.

But that did not mean NovaTrade needed a new class, directory, or framework.

We did not introduce:

CompositionRoot class

composition/

custom DI container

service locator

dependency registry

factory framework

Angular already had enough mechanism to express the runtime choice.

The architectural improvement came from making the responsibility explicit, not from adding more infrastructure.

The Consumer and the Composition Boundary Own Different Things

The Orders feature should be able to say:

I need ORDER_API

without also saying:

and production must satisfy it
with RestOrderApi

Those are different decisions.

Consumer ownership
Which capability
do I require?
Composition ownership
Which implementation
satisfies that capability
here?

Why the Distinction Matters

Once the higher-level Application depended on the capability boundary, the implementation choice could vary independently.

Production could choose:

ORDER_API
    ↓
RestOrderApi

while another composition could choose:

ORDER_API
    ↓
GraphqlOrderApi

The Application did not need to change merely because the concrete implementation changed.

That later substitution was useful evidence that the responsibility had been placed at the right boundary.

What Angular DI Did — and Did Not Do

Angular DI did
supply dependencies

connect providers

assemble runtime objects
Angular DI did not decide
what higher-level code
should depend on

who owns the capability

where business rules belong

A framework mechanism can support an architecture.

It does not define the architecture for us.

The Lesson

Dependency Inversion decides what higher-level code depends on.

Dependency Injection decides how that dependency is supplied. Composition decides which implementation the running application receives.

Keeping those questions separate makes the architecture easier to reason about.

More importantly, it prevents a framework feature from hiding an architectural decision.

From the book

Frontend Architecture with TypeScript, Angular, and React

This distinction comes from the NovaTrade architecture story in my upcoming book: building frontend architecture around responsibilities and business behavior rather than around framework mechanisms.

Explore the frontend architecture book →