Hexagonal Architecture: What Is It and How Does It Work? - NDepend Blog (original) (raw)
April 23, 2026 9 minutes read
Hexagonal architecture is a model — or pattern, if you prefer — for designing software applications around a simple idea: put inputs and outputs at the edges of your design, and shield the core from everything else.
When inputs and outputs live at the edges, you can swap their handlers without ever touching the core code. The database becomes pluggable. The UI becomes pluggable. The message bus becomes pluggable. Even the tests become “just another adapter” plugged into the same core.
In this post I’m going to walk through how hexagonal architecture actually works in practice, what the ports and adapters really are, how it compares to clean and onion architecture, where DDD fits, and how it all plays out in C# code. But first, why bother?
One major appeal of hexagonal architecture is that it makes your code easier to test. You can swap in fakes for testing, which makes the tests fast and stable. No spinning up databases. No mocking HTTP servers. No flaky integration runs.
Hexagonal architecture was a deliberate departure from layered architecture. You can use dependency injection and other techniques in layered architecture to enable testing, sure — but there’s a key difference in the hexagonal model: the UI can be swapped out, too. Web, CLI, test harness, scheduled job, message handler: same core, different driver.
And that was the original motivation. There’s a bit of interesting trivia about its origins. The story goes a little like this….
- Hexagonal Architecture? More Like Ports and Adapters!
- Interfaces As Ports
- The Two Sides: Driving and Driven Adapters
- Concretions As Adapters
- It’s About Swappable Components
- The Dependency Rule
- And About the Test Drive
- The UserRepo Revisited
- Structuring a .NET Solution Around the Hexagon
- Hexagonal vs Layered, Onion, and Clean Architecture
- Where Domain-Driven Design Fits In
- Mapping Between the Domain and Adapters
- A Real-World Example: Netflix Studio
- When NOT to Use Hexagonal Architecture
- Common Misconceptions
- FAQ
- What is hexagonal architecture in simple terms?
- What is the difference between hexagonal and clean architecture?
- What are ports and adapters?
- What are primary and secondary adapters?
- Is hexagonal architecture the same as microservices?
- Does hexagonal architecture work without dependency injection?
- How does hexagonal architecture help with testing?
- That’s All There Is to It
Hexagonal Architecture? More Like Ports and Adapters!
Hexagonal architecture was proposed by Alistair Cockburn in 2005. “Hexagonal architecture” was actually the working name for what Alistair eventually called the “ports and adapters pattern.” The renaming never quite took. “Hexagonal architecture” stuck, and that’s the name most developers still use today.
Alistair had his “eureka” moment after reading Object Design: Roles, Responsibilities, and Collaborations by Rebecca Wirfs-Brock and Alan McKean. As he put it, the authors “call [the adapter] stereotype ‘Interfacers,’ but show examples of them using the GoF pattern ‘Adapter.'”
That word — “Interfacers” — is the whole pattern in a nutshell. The hexagonal model is really about what sits at the interface between your code and everything else.
There’s a deeper lineage worth knowing about, too. The idea didn’t appear out of nowhere. Ivar Jacobson’s 1992 book on object-oriented software engineering described the same separation under the name “boundary objects.” Cockburn’s contribution was to name the parts, draw the picture, and explain *why* the symmetry between input and output sides matters.
Picture a hexagon with another, larger hexagon around it. The center hexagon is the core of the application — the business and application logic, the thing your company is actually paying for.
The space between the inner and outer hexagons is the adapter layer. Each side of the hexagon represents a port.
It’s not as if there are six — and only six — ports in the hexagonal architecture. The analogy falls apart there. The sides are simply representations in the diagram. Alistair chose a flat-sided shape instead of a circle to convey something specific: a port is a *named place* where one specific kind of conversation happens, not a vague continuum.
The model is balanced — Alistair has proclaimed his affinity for symmetry — with some external services on the left and others on the right. Hexagonal architecture is fundamentally a model for how to structure the I/O boundary of an application.
- I/O goes on the outside of the model.
- Adapters sit in the gray area.
- The sides of the hexagons are the ports.
- The center is the application and domain.
There are no specific requirements about the core — only that all of the business, application, and domain logic lives there, and that the core has zero knowledge of what’s outside the hexagon.
So what are ports anyway? In C#, they’re interfaces.
Interfaces As Ports
Twenty years after Alistair’s paper, we throw around the word “interface” without remembering how the world looked before we had a clear concept for it.
But what is an interface really?
An interface defines how one module communicates with another. That’s it. No behavior, no state, no implementation choices baked in — just a contract.
For Example
Take two modules: “module A” and “module B.”
When module A needs to send a message to module B, it does so through the interface. The interface is written in the language of module A — meaning the method names, the parameter types, the return types are expressed in terms the core understands, not in terms the database or the HTTP layer would use. (Don’t worry too much about module B yet; that comes later.)
In hexagonal architecture terms, the interface is the port. A port can represent the user, another application, a message bus, a database, a file system, an email service. In hexagonal architecture, a port — much like an interface — is an abstraction.
Ab-what?
Abstraction just means we don’t know how something does what it does. With an abstraction, we only know the high-level details.
For example, the instruction “Tell Johnny to meet me at the bank” is an abstraction. We don’t care how you tell Johnny so long as he gets the message.
If we wanted to be concrete about it, rather than abstract, we’d say “Call Johnny using the following procedure: turn on your phone; tap the phone icon; now, tap the following numbers on the screen: 555-5555; tap send….” I won’t bore you with any more details.
So we have this interface, acting as a port. Our module can use the interface to send messages. Now we need to make that message actually talk to something else. Enter the adapter.
The Two Sides: Driving and Driven Adapters
Before we get to the implementation, there’s a distinction that the original article makes clear but that most introductions skip over. Not all ports are the same.
Hexagonal architecture has two sides, and the terminology you’ll see across the literature is, frankly, all over the place:
- Driving / Primary / Inbound adapters — the ones that call the application. A user clicks a button in a web UI. A test runner invokes a use case. A scheduled job fires. Something outside the hexagon wants the core to do something.
- Driven / Secondary / Outbound adapters — the ones the application calls. The core needs to save a record, send an email, publish an event, fetch a rate from an external API.
The conventional drawing puts driving adapters on the left and driven adapters on the right. The dependency direction is the same on both sides: arrows always point inward, toward the core. The core never imports an adapter; an adapter either implements a port the core defined, or it calls a port the core exposed.
This matters because the two sides need slightly different ports. On the driving side, the core exposes a port (sometimes called a “use case” port or an “application service” interface) that drivers call into. On the driven side, the core defines a port that the outside world implements — the classic repository pattern is a driven port. Get this mental model straight and the rest of the pattern becomes obvious.
Concretions As Adapters
Finally, the adapter is where we want to think about concrete implementation. This is how the message is either handled or passed along. A message to “save the user record” might go to a database, a file, or a network call over HTTP. The adapter might even keep the message in memory. Anything goes so long as the adapter responds according to the port’s contract.
Here’s a concrete example using some C# code. Notice how the UserAdmin only knows about the interface.
| | public class UserAdmin{ private readonly IUserRepo _userRepo; public UserAdmin(IUserRepo userRepo) { _userRepo = userRepo; } UserData _userData; public void Save() { Validate(_userData); _userRepo.Save(_userData); } ...} | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
In this example, the Save method uses the IUserRepo interface.
The UserAdmin class has no idea how the _userRepo does its thing. All our UserAdmin object knows about is the Save method on whatever the _userRepo references via the interface.
Let’s say the _userRepo represents module B in our earlier example. Module A is the UserAdmin class. Module A sends a message to module B via the IUserRepo interface. Here’s where our adapter comes in. The adapter is the implementing class of the IUserRepo. In our running application, this could write to a database, as in the following code:
| | class UserDatabaseRepository : IUserRepo{ public void Save(UserData userData) { using(var db = GetDatabaseConnection()) { db.ExecuteSave(userData); } }} | | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
Or, I could send the record over HTTP, as in this example:
| | class UserHttpRepository : IUserRepo{ public void Save(UserData userData) { using(var http = GetHttpConnection(Connections.UserRepository)) { http.Post(userData); } }} | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
Either way, module A behaves identically. It interacts with the repository without knowing — or caring — what that repository does with the message. This is the power of the adapter.
Our UserDatabaseRepository and UserHttpRepository classes are the adapters. Each adapts the message to the underlying I/O. The adapters aren’t the database and the TCP port themselves; they adapt the message coming through the port to its destination.
But how does this help my code, you’re wondering, and what does it have to do with hexagonal architecture?
It’s About Swappable Components
Module A can use the interface to send a message. It has no way of knowing who or what will actually receive that message. This is the biggest benefit of hexagonal architecture.
Hexagonal architecture is all about swapping components — specifically, external components. In the example above, the module host would inject the IUserRepo into the UserAdmin class. The host could be a web app, a console application, a test framework, or even another app. The point is to make the core independent of its inputs and outputs.
Alistair stressed the importance of decoupling the application from the UI. This is where he really sought to differentiate his approach from layered architecture. The main goal of decoupling through ports and adapters is to be able to test-drive the application using software — a test harness — without the UI ever being involved.
The Dependency Rule
Hexagonal architecture lives or dies by one rule: dependencies point inward. The core never references the adapters. The adapters reference the core. Nothing in the domain ever knows about Entity Framework, ASP.NET, RabbitMQ, AWS SDKs, or whatever else is fashionable this quarter.
If you’re familiar with the SOLID principles, this is the Dependency Inversion Principle applied at an architectural scale. High-level modules (your domain) do not depend on low-level modules (your infrastructure). Both depend on abstractions — and in hexagonal architecture, the abstractions are owned by the domain.
That last detail is the one people miss. When the domain owns the IUserRepo interface, the database project depends on the domain. Not the other way around. The arrow points the “wrong” way compared to how most CRUD apps are wired up. That inversion is the whole point.
A quick sanity check you can run on any codebase: open the domain project, look at its references, and ask whether it depends on anything that has the words “EntityFramework,” “Http,” “Sql,” “AspNetCore,” “Azure,” “Aws,” or any third-party SDK in its name. If yes, the hexagon has a leak.
And About the Test Drive
I’ve already enumerated the advantages of using hexagonal architecture in your design. Now let me clarify explicitly why you should use this pattern.
The bottom line is that you don’t need to rely on external factors to test your application.
Instead, just make the core of the system interact through ports. Your test framework will drive the application through those ports. You could even use files and scripts to drive it instead.
To give you a common scenario, let’s say we’re using a .NET testing framework such as xUnit. The test runner, in this case, is the host.
The following four interactions between the tests and the application will happen via ports:
- Tests send input to the application.
- Test doubles receive output from the application.
- Test doubles return input to the application.
- Tests receive output from the application.
The tests and the test doubles (mocks, fakes, stubs) drive the application through the ports. But what does that look like, you ask?
The UserRepo Revisited
Let’s look at that UserRepo again in light of testing. It’s common to use a mock or fake during unit testing. A FakeUserRepo might look like this:
| 12345678910111213141516171819202122 | using UserDomain.Interfaces;using UserDomain.Data;using System.Collections.Generic;using System.Linq;namespace UserTests{ public class FakeUserRepo: IUserRepo { List<UserData> _users = new List<UserData>(); public void Save(UserData user) { _users.Add(user); } public bool IsSaved(int userId) { return _users.Any(user => user.ID == userId); } }} |
|---|
This is a fake because it stores the data in memory in the List. Notice that it implements IUserRepo. I also added a very rudimentary way to check the List.
Now we need to pass this FakeUserRepo adapter to the UserAdmin in our test code like this:
| | public class UserAdminTests{ [Fact] public void SavesTheUser() { var fakeRepo = new FakeUserRepo(); var sut = new UserAdmin(fakeRepo); var userData = new UserData { ID = 1 }; sut.Save(userData); Assert.True(fakeRepo.IsSaved(1)); }} | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
You can see that this test code is passing the FakeUserRepo into the UserAdmin class using its constructor.
In .NET-land, a web UI would interact with the UserAdmin class through an HTTP endpoint (the port) and ASP.NET Web API (the adapter). Web API routes and adapts the HTTP message to the controller.
Where it leaves you is to write the adapter code into the controller. That’s where you adapt the action and message to the appropriate application class — in this case, UserAdmin.
Structuring a .NET Solution Around the Hexagon
The conceptual diagram is one thing. Translating it into a Visual Studio solution is another. Here’s the layout I see working well in C# projects, and that you’ll find in most reference implementations including Microsoft’s own eShop sample and the Ardalis Clean Architecture template:
- Domain — entities, value objects, domain events, and the port interfaces (IUserRepo, IEmailNotifier, etc.). Zero external dependencies. Just C# and maybe a JSON library if you must.
- Application — use case classes like UserAdmin. References Domain. This is where the orchestration of domain objects happens. Still no Entity Framework, still no ASP.NET.
- Infrastructure (driven adapters) — the EF Core repository, the SMTP email sender, the Azure Service Bus publisher. References Domain so it can implement the ports.
- Web or Api (driving adapter) — the ASP.NET Core project. Controllers or minimal API endpoints translate HTTP into use case calls. References Application.
- Composition Root — usually Program.cs in the Web project, where the DI container wires concrete adapters to the port interfaces. This is the only place where everything meets.
Notice what’s missing: there’s no project that everything references. The domain sits at the bottom of the dependency graph, not the top. If you find yourself adding a “Common” or “Shared” project that the domain depends on, take a hard look at what’s in it. It’s usually a sign of a leak.
If you’d like to verify the structure, NDepend’s dependency graph and matrix make this kind of “show me the arrows” check a one-click exercise — you can see at a glance whether anything in your domain assembly references an infrastructure assembly, which is exactly the violation hexagonal architecture exists to prevent.
Hexagonal vs Layered, Onion, and Clean Architecture
If you’ve read about software architecture for more than a week you’ve already seen onion architecture and clean architecture, and you may be wondering whether hexagonal is just a re-skin. Short answer: they’re cousins, not the same thing, but in practice the differences are smaller than the marketing makes them sound.
Layered (or N-tier) Architecture
The classic three-tier layout: Presentation on top, Business in the middle, Data at the bottom, and each layer references the one below. The problem is that the Business layer ends up depending on the Data layer, which means the database leaks into the heart of the application. Change your database, and the business logic feels it.
Hexagonal architecture inverts that bottom dependency. The data layer implements an interface owned by the business layer. The arrow flips, and the leak is sealed.
Onion Architecture
Proposed by Jeffrey Palermo in 2008, onion architecture is hexagonal architecture redrawn with more rings. Innermost is the domain model, then domain services, then application services, then infrastructure. The dependency rule is the same: outer rings depend on inner rings, never the reverse.
If you put hexagonal and onion diagrams side by side, the family resemblance is obvious. Onion just has more concentric layers; hexagonal collapses the inside into a single “core.”
Clean Architecture
Robert C. Martin’s “Clean Architecture,” published as a book in 2017, generalizes both hexagonal and onion. Same dependency rule. Same emphasis on use cases as a first-class concept. The familiar diagram with the four rings — Entities, Use Cases, Interface Adapters, Frameworks & Drivers — is essentially onion architecture with explicit terminology for the layers.
If you’ve adopted “Clean Architecture” templates in .NET, you’ve adopted a flavor of hexagonal architecture. The names are different. The dependency rule is the same.
The practical takeaway: don’t get hung up on which name to use. Pick one, be consistent within your team, and remember that all three patterns are solving the same problem — how to keep the domain pristine while letting the I/O periphery change freely.
Where Domain-Driven Design Fits In
Hexagonal architecture and Domain-Driven Design are frequently discussed together for good reason. DDD gives you the vocabulary for what goes inside the hexagon; hexagonal architecture gives DDD a place to live.
In a typical DDD-flavored hexagonal app:
- Entities, value objects, and aggregates sit in the domain core. They contain business rules and invariants. They have no dependencies on anything outside the core.
- Domain services handle logic that doesn’t naturally belong to a single entity.
- Application services (or use cases, or interactors — pick your term) orchestrate the domain objects to fulfill a single use case. UserAdmin.Save is an application service.
- Repositories are driven ports. They live as interfaces in the domain, with concrete implementations as adapters in infrastructure.
- Bounded contexts map naturally to entire hexagons. A large system is a collection of hexagons that talk to each other, often through messaging adapters.
You don’t need DDD to do hexagonal architecture, and you don’t need hexagonal architecture to do DDD. But the combination is so natural that most production examples you’ll read about online use both.
Mapping Between the Domain and Adapters
One topic that gets glossed over in the introductory articles: how the domain talks to adapters when the data shapes don’t match.
Take a User entity in your domain. It has invariants, behavior, maybe a few private fields and a constructor that won’t let you build an invalid one. Now ask: do you really want EF Core hydrating that thing directly from a database row? Probably not — EF will fight you on the private setters, the value objects, and the lack of a parameterless constructor.
There are roughly four ways to handle this, in increasing order of decoupling:
- Use the domain object as the persistence model. Easiest, fastest to ship, and the choice most teams make. The cost is that ORM concerns subtly bleed into the domain (lazy loading, navigation properties, attribute soup).
- Two-way mapping in the repository. The repository converts between a persistence-friendly POCO and the rich domain object. The domain stays clean; the cost is the mapping code.
- One model per use case. Each use case has its own data shapes for input and output, and the repository or query handler returns exactly what that use case needs. Common in CQRS-flavored designs.
- Domain events all the way down. The domain emits events, the infrastructure reacts. Strongest decoupling, highest complexity.
There’s no correct answer. Smaller systems are fine with option 1. Once the domain gets complicated enough that the ORM is fighting you, option 2 starts paying for itself. By the time you’re at option 4, you probably also have a dedicated team paying attention to architecture full-time.
A Real-World Example: Netflix Studio
The classic story for hexagonal architecture in the wild comes from Netflix’s engineering blog. The Netflix Studio team had a service that started life with a JSON-over-HTTP API. Business needs changed, and the team needed to expose the same functionality over GraphQL.
Because the service was built hexagonally, the entire core remained untouched. The team added a new driving adapter for GraphQL, wired it to the same application services, and shipped. The blog post claims the swap took about two hours. The interesting detail isn’t the two hours — it’s that the test suite, which was already driving the core through ports, gave the team the confidence to make the change without a multi-week regression cycle.
That’s the payoff hexagonal architecture is selling. Not “elegance.” Not “decoupling for its own sake.” The ability to change a delivery mechanism on a Tuesday and not blow up on Wednesday.
When NOT to Use Hexagonal Architecture
Hexagonal architecture is not free. The indirection costs. There are more interfaces, more projects, more files, more places where you have to ask yourself “where does this go?” Junior developers joining the team will look at the dependency graph and need a guided tour.
I would not reach for hexagonal architecture on:
- CRUD apps with no business logic. If your application is a thin layer between an HTTP request and a database write, you’re adding ceremony to no real benefit. A skinny controller calling EF Core directly is fine.
- Throwaway scripts and one-off tools. Same reason. The pattern earns its keep on long-lived systems with non-trivial business logic.
- Code where the domain genuinely is the framework. If you’re writing an ASP.NET middleware library, ASP.NET is your domain. Don’t hide it behind ports.
On the other hand, if you’re building a system with real business rules, that will live for years, that needs a strong test suite, and where the I/O surface (databases, APIs, messaging) is genuinely likely to change — the cost of the pattern is repaid quickly.
Common Misconceptions
A few things I see beginners get wrong:
- “The hexagon has six sides because there are six kinds of ports.” No. Six is just an arbitrary number. There can be one port or twenty.
- “Hexagonal architecture means every class has an interface.” No. The interfaces belong at the hexagon boundary, not on every domain class. Adding an interface to every class is a separate (bad) habit sometimes called “header interfaces.”
- “The repository pattern is hexagonal architecture.” Repositories are one example of a driven port. Hexagonal architecture is the broader idea of putting all external dependencies behind ports.
- “Adapters belong to the core.” They don’t. Adapters live outside the hexagon. The core only knows about the ports.
FAQ
What is hexagonal architecture in simple terms?
Hexagonal architecture is a way of structuring an application so that the business logic at the center has no idea what kind of UI, database, or external service is connected to it. Everything outside talks to the core through interfaces called ports, and the actual technology (SQL, HTTP, the file system) lives in adapters that plug into those ports. Swap an adapter and the core doesn’t notice.
What is the difference between hexagonal and clean architecture?
Clean architecture, onion architecture, and hexagonal architecture are all variations on the same theme: keep the domain at the center, point all dependencies inward, and put I/O at the edges. Clean architecture (Robert C. Martin) and onion architecture (Jeffrey Palermo) draw more rings and name more layers. Hexagonal architecture (Alistair Cockburn) emphasizes the symmetry between driving and driven sides. In day-to-day code, the resulting projects look very similar.
What are ports and adapters?
A port is an interface owned by the core that defines a conversation — “save a user,” “send a notification,” “fetch the current exchange rate.” An adapter is a concrete implementation of that conversation against a specific technology: a SQL Server repository, an SMTP email sender, an HTTP client to a remote API. The core only ever sees ports; the host wires real adapters to the ports at startup.
What are primary and secondary adapters?
Primary adapters (also called driving or inbound) call into the core. Examples: an ASP.NET controller, a console command handler, an xUnit test, a scheduled job. Secondary adapters (also called driven or outbound) are called by the core. Examples: a database repository, a message bus publisher, an outbound HTTP client.
Is hexagonal architecture the same as microservices?
No. Hexagonal architecture is a way of structuring code inside a single deployable unit. Microservices is a way of splitting a system across deployable units. They compose well — a common pattern is to give each microservice an internal hexagonal structure — but neither requires the other.
Does hexagonal architecture work without dependency injection?
You can implement it without a DI container by manually wiring adapters in your composition root. But constructor injection is the natural mechanism for handing adapters to the core, so most production .NET implementations rely on the built-in Microsoft.Extensions.DependencyInjection container or an equivalent.
How does hexagonal architecture help with testing?
Because the core only depends on port interfaces, you can substitute in-memory or fake implementations during tests. There’s no database to spin up, no HTTP server to mock, no Kafka cluster to seed. Tests run in milliseconds, which means you write more of them, which means you catch more bugs. That’s the original motivation Alistair Cockburn cited when he created the pattern.
That’s All There Is to It
Although hexagonal architecture sounds like a vague, mystical concept from the ages, it’s actually widespread in modern software development. The main theory behind it is decoupling the application logic from the inputs and outputs. The supporting machinery — the ports, the adapters, the dependency rule — exists to make that decoupling enforceable rather than aspirational.
The goal is to make the application easier to test, easier to change, and easier to reason about three years from now when the original team has moved on. Alistair Cockburn changed the terminology from “hexagonal architecture” to “ports and adapters,” but thankfully “hexagonal architecture” sort of stuck. It just sounds a heck of a lot cooler.
This article is brought to you by the team behind NDepend — a proven .NET static analysis tool for improving code maintainability, security, and overall quality. Whether you’re modernizing a legacy .NET application or starting fresh in C#, get started with your free full-featured trial today!