How Poorly Designed Software Is Facilitated by Using Dependency Injection and Containers

NM

Nicklas Millard

published September 28, 2022

Dependency Injection (DI) and Inversion of control (IoC) often make you do stupid things and let you create poorly designed software.

Dependency Container frameworks such as Java’s Spring Boot or .NET ServiceCollection lets you get away with lazy designs because, when relied upon, you rarely have to think for yourself.

Just a brief recap for those who are in the dark: dependency injection is the practice whereby you let a class take its dependencies as constructor arguments rather than newing them up. Remember the expression: new is glue. Inversion of control is where you let something else — typically a dependency container — inject the required dependencies.

What a lazy design looks like.

Careless developers create messy dependency graphs like this below.

public class UserService {
  public UserService(
    IUserRepository userRepository,
    IUserManager userManager,
    IRolesRepository rolesRepository,
    INotificationService notificationService)
  {
   // Assign 
  }
}

Let’s ignore that this is a so-called “Service” class. As you know, service classes are frequently logic dumpsters and an anti-pattern.

Anyway, you might say “it’s four constructor arguments. How can this possibly be bad or poorly designed?” While I understand the sentiment, let me exemplify how this is deceivingly simple.

First off, using the UserService is easy with dependency frameworks. It’s nothing more than taking a dependency like so:

// Imagine this is an MVC controller
public class UserController : ControllerBase
{
  private readonly UserService service;

  public UserController(UserService service)
  {
    this.service = service;
  }
}

But have you ever had one of those moments of “how the heck do I instantiate this object myself?”

Testing objects with nasty object graphs.

You’re suddenly required to new up a bunch of collaborating objects that take dependencies of their own.

The example below might be contrived, but it does resemble what I’ve seen on many occasions while working on production applications for both small and large projects. Testing a single class quickly turns into 7 collaborating objects.

// Arrange
var appDbContext = new AppDbContext();
var sut = new UserService(
  new EfUserRepository(appDbContext),
  new DefaultUserManager(new DefaultPasswordHasher(SHA256.Create())),
  new EfRolesRepository(appDbContext),
  new DefaultNotificationService(new EfNotificationRepository(appDbContext))
);

// Act
bool result = sut.SaveUser("username", "password");

// Assert
Assert.True(result);

I think this is best described as explosive diarrhea coding. One object sh*ts out many others in a shotgun pattern.

The bright and clever developer will proudly enunciate this is not a unit test and insists on mocking the objects instead. While that’s true, mocking doesn’t really alleviate the root of the problem.

Mocking is a design tool and not a testing strategy.

Ways to counter this nonsense.

Take a moment to consider if you really need to add that additional constructor argument.

Maybe there’s a concept hiding within the messy object graph.

I like to examine a class’ dependencies and methods to see if there are dependencies only used in a single method, and, if it’s possible to rip out the method and dependency to create a single cohesive class with a very clear responsibility.

You want to analyze the fan-in and fan-out complexity.

You can think of fan-out as the number of dependencies a single object takes, and fan-in as the number of objects that uses a single object.

You generally want to go for low fan-out and high fan-in, as this leads to easily tested and flexible code.

But, there are exceptions to any advice. You don’t need to think too hard about reaching high fan-in for high-level, business-oriented classes, as these are less commonly required to be reusable.

Let's Stay in touch

You're always welcome to reach out at nicklas@mjukvare.com!

Nicklas Millard

© 2026