When learning Angular, Spring, and Node.js, I often came across terms like SOLID, Dependency Injection (DI), Inversion of Control (IoC), IoC Container, and Composition.
At first, these concepts can feel like they are all the same thing. They are not.
The key realization is:
SOLID is about how we design software. Composition is about how we build larger systems from smaller pieces. Dependency Injection is a technique for providing those pieces. IoC containers automate that process.
Understanding this relationship makes Angular, Spring, and Node.js architectures much easier to reason about.
1. SOLID Is a Design Principle, Not a Framework Feature
SOLID is a collection of software design principles.
For example, Single Responsibility Principle (SRP) says that a component should have a focused responsibility.
Instead of having one class responsible for HTTP handling, database access, validation, email, and payment processing, we can separate those responsibilities:
Controller
↓
Service
↓
Repository
↓
Database
Each part has a focused job.
Similarly, the Open/Closed Principle (OCP) encourages us to design components that can be extended without constantly modifying their existing implementation.
These principles don't require Angular, Spring, or an IoC container.
You can follow SOLID in plain JavaScript.
2. Composition Is the Bigger Idea
Composition means:
Build a larger behavior by combining smaller, focused pieces.
This works in both functional and object-oriented programming.
In functional programming:
function A
↓
function B
↓
function C
A larger function can be created by composing smaller functions.
In object-oriented programming:
OrderService
│
├── PaymentService
└── EmailService
OrderService is composed using other objects.
The important relationship is often:
HAS-A rather than IS-A
For example:
OrderService HAS-A PaymentService
rather than:
OrderService IS-A PaymentService
This is one reason composition is often preferred over deep inheritance hierarchies.
3. Dependency Injection Is a Way of Doing Composition
Consider this:
class OrderService {
constructor(private paymentService: PaymentService) {}
}
OrderService needs PaymentService.
Instead of creating it internally:
class OrderService {
constructor() {
this.paymentService = new PaymentService();
}
}
we provide it from outside:
const paymentService = new PaymentService();
const orderService = new OrderService(paymentService);
This is Dependency Injection.
The dependency is injected into the object instead of the object creating the dependency itself.
So DI helps us build a loosely coupled composition.
4. IoC Is the Bigger Concept
Normally, our code controls object creation:
Application code
↓
creates Service
↓
creates Repository
↓
creates Database
With IoC, that control is moved somewhere else.
For example, Angular or Spring can control object creation and dependency resolution:
Application
↓
"I need OrderService"
↓
IoC Container
↓
resolves PaymentService
↓
resolves Repository
↓
creates OrderService
That's Inversion of Control.
The framework is now controlling the creation and wiring instead of our application code doing everything manually.
5. Angular's IoC Container
Angular provides a Dependency Injection system.
Suppose we have:
@Injectable()
class UserService {
}
and:
@Component(...)
class UserComponent {
constructor(private userService: UserService) {}
}
The component only declares:
"I need a UserService."
It doesn't say:
new UserService()
Angular's injector resolves the dependency.
Conceptually, Angular can build something like:
UserComponent
↓
UserService
↓
UserRepository
↓
HttpClient
Angular's DI system manages this dependency graph.
So a useful mental model is:
Angular DI is Angular's mechanism for implementing IoC.
Angular also has hierarchical injectors, which allow dependencies to exist at different scopes such as the application/root level or component level.
6. Spring's IoC Container
Spring follows a similar idea in Java.
Instead of manually doing:
UserRepository repository = new UserRepository();
UserService service = new UserService(repository);
UserController controller = new UserController(service);
Spring can manage these objects as beans and inject their dependencies.
Conceptually:
Spring IoC Container
↓
UserRepository
↓
UserService
↓
UserController
Spring therefore takes responsibility for constructing and wiring the object graph.
This is why the term IoC Container is so commonly associated with Spring.
7. What About Node.js?
Node.js itself does not provide an Angular/Spring-style IoC container.
But that doesn't mean Node.js cannot follow SOLID or use Dependency Injection.
You can simply do it manually.
class UserService {
constructor(repository) {
this.repository = repository;
}
}
Then the application's composition root can wire everything:
const repository = new UserRepository();
const service = new UserService(repository);
const controller = new UserController(service);
The dependency graph is:
UserRepository
↓
UserService
↓
UserController
This is still DI and composition.
The difference is that you are performing the wiring yourself.
Node.js gives you modules and functions/classes that allow you to structure the application, but it doesn't automatically construct your dependency graph.
8. Why Functional Programming Often Doesn't Need an IoC Container
This was one of the most interesting differences.
In functional programming, dependencies are often passed directly as function arguments.
For example:
function calculateTotal(cart, taxService) {
const tax = taxService.calculate(cart);
return cart.total + tax;
}
The dependency is explicit:
calculateTotal
↑
taxService
We can provide a different implementation:
calculateTotal(cart, realTaxService);
or:
calculateTotal(cart, fakeTaxService);
No IoC container is required.
This is still dependency injection.
The difference is that functional programming often prefers explicit composition rather than a framework-managed dependency graph.
9. React Uses Composition Heavily
React is primarily based around functions and component composition.
Instead of a large class hierarchy, we can build:
Dashboard
├── Header
├── Sidebar
├── UserProfile
├── Orders
└── Notifications
The Dashboard component is composed from smaller components.
Dependencies can also be passed through props:
App
↓
UserComponent
↓
userService
React also has Context, which allows values to be made available to components in a subtree.
Context can look somewhat similar to dependency injection, but it is not the same kind of general-purpose IoC container provided by Angular or Spring.
10. Composition vs Dependency Injection vs IoC
The relationship can now be summarized:
SOLID
│
├── Design principles
│
└── Encourage loose coupling
│
▼
Composition
│
└── Build larger systems from smaller pieces
│
▼
Dependency Injection
│
└── Provide dependencies from outside
│
▼
IoC Container
│
└── Automates dependency creation/wiring
These aren't competing concepts.
They operate at different levels.
11. The Same Architecture, Different Approaches
Consider:
Controller
↓
Service
↓
Repository
Manual Node.js
You create everything
↓
You inject dependencies
↓
You control composition
Angular
Angular Injector
↓
Creates dependencies
↓
Injects them
↓
Manages scopes
Spring
Spring IoC Container
↓
Creates beans
↓
Resolves dependencies
↓
Injects them
Functional approach
Functions
↓
Explicit arguments
↓
Composition
The underlying architectural goal can be the same:
Keep responsibilities separate and dependencies replaceable.
The mechanism is different.
12. The Most Important Mental Model
The biggest lesson is that SOLID does not require DI, and DI does not require an IoC container.
You can write SOLID JavaScript without any framework.
You can perform DI manually:
const repository = new UserRepository();
const service = new UserService(repository);
You can then introduce an IoC container when manually managing a large dependency graph becomes cumbersome.
So the hierarchy I now use to understand these concepts is:
SOLID
↓
How should I design my components?
Composition
↓
How should I combine those components?
Dependency Injection
↓
How should I provide their dependencies?
IoC
↓
Who should control object creation?
IoC Container
↓
Can a framework automate that control?
Once this distinction is clear, Angular, Spring, Node.js, and functional frameworks stop looking like completely different architectural worlds.
They are often solving the same fundamental problem:
How do we build a large system from small, independent pieces without tightly coupling everything together?
They simply choose different mechanisms for doing it.
Top comments (0)