Use abstraction carefully in .NET

Abstraction is a powerful concept in software design

Abstraction is a powerful concept in software design, but over-abstraction can inadvertently harm your project. Here’s a real experience from a .NET Core (C#) project.

-> The Problem: Abstraction Done Too Early
In one of our enterprise .NET Core applications, we aimed for “perfect architecture” and created multiple interfaces for a simple CRUD-based User module, including:
– IUserService
– IUserManager
– IUserProcessor
– IUserRepository
– IBaseRepository<T>
– IReadOnlyRepository<T>

Example:

public interface IUserService 
{ 
 Task<UserDto> GetUserAsync(int id); 
} 

public class UserService : IUserService 
{ 
    private readonly IUserManager _userManager; 

    public UserService(IUserManager userManager) 
    { 
        _userManager = userManager; 
    } 

    public async Task<UserDto> GetUserAsync(int id) 
    { 
        return await _userManager.GetUserAsync(id); 
    } 
}

This resulted in:
– No business logic
– Just method-to-method forwarding
– More files, more DI registrations, more confusion

-> Real Impact on the Project
– New developers took longer to understand the flow
– Debugging required jumping across 4–5 layers
– Change requests took more time
– “Flexible architecture” became hard to maintain

-> The Fix: Abstraction Where It Matters
We refactored by asking, “Is this abstraction solving a real problem today?”
What we kept:
– Repository abstraction (DB may change)
– Service layer only where business logic exists

-> What we removed:
– Pass-through interfaces
– Premature layers

-> Simplified version:

public class UserService
{
    private readonly UserRepository _repository;

    public UserService(UserRepository repository)
    {
        _repository = repository;
    }

    public async Task<UserDto> GetUserAsync(int id)
    {
        var user = await _repository.GetByIdAsync(id);

        if (!user.IsActive)
            throw new Exception("Inactive user");

        return MapToDto(user);
    }
}

– Clear
– Readable
– Easy to change
– Faster onboarding

-> Following are the important points:
– Abstraction is a tool, not a rule
– Don’t abstract until you see variation or change
– YAGNI still applies in modern .NET Core projects
– Simple code scales better than “clever” architecture

Good architecture evolves -> it is not forced on Day One.