← Open in the full interactive course (progress tracking, search & more)

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

Adapter

Port: IOrderRepository
Port: IPaymentGateway
Port: IPlaceOrder (inbound)
Application CoreDomain + use-cases — no framework references
Adapter: EfOrderRepository
Adapter: StripeGatewayAdapter
Adapter: OrdersApiController
Driving vs. driven Ports — the direction of the conversation

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?

PROBLEM → NEED → SOLUTION
PROBLEM
NEED
SOLUTION

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 ArchitectureSame underlying idea
Domain / Application (inner rings)The hexagon's coreBusiness logic, isolated from technology
An interface the Domain/Application layer defines (e.g. IProductRepository)A PortAn abstraction the core owns, shaped around what the core needs
An Infrastructure implementation (e.g. EfProductRepository)An AdapterA technology-specific implementation, plugged in from outside
The Dependency Rule — arrows point inwardAdapters depend on Ports, never the reverseThe core never knows a specific technology exists
Concentric circlesA hexagon with socketsTwo 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

FROM ONE CLASS'S ADAPTER TO A WHOLE APPLICATION'S BOUNDARY
1. THE CORE DEFINES A PORT — SHAPED AROUND WHAT IT ACTUALLY NEEDS
2. AN ADAPTER IMPLEMENTS THE PORT, TRANSLATING TO A SPECIFIC TECHNOLOGY
3. A SECOND ADAPTER CAN IMPLEMENT THE SAME PORT, INTERCHANGEABLY
4. THE COMPOSITION ROOT WIRES THE CHOSEN ADAPTER TO THE 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

The wall doesn't care what's plugged into it

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

BOTH SIDES OF THE HEXAGON, WORKED THROUGH
1. A DRIVING (INBOUND) PORT — SOMETHING OUTSIDE CALLS INTO THE CORE
2. A DRIVEN (OUTBOUND) PORT — THE CORE CALLS OUT TO SOMETHING OUTSIDE
3. IN BOTH DIRECTIONS, THE RULE IS IDENTICAL

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?

SituationLeans toward
A team already fluent in Clean Architecture's ring vocabularyKeep 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 eitherSame 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 exclusiveReconsider the framing — they're the same underlying goal, described two well-known ways
Rule of thumb: the discipline — core logic depends only on abstractions it owns, technology plugs in from outside, dependencies never point back toward the core — is what actually matters. Whether you draw circles or a hexagon while explaining it to your team is a communication choice, not a different architecture.

Mental Model

Port = an interface the application core owns, shaped around what the core needs.
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


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.


dotnetmadeeasy.com — Learn C# and .NET, the right way.