Hexagonal Architecture in Modern .NET

Architecture discussions often start with diagrams. They focus on shapes, layers, or folder structures. That is not where real architectural problems come from. They come from change. Modern .NET systems live in a constantly shifting environment. Databases evolve. Cloud infrastructure changes. New integrations appear. Performance requirements force caching and messaging layers to be introduced. Over time, systems must adapt or they become brittle.
Hexagonal architecture provides a way to protect the core business logic of a system from the volatility of the outside world. If your system has a well defined domain surrounded by uncertain infrastructure then this pattern could well save you hours of pain.
The modern .NET reality
Today’s .NET applications look very different from the classic MVC layered systems of the past.
We now build systems using minimal APIs, dependency injection everywhere, EF Core, background workers, distributed caching, and messaging infrastructure. Many Developers also adopt vertical slice architecture, where each feature is implemented as an independent unit. This evolution introduces a new risk. Modern applications interact with far more external systems than before. Without strong boundaries, business logic quickly becomes tightly coupled to infrastructure.
You start seeing patterns like this.
public async Task CreateUser(CreateUserRequest request)
{
var user = new User(request.Email);
_dbContext.Users.Add(user);
await _dbContext.SaveChangesAsync();
await _redisCache.SetStringAsync($"user:{user.Id}", user.Email);
}
This looks harmless. But the domain logic is now directly tied to EF Core and Redis. Changing persistence or caching strategies would require modifying business logic.
Hexagonal architecture prevents this coupling.
The core principle
The central rule is simple.
All dependencies must point inward toward the domain.
The domain must never depend on infrastructure.
Instead of directly using concrete technologies, the domain defines ports, which are abstractions describing what it needs from the outside world.
Adapters implement these ports using real infrastructure.
This ensures the domain remains stable even as infrastructure evolves.

A real code example
Consider a simple use case: creating a user.
In hexagonal architecture, the domain defines what it needs without referencing infrastructure.
Step 1. Define a port in the domain
public interface IUserRepository
{
Task AddAsync(User user, CancellationToken stopToken);
}
This interface belongs to the domain. It represents a capability, not a technology.
The domain does not know whether this will be implemented using EF Core, a REST API, or a message queue.
Step 2. Implement the port in infrastructure
internal sealed class EfUserRepository(UserDbContext db)
: IUserRepository
{
public async Task AddAsync(User user, CancellationToken stopToken)
{
db.Users.Add(user);
await db.SaveChangesAsync(stopToken);
}
}
This adapter translates domain operations into concrete persistence logic.
The domain remains completely unaware of EF Core.
Step 3. Use the port in an application handler
This is where modern .NET patterns come in.
A vertical slice handler orchestrates the use case.
internal sealed class CreateUserHandler(
IUserRepository repository)
{
public async Task Handle(CreateUserCommand command,
CancellationToken stopToken)
{
var user = User.Create(command.Email);
await repository.AddAsync(user, stopToken);
}
}
The handler depends only on abstractions defined by the domain.
Infrastructure details are invisible.
How minimal APIs fit naturally
Modern .NET minimal APIs integrate cleanly with this approach.
The endpoint simply delegates to the handler.
app.MapPost("/users", async (
CreateUserCommand cmd,
CreateUserHandler handler,
CancellationToken stopToken) =>
{
await handler.Handle(cmd, stopToken);
return Results.Ok();
});
Notice what is missing.
There is no EF Core. No Redis. No infrastructure logic. The endpoint remains thin and focused on HTTP concerns.
Introducing additional infrastructure without touching the domain
This is where hexagonal architecture shines. Suppose performance requirements demand caching.You do not change domain logic. Instead, you introduce a new adapter.
internal sealed class CachedUserRepository(
IUserRepository inner,
IDistributedCache cache)
: IUserRepository
{
public async Task AddAsync(User user, CancellationToken stopToken)
{
await inner.AddAsync(user, stopToken);
await cache.SetStringAsync(
$"user:{user.Id}",
user.Email,
stopToken);
}
}
The domain remains untouched. The application logic remains untouched. Only the infrastructure layer evolves.
How this works with vertical slice architecture
Vertical slice architecture and hexagonal architecture address different concerns. Vertical slice focuses on organising code around features. Hexagonal focuses on protecting the domain.
Together they create a powerful combination.

The handler orchestrates the use case. The domain enforces business rules. Ports define boundaries. Adapters implement infrastructure.
This structure scales extremely well in large systems.
in real enterprise systems
In small applications, tight coupling may never cause serious problems. In long-lived enterprise systems, it always does. Systems evolve. Databases change. Caching layers are introduced. Messaging becomes necessary. Integrations multiply. Without hexagonal boundaries, each change spreads throughout the codebase. With hexagonal architecture, change remains localized to adapters.
This drastically reduces risk over time.
The balance
The most common way Developers go too far with hexagonal architecture is by turning it into an abstraction factory instead of a protection mechanism. They start creating ports for everything, not just for the parts of the system that are truly volatile. You end up seeing interfaces wrapping simple EF Core queries, adapters that add no real value, and multiple layers of indirection that make the code harder to follow without actually improving flexibility. At that point, the architecture is no longer protecting the domain, it is just adding friction.
Another sign of overdoing it is when the cost of change inside the application becomes higher than the cost of change in infrastructure. For example, if adding a simple new feature requires creating an interface, an adapter, a decorator, a registration class, and several test doubles, you have crossed the line. The purpose of hexagonal architecture is to reduce the impact of external change, not to make everyday development slower or more complex. Developers also run into trouble when they apply hexagonal boundaries to unstable parts of the system. Early in a project, business rules are still evolving rapidly. If you lock those areas behind rigid abstractions too soon, you actually make the system harder to adapt. Hexagonal works best when the domain concepts are reasonably well understood and likely to remain stable over time. Protecting something that is still in flux just creates unnecessary churn.
The healthy balance is to be selective. Apply hexagonal boundaries where infrastructure volatility is high and domain stability is strong. Leave simple CRUD paths, reporting queries, and transient workflows more direct and pragmatic. In other words, treat hexagonal architecture like armour. You put it around the parts of the system that must endure long-term pressure, not around everything by default. It provides a simple but powerful principle for modern .NET systems. Keep business logic at the centre. Define clear boundaries using ports. Implement infrastructure at the edges using adapters. This approach allows systems to evolve safely as requirements and technologies change.





