What It Is
Hexagonal architecture, often called ports and adapters, is a way of structuring software so the application core does not depend directly on UI frameworks, databases, message brokers, or third-party APIs. Instead, the core defines ports, and outer adapters implement those ports or translate external input into forms the core can use.
The shape is called hexagonal not because six sides matter, but because the application is shown in the center with multiple equally valid entry and exit points around it. HTTP is just one adapter. A CLI, a test harness, a queue consumer, or a scheduled job could drive the same use case through a different adapter.
Why Teams Use It
Teams use hexagonal architecture when they want the application core to remain stable while integrations change. It is especially useful when the same business capability may be triggered through more than one interface or when external systems are volatile enough that direct coupling would make testing and change harder.
The Core Idea: Ports and Adapters
A port is a boundary defined by the application. It expresses what the core needs from the outside world or what the outside world can ask the core to do.
- Inbound ports describe use cases the application exposes, such as creating an order or resetting a password
- Outbound ports describe dependencies the application needs, such as loading an order, sending an email, or publishing an event
- Adapters sit on the outside and translate between technical protocols and those ports
This allows the application core to speak its own language while adapters deal with HTTP, SQL, SDKs, queues, and file formats.
How It Differs from Clean and Onion
Hexagonal, clean, and onion architecture all aim to protect business logic from infrastructure concerns. In practice, they overlap heavily, and many codebases could reasonably be described by more than one label.
Hexagonal architecture usually emphasizes interaction boundaries most directly. The language of ports and adapters makes it very explicit which parts of the system are input mechanisms, which are output mechanisms, and where the application core sits between them.
Pros
- The application core stays independent from framework and transport details
- Testing becomes easier because adapters can be replaced with simple in-memory implementations
- Multiple entry points can reuse the same application behavior without duplicating business rules
- Replacing a database, queue, or external provider is more manageable when dependencies are behind outbound ports
- The architecture makes integration boundaries visible instead of letting them spread across the codebase
Cons
- It introduces extra abstractions that can feel heavy in a small app
- If every action becomes a port and adapter too early, the structure can become more ceremonial than useful
- Teams can over-engineer boundaries around simple dependencies that are unlikely to change
- Poor naming can make ports feel vague, especially when they mirror technical details instead of business intent
Common Misconceptions
- "The hexagon shape defines the design." The drawing is just a teaching device. The important part is the dependency boundary, not the number of sides.
- "Ports are just interfaces for everything." A useful port represents a meaningful application boundary, not a reflex to abstract every class.
- "Controllers are the application." In a hexagonal design, controllers, endpoints, and consumers are adapters. They should translate and delegate, not own business decisions.
- "Hexagonal architecture is only for microservices." It works inside a monolith too, especially when you want strong seams around domain behavior and integrations.
When to Choose It
Choose hexagonal architecture when your application has real business workflows and several technical integration points, or when the same use case should be callable from multiple interfaces. It is especially helpful when you want testable application services that are not tied to HTTP, a specific ORM, or one messaging technology.
It is a weaker fit for a tiny application with one interface and minimal domain behavior. In that situation, the cost of ports, adapters, and mapping layers may exceed the value until the application grows.
Example Structure
One way to organize a .NET solution is to make the core explicit and keep adapters at the edge:
src/
Billing.Core/
Ports/
In/
ICreateInvoiceUseCase.cs
Out/
IInvoiceRepository.cs
IEmailGateway.cs
Invoices/
CreateInvoice/
CreateInvoiceCommand.cs
CreateInvoiceService.cs
Domain/
Invoice.cs
Billing.Adapters.Web/
Controllers/
InvoicesController.cs
Requests/
CreateInvoiceRequest.cs
Billing.Adapters.Persistence/
EfCoreInvoiceRepository.cs
BillingDbContext.cs
Billing.Adapters.Notifications/
SendGridEmailGateway.cs
Billing.Bootstrap/
DependencyInjection.cs
Program.csThe naming varies by team, but the idea is consistent: the core defines the contracts, and the adapters implement or drive them.
Code Example
A service in the core can depend on outbound ports while an HTTP controller acts only as an inbound adapter.
// Billing.Core/Ports/Out/IInvoiceRepository.cs
namespace Billing.Core.Ports.Out;
public interface IInvoiceRepository
{
Task AddAsync(Invoice invoice, CancellationToken cancellationToken = default);
}
// Billing.Core/Invoices/CreateInvoice/CreateInvoiceService.cs
namespace Billing.Core.Invoices.CreateInvoice;
public sealed class CreateInvoiceService : ICreateInvoiceUseCase
{
private readonly IInvoiceRepository _invoiceRepository;
private readonly IEmailGateway _emailGateway;
public CreateInvoiceService(IInvoiceRepository invoiceRepository, IEmailGateway emailGateway)
{
_invoiceRepository = invoiceRepository;
_emailGateway = emailGateway;
}
public async Task Handle(CreateInvoiceCommand command, CancellationToken cancellationToken)
{
var invoice = Invoice.Create(command.CustomerId, command.Amount);
await _invoiceRepository.AddAsync(invoice, cancellationToken);
await _emailGateway.SendInvoiceCreatedAsync(invoice.Id, cancellationToken);
}
}
// Billing.Adapters.Web/Controllers/InvoicesController.cs
[ApiController]
[Route("api/invoices")]
public sealed class InvoicesController : ControllerBase
{
[HttpPost]
public Task Create(
[FromBody] CreateInvoiceRequest request,
[FromServices] ICreateInvoiceUseCase useCase,
CancellationToken cancellationToken)
{
return useCase.Handle(new CreateInvoiceCommand(request.CustomerId, request.Amount), cancellationToken);
}
}The controller knows about HTTP. The service knows about the use case. The repository and email gateway adapters know about technical implementation. Each part stays in its lane.
Conclusion
Hexagonal architecture is a strong option when you need your application core to remain stable across changing integrations and multiple delivery mechanisms. It adds abstraction, so it should be used intentionally, but its ports and adapters model gives teams a clear way to separate business behavior from technical plumbing.