The Over Engineering Epidemic

How Interfaces, Unit Tests, and Dependency Injection Are Drowning .NET Applications

The .NET development community has fallen into a dangerous pattern of cargo cult programming. We’ve taken valuable tools—interfaces, unit tests, and dependency injection—and turned them into mandatory rituals, applied reflexively regardless of context or actual need. The result? Codebases that are bloated, performance-degraded, and paradoxically harder to understand and maintain than the “legacy” code we’re trying to improve.

The Interface Explosion

Walk into any modern .NET codebase and you’ll find interfaces everywhere. IUserService, IOrderRepository, IEmailSender—each with exactly one implementation. This has become so normalized that developers create interfaces automatically, often before they’ve even written the concrete implementation.

The stated justification: “It makes the code more testable and follows SOLID principles.”

The reality: Most of these interfaces will never have a second implementation. They exist solely to satisfy a dependency injection container and enable mocking in unit tests. The cost is immediate and tangible:

  • Cognitive overhead: Developers must now navigate two files instead of one for every service
  • IDE confusion: “Go to definition” lands on an empty interface instead of actual implementation
  • False abstractions: Interfaces that mirror their implementations 1:1 provide no real abstraction
  • Maintenance burden: Every method signature change requires updates in two places

Consider this typical example:

Interface Example

What value does this interface provide? If you need to swap out the user service implementation, you’ll likely need to change the method signatures anyway. The interface has become a ceremony, not a tool.

The Unit Testing Obsession

Unit testing has become a religious practice in .NET development, with coverage percentages treated as moral imperatives. Teams spend enormous effort mocking dependencies, setting up test scenarios, and maintaining test suites that often provide little real confidence in the application’s behavior.

The stated justification: “Unit tests catch bugs early and enable refactoring.”

The reality: Most unit tests are tightly coupled to implementation details, making them brittle and expensive to maintain. Consider this typical service test:

Unit Test Example

This test requires intimate knowledge of the internal implementation. If you refactor the service to process payment and notification in parallel, or decide to update the order status before sending the notification, this test breaks—even though the external behavior is identical.

The test setup is complex, brittle, and tells us nothing about whether orders actually get processed correctly. It’s testing the orchestration of method calls, not the business logic. When this test passes, you still don’t know if:

  • The payment actually processes correctly
  • The database transaction completes
  • The email notification reaches the customer
  • The order status is persisted correctly

Meanwhile, integration tests that actually exercise the full stack and catch real bugs get dismissed as “too slow” or “too complicated.” The result is test suites with high coverage numbers but low bug-detection rates, and developers who spend more time maintaining test mocks than fixing actual issues.

The Dependency Injection Maze

Dependency injection has become the default approach for any object creation in .NET applications. Every service, repository, and helper class gets registered in the DI container, creating a web of dependencies that’s difficult to understand and debug.

The stated justification: “It enables loose coupling and makes testing easier.”

The reality: DI containers have become a black box where object creation happens magically, making it harder to understand what’s actually running. The performance cost is real:

  • Memory overhead: The container must track all registrations and their lifetimes
  • CPU overhead: Runtime resolution is slower than compile-time instantiation
  • Startup time: Complex dependency graphs increase application startup time
  • Debugging complexity: Stack traces become cluttered with container resolution code

Consider a simple controller that now requires half a dozen injected dependencies:

Dependency Injection Example

This constructor is a maintenance nightmare. Adding a new dependency requires touching multiple files, and understanding what this controller actually does requires tracing through multiple service layers.

The Performance Tax

Each of these patterns carries a performance cost that accumulates across the application:

  • Interface calls: Virtual method calls are slower than direct calls
  • DI resolution: Runtime object creation is slower than compile-time instantiation
  • Excessive abstraction: Multiple layers of services calling other services increase call stack depth
  • Memory allocation: More objects, more interfaces, more garbage collection pressure

