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.
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
↓
RestOrderApiAngular 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_APINow the Orders feature expressed what it needed without choosing how that need would be implemented in production.
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
↓
RestOrderApiThis is where Angular Dependency Injection becomes useful.
Angular knows how to supply a concrete implementation when application code asks for the capability token.
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.tsThe Angular application configuration could see both sides of the boundary:
required capability
↓
ORDER_API
production choice
↓
RestOrderApiThat runtime choice is a different responsibility again.
Which concrete implementation should the running application use?
Three Questions, Not One
What should
higher-level code
depend on?How is that
dependency
supplied?Which implementation
does the running
application receive?Dependency Inversion
≠
Dependency Injection
≠
CompositionThey 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.tsThe 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 frameworkAngular 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_APIwithout also saying:
and production must satisfy it
with RestOrderApiThose are different decisions.
Which capability
do I require?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
↓
RestOrderApiwhile another composition could choose:
ORDER_API
↓
GraphqlOrderApiThe 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
supply dependencies
connect providers
assemble runtime objectswhat higher-level code
should depend on
who owns the capability
where business rules belongA 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 →