SOLID Principles: Decoupling, Inversion & Modern IoC (I, D)
High-level domain policy must never depend on low-level implementation details. This article covers Interface Segregation to eliminate fat contracts, the Dependency Inversion Principle enforced at module boundaries, and a clear-eyed comparison of manual DI, the Service Locator anti-pattern, and modern IoC containers (Inversify, NestJS) under TypeScript 5.0+ Stage 3 Decorators.
TypeScript Low-Level Design & Object-Oriented Architecture
SOLID Principles: Decoupling, Inversion & Modern IoC (I, D)
In TypeScript 5+, OOP is an architectural contract, not an inheritance tree: enforce domain invariants at compile time, encapsulate mutation strictly within aggregates, and invert dependencies so high-level business policy never couples to execution details. The phrase "invert dependencies" is precisely what this article mechanizes. Dependency Inversion is not a suggestion to "use interfaces" — it is a structural rule about which layer owns the interface and which layer implements it.
Combined with Interface Segregation, DIP produces the cleanest test-boundary architecture possible: every collaborator in the domain core is a narrow interface, injected at the composition root, replaceable with a test double without any framework. That architecture is what distinguishes senior-level code from mid-level code in LLD interviews.
1. The Anti-Pattern Graveyard: The Fat Monolithic Interface
Here is the interface every CRUD-first team writes when they first extract a service layer:
The audit exporter now has hidden coupling to 7 methods it never calls. Any change to those method signatures forces a recompile of AuditExporter. Any mock for AuditExporter's tests must stub all 8 methods. And if someone renames exportOrderReport, the interface change propagates through every class that implements IOrderService — even those that never export reports.
2. Interface Segregation Principle (ISP)
2.1 The Mechanical Statement
ISP: No client should be forced to depend on methods it does not use.
The solution is role interfaces: define a separate interface for each distinct capability, sized to the minimal surface that any single consumer needs.
Now a test for AuditExporter only needs to mock IOrderReporter — a single method. The entire OrderService is irrelevant to the test.
3. Dependency Inversion Principle (DIP)
3.1 The Two Rules of DIP
DIP has two parts:
- High-level modules must not import from low-level modules. Both must depend on abstractions.
- Abstractions must not depend on details. Details (implementations) must depend on abstractions.
The critical word is own: the abstraction (interface) must be owned by the module that uses it — the domain — not by the module that implements it — the infrastructure.
The dependency arrows now point inward: StripeGateway depends on IPaymentGateway, not the reverse. The domain layer has zero knowledge of Stripe.
4. The Composition Root: Wiring Without a Framework
The composition root is the single location in an application where all dependencies are instantiated and wired together. In a Node.js API, this is typically the application entry point.
No class outside main.ts calls new on a dependency. All construction is centralized. Swapping StripeGateway for AdyenGateway requires changing one line in one file.
4.1 The Service Locator Anti-Pattern
The Service Locator is the most common misidentification of Dependency Injection. It looks similar but has the opposite effect on testability:
With constructor injection (DIP), the test is:

5. Modern Decorators (TS 5.0+) vs. Legacy Metadata
The history of decorators in TypeScript has two distinct eras:
5.1 Legacy Experimental Decorators (experimentalDecorators)
Before TypeScript 5.0, the only available decorator implementation was the legacy proposal, enabled via "experimentalDecorators": true in tsconfig.json. This required reflect-metadata for IoC containers like InversifyJS:
This works, but reflect-metadata uses legacy Reflect APIs and relies on TypeScript emitting constructor parameter metadata — a coupling between the IoC container and the compiler's emission behavior.
5.2 ECMAScript Stage 3 Decorators (TS 5.0+)
TypeScript 5.0 implemented the finalized TC39 Stage 3 Decorator proposal. These are not backward compatible with legacy experimental decorators. They use a different decorator factory signature and do not rely on reflect-metadata.
5.3 Decorator Metadata in TS 5.2+
TS 5.2 adds Symbol.metadata for lightweight custom registry without reflect-metadata:
IoC Container Landscape:
- InversifyJS 6+ / TSyringe: Still use legacy
experimentalDecorators+reflect-metadata. Mature but carry the reflection overhead. - NestJS: Uses legacy decorators internally. The framework manages metadata; you don't need to install
reflect-metadatamanually. - Manual DI (recommended for interviews): Pure constructor injection at the composition root. Zero dependencies, zero framework, 100% testable. Always demonstrate this first in an LLD interview.
6. Dependency Binding Lifecycles
IoC containers support three binding lifecycles. Choosing the wrong one creates memory leaks or state contamination:
Injecting a transient dependency into a singleton creates a captive dependency — the transient object is retained for the singleton's lifetime, effectively becoming a singleton itself. Always match or extend the lifetime of injected dependencies: a singleton can only safely hold other singletons.
Summary
| Principle | Violation | Fix |
|---|---|---|
| ISP | Fat interface forces clients to depend on unused methods | Split into role interfaces sized to each consumer |
| DIP (rule 1) | Domain imports from infrastructure directly | Domain defines the interface; infrastructure implements it |
| DIP (rule 2) | Interface lives in the infrastructure layer | Move interface ownership to the domain layer |
| Composition Root | Construction scattered across many files | Single wiring point at application entry |
| Service Locator | container.get(key) inside domain classes |
Constructor injection — dependencies explicit in signature |
| Stage 3 Decorators | Legacy experimentalDecorators + reflect-metadata |
TS 5.0+ Stage 3 decorators + Symbol.metadata (TS 5.2+) |
What's Next
In Part 8, we pivot from architecture principles to the GoF creational patterns, translated for TypeScript 5+. Factory Method, Abstract Factory, ESM Singletons, and a type-safe Builder with phantom types that literally prevents
.build()from being called until all required fields are set. Part 8: Creational Design Patterns: Factories, Builders & Const Construction.
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.