Hexagonal Architecture in .NET: Ports and Adapters
Same rule as lesson 249, different picture: instead of circles, a hexagon with sockets — Ports — and the plugs that fit them — Adapters.
Lesson 249 drew Clean Architecture as concentric circles: Domain at the center, Infrastructure and Presentation further out, dependencies only ever pointing inward. A few years before that model became popular, Alistair Cockburn described what turns out to be essentially the same underlying idea, using a completely different picture: a hexagon, with sockets cut into its sides. He called the sockets Ports, and called whatever plugs into them Adapters.
Hexagonal Architecture — also called Ports and Adapters — isn't a rival to Clean Architecture, and it isn't a new rule to learn from scratch. It's the same goal — isolate your core business logic from infrastructure detail, keep dependencies pointing inward — described through a different, equally influential vocabulary. Once you know one, recognizing the other is mostly a matter of translating terms.
In this lesson, you'll learn the Ports and Adapters model precisely, see exactly how it connects to the Adapter Pattern (243) you already learned — because Adapter isn't just similar to this architecture's mechanism, it literally is the mechanism — and build a small, concrete slice showing a port and two interchangeable adapters implementing it.
What Is It?
The Simple Explanation
Hexagonal Architecture puts your application's core logic in the center of a hexagon. The hexagon's sides have sockets cut into them — Ports — each one describing something the core needs from the outside world, or offers to it. Outside the hexagon, Adapters plug into those sockets, one per specific technology: a REST API adapter, a database adapter, a message-queue adapter. The core never touches a specific technology directly — it only ever talks through a Port.
The Technical Definition — Ports and Adapters
Port
- An interface, owned by the application core, describing a capability the core needs (a driven or "outbound" port, like persistence) or offers (a driving or "inbound" port, like a use-case entry point)
- Defined entirely in terms the core cares about — never in terms of any specific technology
Adapter
- A concrete class implementing a Port for one specific technology —
EfOrderRepository,StripePaymentAdapter,OrdersApiController - Lives entirely outside the hexagon; the core never references an Adapter's concrete type
A driving Port (also called a primary or inbound Port) is how something outside asks the core to do work — an IPlaceOrderUseCase that a REST controller Adapter calls into. A driven Port (secondary, or outbound) is how the core asks something outside to do work for it — an IOrderRepository that the core calls, implemented by a database Adapter. Both are still just interfaces owned by the core; the only difference is which side initiates the call.
Why Does It Exist?
- Cockburn observed the same recurring problem Clean Architecture's authors would later describe: business logic quietly wired directly to a specific UI framework, a specific database, a specific external service — so testing the logic meant standing up a real database, and swapping any one piece of infrastructure meant rewriting logic that had nothing to do with that infrastructure
- A way to describe, precisely, that the application core should be "symmetric" — equally isolated from a UI on one side and a database on the other — with a vocabulary that treats every external technology, whether it drives the application or is driven by it, the same structural way
- Ports (interfaces the core owns) and Adapters (technology-specific implementations plugged in from outside) — the hexagon's sides represent as many Ports as the core needs, with no special status given to any one side, unlike a layered diagram that visually implies a single "top" (UI) and a single "bottom" (database)
Big Picture — Two Names for One Underlying Rule
Line the two vocabularies up side by side, and the mapping is almost mechanical:
| Clean Architecture (249) | Hexagonal Architecture | Same underlying idea |
|---|---|---|
| Domain / Application (inner rings) | The hexagon's core | Business logic, isolated from technology |
| An interface the Domain/Application layer defines (e.g. IProductRepository) | A Port | An abstraction the core owns, shaped around what the core needs |
| An Infrastructure implementation (e.g. EfProductRepository) | An Adapter | A technology-specific implementation, plugged in from outside |
| The Dependency Rule — arrows point inward | Adapters depend on Ports, never the reverse | The core never knows a specific technology exists |
| Concentric circles | A hexagon with sockets | Two different pictures of the identical structural rule |
The genuinely new idea Hexagonal contributes — beyond just a different picture — is treating the UI side and the persistence side symmetrically. A layered/concentric-circle diagram visually suggests a single, special "top" layer (Presentation) and a single, special "bottom" layer (Infrastructure). A hexagon has no top or bottom — just however many sides the core actually needs Ports on, each one exactly as isolated from the core as every other.
How It Works — Adapter Pattern (243), at Architectural Scale
- IOrderNotifier, declared inside the application core, with a method shaped around the core's own vocabulary — NotifyOrderShippedAsync(Order order), not "SendSmtpMessage(host, port, ...)"
- SmtpOrderNotifierAdapter : IOrderNotifier — the only class that knows an SMTP client exists at all. This is precisely lesson 243's Adapter Pattern: converting one interface (the vendor SDK's shape) into another (the core's Port) without modifying either side.
- SlackOrderNotifierAdapter : IOrderNotifier — a completely different technology, same Port, zero changes required inside the core
- services.AddScoped<IOrderNotifier, SmtpOrderNotifierAdapter>(); — exactly the same composition-root responsibility from lesson 238, whether you call the abstraction an "interface," an "abstraction owned by the inner layer," or a "Port."
Simple Example — One Port, Two Adapters
// ═══ APPLICATION CORE — the hexagon's inside. No framework references at all. ═══
// A DRIVEN (outbound) Port — the core describes what IT needs, in its own vocabulary
public interface IOrderNotifier
{
Task NotifyOrderShippedAsync(string customerEmail, string trackingNumber);
}
public sealed class PlaceOrderUseCase(IOrderRepository repository, IOrderNotifier notifier)
{
public async Task ShipAsync(int orderId)
{
var order = await repository.GetByIdAsync(orderId)
?? throw new InvalidOperationException("Order not found.");
order.MarkShipped();
await repository.SaveAsync(order);
await notifier.NotifyOrderShippedAsync(order.CustomerEmail, order.TrackingNumber!);
// PlaceOrderUseCase has NO IDEA whether email, Slack, or SMS is behind IOrderNotifier.
}
}
// ═══ ADAPTERS — outside the hexagon. Each one owns exactly one technology's detail. ═══
public sealed class SmtpOrderNotifierAdapter(SmtpClient smtpClient) : IOrderNotifier
{
public async Task NotifyOrderShippedAsync(string customerEmail, string trackingNumber) =>
await smtpClient.SendMailAsync(new MailMessage("orders@shop.com", customerEmail)
{
Subject = "Your order has shipped!",
Body = $"Tracking number: {trackingNumber}"
});
}
public sealed class SlackOrderNotifierAdapter(HttpClient httpClient) : IOrderNotifier
{
public async Task NotifyOrderShippedAsync(string customerEmail, string trackingNumber) =>
await httpClient.PostAsJsonAsync("/webhook", new { text = $"Order shipped, tracking: {trackingNumber}" });
}
// ═══ COMPOSITION ROOT — Program.cs — the ONLY place the concrete Adapter is chosen ═══
builder.Services.AddScoped<IOrderNotifier, SmtpOrderNotifierAdapter>();
// Swap to SlackOrderNotifierAdapter here, and NOTHING inside PlaceOrderUseCase changes.Code → Meaning → Result: PlaceOrderUseCase is the hexagon's core — it depends on IOrderNotifier, a Port it effectively owns, and never on SmtpClient or HttpClient directly. Both notifier classes are Adapters, each translating the core's simple, vocabulary-shaped request into whatever a specific technology actually requires — precisely the Adapter Pattern (243) from earlier in this Part, operating at the boundary of the entire application instead of around one third-party class.
Real-World Example — Testing the Core Without Any Real Infrastructure
Because PlaceOrderUseCase depends only on Ports, a unit test can supply fake, in-memory Adapters and exercise the core's actual business logic — the rule that shipping an order marks it shipped and notifies the customer — without touching a real database, a real SMTP server, or a real Slack workspace:
public sealed class InMemoryOrderRepository : IOrderRepository
{
private readonly Dictionary<int, Order> _orders = new();
public Task<Order?> GetByIdAsync(int id) => Task.FromResult(_orders.GetValueOrDefault(id));
public Task SaveAsync(Order order) { _orders[order.Id] = order; return Task.CompletedTask; }
}
public sealed class RecordingOrderNotifier : IOrderNotifier
{
public List<string> NotifiedEmails { get; } = [];
public Task NotifyOrderShippedAsync(string customerEmail, string trackingNumber)
{
NotifiedEmails.Add(customerEmail);
return Task.CompletedTask;
}
}
// The test wires the SAME PlaceOrderUseCase to fake Adapters — no SMTP server, no real database
var repository = new InMemoryOrderRepository();
var notifier = new RecordingOrderNotifier();
var useCase = new PlaceOrderUseCase(repository, notifier);
// ... seed an order, call ShipAsync, then assert notifier.NotifiedEmails contains the customer.This is the practical payoff Ports and Adapters is built to deliver: the exact same core logic runs in production behind real Adapters, and in a test suite behind fake ones — with PlaceOrderUseCase itself completely unaware of, and unaffected by, which situation it's in.
Analogy — a Wall Socket and Every Country's Plug
An electrical wall socket defines a standard shape — a Port. It doesn't know or care whether a lamp, a laptop charger, or a vacuum cleaner is plugged in; it just supplies power to whatever fits its standard shape. Each specific appliance has a plug — an Adapter — shaped to match the socket, hiding the appliance's own internal complexity behind that one standard interface. Travel to a different country, and only the plug (Adapter) needs to change — the socket's underlying purpose, and the appliance's own inner workings, stay exactly the same.
Your application core is the electrical system inside the wall — it defines Ports, standard shapes it needs filled. A REST controller, a database, a message queue are all "appliances" plugging in through Adapters shaped to fit. Swap the database technology, and only that one Adapter — the plug — needs replacing; the core's wiring never gets touched.
Under the Hood — Driving vs. Driven, Made Concrete
- IPlaceOrderUseCase — an ASP.NET Core controller (a driving Adapter) calls this Port to trigger core logic. The controller depends on the Port; the core doesn't depend on the controller at all.
- IOrderRepository — the core calls this Port to persist an order; an EF Core Adapter (a driven Adapter) implements it. The core still owns the interface; the database technology still knows nothing about the core beyond that interface.
- Whether an Adapter is calling INTO the core (driving) or the core is calling OUT to an Adapter (driven), the Adapter always depends on the Port — never the reverse. This is exactly Clean Architecture's Dependency Rule (249), restated without reference to "inner" or "outer" at all — just "the core defines it, an Adapter implements or calls it."
Common Confusion
"Should I use Clean Architecture or Hexagonal Architecture?" — this is the wrong question
They're not two competing choices you pick between, the way you'd pick between two different database technologies. They're two well-known, historically influential ways of describing essentially the same underlying structural rule — isolate the core, dependencies point inward, infrastructure is replaceable detail. Many real teams say "Clean Architecture" and then draw a hexagon, or say "Hexagonal" and then organize their solution into concentric-circle-style projects. Picking a vocabulary is a communication choice for your team, not an architectural decision with different runtime consequences.
"Every interface is a Port" — not quite
Every Port is an interface, but not every interface in your codebase is meaningfully a "Port" in the Hexagonal sense. The term specifically means an interface sitting at the application core's actual boundary with the outside world — persistence, external services, the UI's entry point into the core. An interface used purely for internal polymorphism inside the core (like a Strategy from lesson 241, entirely within the domain) isn't a Port; it's just an ordinary interface used for an unrelated, internal reason.
Common Mistakes
Mistake 1 — Shaping a Port around the Adapter's technology instead of the core's needs
Defining IOrderNotifier with a method like SendSmtpMessageAsync(string host, int port, MailMessage message) — the Port has quietly absorbed SMTP's shape, so a Slack Adapter would have to awkwardly pretend to be an SMTP server just to satisfy the interface. Shape every Port entirely around what the core needs to express — NotifyOrderShippedAsync(string customerEmail, string trackingNumber) — and let each Adapter absorb the translation cost, exactly as lesson 243 taught.
Mistake 2 — Treating "Hexagonal" and "Clean Architecture" as requiring a debate over which is "more correct"
Spending a design meeting arguing whether a project should be "Hexagonal" or "Clean," as if choosing wrong would produce a worse system. Recognize both describe the identical underlying discipline; pick whichever vocabulary your team already understands and communicates with most naturally, and move on to the actual work of isolating the core.
Mistake 3 — Adding Ports for things that will never realistically have a second Adapter
Defining a Port and a single Adapter for something that will provably never be swapped or faked in a test — pure ceremony, adding indirection with no real payoff. Just as lesson 240 taught for Factory, a Port earns its place when there's a genuine reason to keep the core ignorant of a specific technology — commonly, for testability, or because the technology genuinely might change — not automatically for every single external touchpoint.
When Should I Use It?
| Situation | Leans toward |
|---|---|
| A team already fluent in Clean Architecture's ring vocabulary | Keep using rings — the underlying discipline is identical; don't force a vocabulary switch without a reason |
| An application that genuinely has multiple driving Adapters (a REST API AND a message-queue consumer AND a CLI, all triggering the same core logic) | Hexagonal's symmetric "no special top layer" framing communicates this shape especially clearly |
| A small application with one UI and one database, unlikely to ever swap either | Same guidance as lesson 249 — the full Ports-and-Adapters ceremony is real, avoidable overhead here |
| Deciding between "Hexagonal" and "Clean Architecture" as if they were mutually exclusive | Reconsider the framing — they're the same underlying goal, described two well-known ways |
Mental Model
Adapter = a technology-specific implementation of a Port, living entirely outside the core.
The rule = Adapters depend on Ports; the core never depends on a specific Adapter.
Remember: this is Clean Architecture's Dependency Rule (249), and literally the Adapter Pattern (243), described through a hexagon instead of concentric circles — not a competing idea.
Key Takeaway
- Hexagonal Architecture — Ports and Adapters — puts the application core at the center of a hexagon; Ports are interfaces the core owns, and Adapters are technology-specific implementations plugged in from outside.
- Driving (inbound) Ports let something outside call into the core; driven (outbound) Ports let the core call out to something outside — in both directions, the Adapter depends on the Port, never the reverse.
- This is literally the Adapter Pattern (243) applied at the scale of an entire application's boundary — the same "translate one interface shape into another" mechanism, just centered on the whole core instead of one third-party dependency.
- Hexagonal and Clean Architecture (249) pursue the identical goal — isolate business logic, dependencies point inward — through different vocabulary and a different visual metaphor. They are not competing choices.
- The genuine payoff Hexagonal emphasizes: a symmetric core, equally isolated from every direction — UI, database, external service alike — with no single side treated as structurally special.
Check Your Understanding
You've mapped Hexagonal Architecture's vocabulary onto Clean Architecture's, and onto the Adapter Pattern you already knew. Let's confirm the mapping holds.
1. In the Ports and Adapters model, what is a "Port"?
Show answer
Correct: B
Why B is correct: A Port is precisely the abstraction the application core owns — an interface shaped around what the core needs or offers, deliberately independent of any specific technology's own conventions.
Why A is incorrect: A concrete, technology-specific implementing class is an Adapter, not a Port — Ports are the interfaces Adapters implement.
Why C is incorrect: This is an unrelated, coincidental use of the word "port" from networking — Hexagonal Architecture's "Port" is a design-pattern term, not a TCP/IP concept.
Why D is incorrect: The composition root (238) is where concrete Adapters get bound to Ports — it's a related but distinct concept from the Port abstraction itself.
Reinforcement: Port = interface owned by the core. Adapter = technology-specific implementation of that interface.
2. Why does this lesson state that Hexagonal Architecture is literally the Adapter Pattern (243), rather than merely similar to it?
Show answer
Correct: B
Why B is correct: The mechanism is identical, not just similarly named — a Hexagonal Adapter translates a specific technology's shape into the interface the core actually depends on, precisely the structural definition of the Adapter Pattern from lesson 243, just applied at architectural scale.
Why A is incorrect: The shared name reflects a genuinely shared mechanism, not a coincidence — this lesson explicitly traces the structural identity between the two.
Why C is incorrect: Neither the Adapter Pattern nor Hexagonal Architecture requires inheritance — both are built on interfaces implemented by concrete classes.
Why D is incorrect: Adapters can wrap any external technology — email, message queues, REST APIs, UI controllers — not exclusively databases.
Reinforcement: Recognizing the same named pattern recurring at a larger scale is a recurring theme across this Part — Dependency Inversion (081) inside Clean Architecture (249), and now Adapter (243) inside Hexagonal Architecture.
3. A team is debating whether their new project "should be Hexagonal or Clean Architecture," treating it as a decision with different runtime consequences. What does this lesson say about that framing?
Show answer
Correct: B
Why B is correct: This is the lesson's central Common Confusion point — the two are the same underlying discipline, described two different, well-known ways; picking one is a communication choice for the team, not a decision with different structural outcomes.
Why A is incorrect: Neither is "superior" — they emerged independently around the same underlying insight and are used interchangeably by real teams today.
Why C is incorrect: Neither vocabulary is tied to a specific application type — both apply equally to web APIs, console apps, or any other application shape.
Why D is incorrect: The lesson explicitly notes many real teams blend the vocabularies (saying "Clean Architecture" while drawing a hexagon, or vice versa) without any conflict.
Reinforcement: Treat the choice between these vocabularies as a team communication preference, not an architectural decision with real consequences of its own.
4. What is the difference between a "driving" (inbound) Port and a "driven" (outbound) Port?
Show answer
Correct: B
Why B is correct: The distinction is about which side initiates the call — driving Ports are entry points into the core called by outside Adapters; driven Ports are called by the core to reach outside Adapters — but the dependency direction (Adapter depends on Port) stays identical in both cases.
Why A is incorrect: Both kinds of Ports are used in production and in tests — the driving/driven distinction is about call direction, not about test versus production usage.
Why C is incorrect: Nothing about Hexagonal Architecture mandates abstract classes versus interfaces for either kind of Port — both are typically plain interfaces.
Why D is incorrect: The lesson explicitly distinguishes the two by the direction of the call, even though the underlying dependency rule stays the same in both directions.
Reinforcement: "Driving" = called from outside, in. "Driven" = called by the core, out. Both keep the Adapter depending on the Port, never the reverse.
You now recognize the same core discipline through two names — rings and a hexagon — and can translate fluently between them. Next: Domain-Driven Design, the methodology for figuring out what belongs inside that isolated core in the first place.