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.
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.
EfOrderRepository, StripePaymentAdapter, OrdersApiControllerA 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.
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.
// ═══ 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.
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.
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.
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 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.
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.
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.
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.
| 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 |
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"?
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?
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?
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?
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.
dotnetmadeeasy.com — Learn C# and .NET, the right way.