Published
- 4 min read
5 toxic ingredients to avoid in your everyday .NET app

Today’s issue is brought to you by the Real World Web Development with .NET 9 .
“Real World Web Development with .NET 9” by best-selling author Mark J. Price helps you build professional websites and services using proven technologies like ASP.NET Core MVC, Web API, and OData—trusted by organizations for delivering robust web applications.
Find out more here: Build professional web apps
The silent killer of web apps:
Enterprise overengineering.
Some devs think they’re building the next Amazon. So they put CQRS, event buses, mediators, and five service layers into a simple CRUD app.
The result?
Every change requires going through:
- 3 interfaces,
- 17 files,
- across 5 different projects
Just to return an additional field from your database.
Meanwhile, productivity tanks. Features that should take hours now take weeks. And all because someone thought “more layers” meant “more professional.”
But how do you recognize that the app is being overengineered?
Here are 5 symptoms.
1. An interface for every class
This one I see a lot.
There is a:
- IOrderService
- IUserService
- IDocumentService
Interface that is injected and called from a controller.
But here is the thing. That interface doesn’t have any purpose. It’s relevant like a semaphore in GTA.
Why?
- Only one implementation exists.
- It’s not like you need it to write unit tests for a controller (please don’t do this.)
- You don’t need to abstract a service class from the controller.
- It adds unnecessary indirection when there’s only one implementation and no plans for multiple.
Eliminate this interface.
You can always add it later using “Extract Interface” refactoring.
2. MediatR for all use cases
I don’t use MediatR.
But it can be beneficial when you need support for pipelines and notifications.
The problem with forcing MediatR everywhere is that it combines multiple layers:
- Handlers,
- Requests,
- Responses,
- Pipelines
Into one tangled abstraction, forcing your code through unnecessary indirection at every turn.
3. Layered/Clean architecture with way too many projects
Some “Clean Architecture” templates go overboard and introduce a project for every file type. So you end up with a separate project for:
- Domain,
- Application,
- Infrastructure,
- Web,
- Common,
- Contracts,
- Identity,
- Internalization,
- SharedKernel…
Where does that lead you?
Well, every change becomes a cross-project refactor, even when you’re just tweaking a view model. Let’s not forget that there is a different DTO for communication between layers.
And to make your life easier with mapping all those DTOs, you end up using..
4. Mapping all models with AutoMapper
We developers are drawn to complexity, like bugs are to light.
For example, object mapping.
The complex solution? A mapping library.
Problems:
- Debugging complexity - Mapping code is a black box. Errors could be challenging to trace.
- Performance penalty - Reflection introduces performance overhead.
- Risks of adding yet another library - One more dependency you need to worry about.
- Run-time errors - The code compiles. But it breaks when you run it.
The simplest solution? Constructor in a DTO (data transfer object).
Advantages:
- Simplicity
- Performance
- No external dependencies
- Type safety
5. Abstracting the database abstraction
Developers like to slap a Repository + Unit of Work combo on top of the EF Core. And claim that they have perfectly isolated the database “in case we need to switch databases”.
But let’s be honest, how often do you change your database?
And when you do, then it’s a huge undertaking that requires:
- Rewriting queries
- Migrating schemas
- Handling data inconsistencies
- Retesting everything end-to-end
- Writing data to the old and new databases during the transition
- Handling downtime during the switch in production
And the repository layer can’t help you handle all that more easily.
On the other hand, having a double abstraction leads to:
- More boilerplate code
- Extra indirection when debugging
- Reduced access to EF Core’s powerful features
EF Core is already a repository and a unit of work. Instead of wrapping it in layers you don’t need, embrace what it gives you out of the box. You’ll write less code and move faster.
Sometimes, abstraction is just extra work with no real upside.
Here’s the truth:
It’s easy to mistake complexity for professionalism.
We copy what big tech companies do, forgetting they operate at a different scale and with different needs. But complexity should solve real problems. Not create them.
A better approach:
- Start simple.
- Scale complexity only when the app demands it.
- Build for today. Not for some fantasy scale that may never come.
Complexity isn’t a badge of honor.
It’s a tax on everyone who comes after you.
Every Friday I share actionable .NET advice, insights, and tips to learn the latest .NET concepts, frameworks, and libraries inside my FREE newsletter.
Join here 7,100+ other developers