DEV Community

Cover image for From SOLID to Composition, Dependency Injection, and IoC: How Angular, Spring, and Node.js Differ
Bhargav Patel
Bhargav Patel

Posted on

From SOLID to Composition, Dependency Injection, and IoC: How Angular, Spring, and Node.js Differ

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A larger function can be created by composing smaller functions.

In object-oriented programming:

OrderService
    │
    ├── PaymentService
    └── EmailService
Enter fullscreen mode Exit fullscreen mode

OrderService is composed using other objects.

The important relationship is often:

HAS-A rather than IS-A

For example:

OrderService HAS-A PaymentService
Enter fullscreen mode Exit fullscreen mode

rather than:

OrderService IS-A PaymentService
Enter fullscreen mode Exit fullscreen mode

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) {}
}
Enter fullscreen mode Exit fullscreen mode

OrderService needs PaymentService.

Instead of creating it internally:

class OrderService {
  constructor() {
    this.paymentService = new PaymentService();
  }
}
Enter fullscreen mode Exit fullscreen mode

we provide it from outside:

const paymentService = new PaymentService();
const orderService = new OrderService(paymentService);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 {
}
Enter fullscreen mode Exit fullscreen mode

and:

@Component(...)
class UserComponent {
  constructor(private userService: UserService) {}
}
Enter fullscreen mode Exit fullscreen mode

The component only declares:

"I need a UserService."

It doesn't say:

new UserService()
Enter fullscreen mode Exit fullscreen mode

Angular's injector resolves the dependency.

Conceptually, Angular can build something like:

UserComponent
      ↓
UserService
      ↓
UserRepository
      ↓
HttpClient
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Spring can manage these objects as beans and inject their dependencies.

Conceptually:

Spring IoC Container
        ↓
UserRepository
        ↓
UserService
        ↓
UserController
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Then the application's composition root can wire everything:

const repository = new UserRepository();
const service = new UserService(repository);
const controller = new UserController(service);
Enter fullscreen mode Exit fullscreen mode

The dependency graph is:

UserRepository
      ↓
UserService
      ↓
UserController
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

The dependency is explicit:

calculateTotal
      ↑
  taxService
Enter fullscreen mode Exit fullscreen mode

We can provide a different implementation:

calculateTotal(cart, realTaxService);
Enter fullscreen mode Exit fullscreen mode

or:

calculateTotal(cart, fakeTaxService);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The Dashboard component is composed from smaller components.

Dependencies can also be passed through props:

App
 ↓
UserComponent
 ↓
userService
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

These aren't competing concepts.

They operate at different levels.

11. The Same Architecture, Different Approaches

Consider:

Controller
    ↓
Service
    ↓
Repository
Enter fullscreen mode Exit fullscreen mode

Manual Node.js

You create everything
        ↓
You inject dependencies
        ↓
You control composition
Enter fullscreen mode Exit fullscreen mode

Angular

Angular Injector
        ↓
Creates dependencies
        ↓
Injects them
        ↓
Manages scopes
Enter fullscreen mode Exit fullscreen mode

Spring

Spring IoC Container
        ↓
Creates beans
        ↓
Resolves dependencies
        ↓
Injects them
Enter fullscreen mode Exit fullscreen mode

Functional approach

Functions
    ↓
Explicit arguments
    ↓
Composition
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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)