In a typical web application, these costs might seem negligible for individual requests, but they compound. A simple operation that could be a direct database call becomes a journey through multiple service layers, each with its own overhead.

The Maintainability Paradox

The cruelest irony is that these patterns, adopted in the name of maintainability, often make code harder to maintain:

  • Onboarding complexity: New developers must learn the DI container configuration, understand the interface hierarchy, and navigate the service layer maze
  • Debugging difficulty: Following code execution requires jumping between interfaces and implementations
  • Change resistance: Simple changes require updates to multiple interfaces, services, and tests
  • Analysis paralysis: Developers spend more time designing abstractions than solving business problems

A More Pragmatic Approach

This isn’t an argument against interfaces, testing, or dependency injection entirely—it’s a call for pragmatic application:

Use interfaces when you actually need them:

  • When you genuinely have multiple implementations
  • When you’re defining a contract for external consumers
  • When you need to isolate external dependencies for testing

Focus on integration tests:

  • Test the behavior users actually care about
  • Use unit tests sparingly for complex business logic
  • Don’t test implementation details

Apply dependency injection judiciously:

  • Use it for cross-cutting concerns (logging, configuration)
  • Consider direct instantiation for simple object graphs
  • Don’t inject everything just because you can

Embrace simplicity:

  • Static classes aren’t evil for stateless operations
  • Direct database access isn’t always wrong
  • Simple, straightforward code is often the best code

Conclusion

The .NET community has created a culture where complexity is mistaken for sophistication, and over-engineering is rewarded as “best practice.” We’ve lost sight of the primary goal: building software that works reliably and can be understood and modified by the developers who come after us.

It’s time to step back and ask: Does this interface actually provide value? Does this test actually catch bugs? Does this abstraction actually make the code easier to change?

More often than not, the answer is no. It’s time to build simpler, more maintainable .NET applications by using the right tool for the job—not every tool we have available.

Ripshout: Download Shoutcast Radio While You Listen

Github page

RipShout

A ShoutCast Internet Radio Stream Player, Ripper, Best Friend.

I love listening to Digitally Imported and RadioTunes radio while I’m working but like normal radio, I don’t always catch the name of a song that I like and am left with great sadness hoping it comes on again. RipShout helps with this by recording all the music you hear all day, breaking it up in to nice MP3s with album art (if possible) and artist, album tags so you can play through the songs and find the one that you were rocking out to to buy on iTunes or whatever.

It also fetches fan art and album art and rotates it Zune style thereby making your music listening experience much more awesome. I hope you enjoy this as much as I do.

MlaHA9ogrX

Ob1vDNLP9D

Installing

You can install by simply downloading the msi from the release page

Via WinGet:

winget install FleetingClarityStudios.RipShout

image

image

image

image

image

Peepr: A simple, lightweight image and video viewer

Github page

Peepr

Peepr is a simple image and video viewer for windows. I created Peepr because I often have folders with a mish mash of images and videos of all formats and if I want to browse through them all at once I have to use an image viewer like ImageGlass and then open the videos with VLC and forget doing a slideshow with everything.

So I made Peepr to handle everything itself. It was also a good excuse to try building with AvaloniaUI which I recommend.

image image

Features

  • Supports all common image formats
  • Supports all common video formats (It uses VLC’s library)
  • Navigate using arrow buttons, keyboard arrow keys or forward / back buttons on your mouse
  • Slideshow! (Delay configurable)
  • Escape to close
  • Delete to delete file
  • Optional associate Images and or Videos with Peepr

Simplicity

Peepr is designed to be simple. If you want to edit your images, use an image editor that is built for that. Peepr is just for peeping. ;)

Installation

In the release section either download the install.exe or the Peepr.zip Both run as portable but the installer is just a little simpler and puts a shortcut on your desktop. If you download the zip, extract it to a folder of your choice and run Peepr.exe

Download From Releases

image

image