Building Software Without Glue
Understanding Dependency Injection and IoC Containers Through a LEGO Analogy
If you’ve been writing .NET applications for a while, you’ve probably seen code like this countless times:
public class Car
{
private readonly Engine _engine = new Engine();
}
At first glance, nothing seems wrong.
A car needs an engine, so it creates one.
Simple.
But this single line introduces one of the biggest obstacles to building maintainable software.
The moment a class starts creating its own dependencies, it becomes tightly coupled to them.
On small projects, this rarely causes problems.
As systems grow, however, that coupling becomes technical debt.
Imagine building a LEGO castle where every brick is glued together.
It looks solid until you decide to replace one tower with another.
Instead of swapping a single piece, you have to break the entire structure apart.
Software behaves the same way.
The Real Problem with new
The new keyword isn’t the enemy.
The problem is where you use it.
When a class creates important dependencies by itself, it slowly becomes what many architects call a Control Freak.
It controls everything.
It decides what to create.
It decides when to create it.
It owns its lifetime.
As a result, nothing outside that class can change its behavior without modifying its source code.
That makes the system rigid, difficult to test, and expensive to maintain.
Imagine You’re Building a LEGO Castle
Now imagine a different way of building.
Instead of assembling every LEGO brick yourself, you have a smart robot builder.
Your job isn’t to snap bricks together.
Your job is simply to provide the blueprint.
You tell the robot:
Use this block here.
Replace this block with another implementation if needed.
Keep this block forever.
Create a fresh one for every request.
Then you press Start.
The robot builds the entire castle automatically.
That’s exactly what an IoC Container does inside an ASP.NET Core application.
IoC and Dependency Injection Are Not the Same Thing
Many developers use these two terms interchangeably.
Technically, they’re different.
Inversion of Control (IoC) is an architectural principle.
It says that object creation and control should no longer belong to individual classes.
Dependency Injection (DI) is simply one technique for implementing that principle.
In other words:
IoC is the idea.
Dependency Injection is the implementation.
Every Dependency Injection approach follows IoC.
But not every IoC implementation uses Dependency Injection.
Understanding this distinction makes many architectural discussions much easier to follow.
What Does an IoC Container Actually Do?
Once your application starts, the IoC Container takes responsibility for two important tasks.
The first is dependency resolution.
Consider the following constructor:
public ProductService(IProductRepository repository)
The container examines the constructor, finds a registered implementation of IProductRepository, creates it if necessary, and injects it automatically.
No manual new ProductRepository() required.
The second responsibility is lifetime management.
The container decides whether an object should be:
Created once for the entire application (Singleton)
Created once per HTTP request (Scoped)
Created every time it’s requested (Transient)
Because of this, developers rarely need to think about object creation or disposal.
The container handles both.
Interfaces Are What Make Software Flexible
In the LEGO world, two blocks are interchangeable if they share the same connection points.
Software works the same way.
Interfaces define the contract.
Imagine your application depends only on:
IEmailService
Today, the implementation might be:
SmtpEmailService
Tomorrow it could become:
SendGridEmailService
Next month:
AzureCommunicationService
The consumer never changes.
Only the registration inside the container changes.
This ability to replace implementations without touching business logic is one of the biggest advantages of Dependency Injection.
Four Rules for Highly Flexible Code
Most maintainable systems follow four simple principles.
First, depend on abstractions instead of concrete implementations.
Second, let the container create your dependencies.
Third, consumers should never know which implementation they receive.
Finally, if your class didn’t create an object, it shouldn’t control that object’s lifetime either.
These ideas form the foundation of modern .NET architecture.
When Is Using new Completely Fine?
A common misconception is that Dependency Injection eliminates the need for new.
It doesn’t.
Creating simple objects directly is perfectly acceptable.
For example:
new List<string>()
or
new StringBuilder()
These classes have no external dependencies and rarely need to be mocked.
The situation changes when your object communicates with external systems.
Database contexts.
HTTP clients.
File systems.
Network connections.
System clocks.
These are volatile dependencies.
They’re more likely to change, require testing, or depend on infrastructure.
Those are the objects that should typically be injected.
The Composition Root: Where Everything Comes Together
Every application should have one central place responsible for assembling object graphs.
This is known as the Composition Root.
In modern ASP.NET Core applications, that place is usually Program.cs.
Here, you register your services:
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IProductService, ProductService>();
From that point onward, the container takes over.
No other part of the application should be responsible for constructing these services.
Everything flows from this single location.
The Four-Step Assembly Process
Building an application with Dependency Injection is surprisingly straightforward.
First, define a contract using an interface.
Next, create its implementation.
Then register that relationship inside the Composition Root.
Finally, start the application and allow the IoC Container to assemble the dependency graph automatically.
Your classes stop worrying about object creation.
Instead, they focus solely on the responsibilities they were designed to perform.
Final Thoughts
Dependency Injection is much more than an ASP.NET Core feature.
It’s a different way of thinking about software design.
By moving object creation outside your classes, your applications become easier to extend, easier to test, and significantly easier to maintain.
The IoC Container quietly handles object creation, dependency resolution, and lifetime management behind the scenes, allowing your code to stay focused on business logic.
The ultimate goal isn’t to eliminate the new keyword.
It’s to eliminate unnecessary coupling.
When dependencies are no longer glued inside your classes, replacing an implementation becomes as simple as changing a single registration inside your Composition Root.
That’s the difference between building software brick by brick...
...and designing a system that can rebuild itself automatically.




