All posts

Hexagonal Architecture vs. Everything Else: A Decision Guide, Not a Religion

KynodexKynodex
15 min read
Hexagonal Architecture vs. Everything Else: A Decision Guide, Not a Religion

Picture a simple e-commerce API. Controllers call services, services call repositories. It works cleanly for months. Then the business asks for a second payment provider, a new notification channel, and a swap of the primary database vendor — all in the same quarter.

Suddenly the "simple" codebase is a tangle where touching one file means chasing three others just to keep things consistent. This is the exact moment architectural patterns stop being an academic debate and start being the difference between a two-day change and a two-week one.


Introduction

Hexagonal architecture gets discussed online as if it's competing in a tournament against Clean Architecture, Onion Architecture, and traditional layered design — with each camp arguing its pattern is objectively correct. That framing misses the point. These aren't rival ideologies. They're tools calibrated for different problems, and the right choice depends entirely on what your application actually needs to withstand: complexity in the domain logic, frequent swapping of external systems, or just fast, predictable delivery on a CRUD app that doesn't need architectural ceremony at all.

This post breaks down what hexagonal architecture actually is, how it differs structurally from the alternatives most often confused with it, and a practical framework for choosing — because picking the wrong one in either direction has a real cost: either unnecessary complexity slowing a simple team down, or a fragile, tightly-coupled system that can't survive the kind of change that arrives in every product's second year.


What Hexagonal Architecture Actually Is

Hexagonal architecture — also called Ports and Adapters — was coined by Alistair Cockburn. Its central premise is straightforward: keep your application's core business logic completely isolated from the technical details of how it talks to the outside world — databases, web frameworks, message queues, third-party APIs, user interfaces.

The mechanism that makes this work is ports and adapters. A port is an interface defined by your business logic, stating what it needs — "give me a way to save an order" — without specifying how that need gets fulfilled. An adapter is the concrete implementation that satisfies a port for a specific technology — a PostgreSQL adapter, a REST adapter, an in-memory adapter used purely for tests. The business logic never knows or cares which adapter is plugged in on the other side of the port.

The practical payoff: swap PostgreSQL for MongoDB, or REST for gRPC, and in a properly built hexagonal system, the core domain logic doesn't change at all — only the adapter does. Your business rules stay entirely untouched by infrastructure decisions, and infrastructure decisions can be deferred or reversed without a rewrite of what actually matters to the product.

What this looks like in code

// The PORT — defined by the business logic, inside the domain.
// It states a need. It knows nothing about SQL, HTTP, or Mongo.
interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(orderId: string): Promise<Order | null>;
}

// The DOMAIN LOGIC — depends only on the port (an interface),
// never on any concrete database or framework.
class PlaceOrderUseCase {
  constructor(private readonly orders: OrderRepository) {}

  async execute(order: Order): Promise<void> {
    order.validate();          // pure business rule, no I/O
    await this.orders.save(order);
  }
}

// ADAPTER #1 — fulfills the port using PostgreSQL
class PostgresOrderRepository implements OrderRepository {
  async save(order: Order) {
    await db.query('INSERT INTO orders ...', [order.id, order.total]);
  }
  async findById(orderId: string) {
    const row = await db.query('SELECT * FROM orders WHERE id = $1', [orderId]);
    return row ? Order.fromRow(row) : null;
  }
}

// ADAPTER #2 — fulfills the SAME port for tests, no database at all
class InMemoryOrderRepository implements OrderRepository {
  private store = new Map<string, Order>();
  async save(order: Order) { this.store.set(order.id, order); }
  async findById(orderId: string) { return this.store.get(orderId) ?? null; }
}

PlaceOrderUseCase never imports pg, never sees a SQL string, and doesn't change at all whether it's running against Postgres in production or an in-memory map in a unit test. That's the entire mechanism — and it's also exactly why hexagonal tests can run in milliseconds without a real database spun up.

Compare that to the same feature in a typical layered structure:

// Layered — the service talks directly to a concrete database client.
// Fast to write. But swapping databases or unit-testing without a
// real DB connection means touching this class directly.
class OrderService {
  async placeOrder(order: Order) {
    order.validate();
    await db.query('INSERT INTO orders ...', [order.id, order.total]); // <- coupled
  }
}

Nothing is "wrong" with the second version — for a small app, it's genuinely less code and less indirection. The difference only matters once you actually need to swap db for something else, or run this logic in a test without a live database connection.


Where It Diverges From the Alternatives

The confusion in most online debates comes from the fact that Hexagonal, Clean, and Onion architecture share the same foundational idea — dependencies point inward, toward the business logic, never outward toward infrastructure. Where they genuinely differ is in how prescriptive they are about what happens inside that boundary.

Layered (N-Tier) Architecture — the default, not a deliberate choice

Traditional layered architecture — controller calls service, service calls repository — is often what a codebase becomes by accident, simply because it's the path of least resistance and the structure most frameworks nudge you toward without any conscious architectural decision being made.

The structural weakness: dependencies in a layered system typically flow downward through concrete implementations, not through abstractions defined by the business layer. That means the business logic layer usually has direct knowledge of the database layer beneath it — and any framework or infrastructure change tends to ripple upward through multiple layers, because nothing enforces a hard boundary preventing it. In practice, on real teams, this shows up as a controller reaching straight into a data-mapping class it was never supposed to touch — a boundary violation that code review often misses simply because the framework itself doesn't stop it from compiling.

The tradeoff isn't purely negative. For a straightforward CRUD application without deep business complexity, layered architecture's simplicity, framework alignment, and the sheer familiarity most engineers already have with it are genuine, durable advantages — not something to apologize for.

Clean Architecture — hexagonal's more opinionated cousin

Clean Architecture, associated with Robert C. Martin, shares hexagonal's inward-dependency principle but adds explicit, named concentric layers — typically Entities at the center, then Use Cases, then Interface Adapters, then Frameworks and Drivers at the outer edge. Where Ports and Adapters is largely silent on how you organize the interior of your business logic, Clean Architecture is deliberately more prescriptive about it, giving teams a more defined internal structure to follow — at the cost of more ceremony and more files for a team that has to actually maintain that additional structure.

Onion Architecture — domain-first, with the domain model doing the heavy lifting

Onion architecture places the domain model even more explicitly at the absolute center, with domain services wrapped immediately around it, and application services and infrastructure layered outward from there. It's the natural fit when the hardest part of your problem is genuinely modeling a rich, complex domain — not swapping infrastructure — because its layering is organized specifically around protecting that domain model's integrity as the system grows.

The honest summary of the differences

Layered architecture is the simplest starting point and shouldn't be treated as inherently wrong — it's wrong only when applied to a problem it doesn't fit. Clean Architecture puts a name and a boundary around every layer of business rule complexity. Onion architecture exists specifically to protect a rich domain model as the center of gravity. Hexagonal architecture optimizes specifically for making every external dependency swappable, with fewer opinions about what happens inside the hexagon itself.


What Hexagonal Architecture Actually Costs You

This is the part evangelistic architecture content tends to skip, and it's the part that determines whether adopting the pattern helps or hurts a given team.

More files, more indirection, for a problem you might not have yet. Every port needs an interface, every adapter needs an implementation, and a request that would be three lines in a layered controller might route through several files in a strict hexagonal setup. For a genuinely simple CRUD service, this is real overhead with no corresponding payoff — nothing is being made more swappable, because nothing was ever going to be swapped.

The boundary still depends on team discipline, not just the pattern. A named architecture doesn't automatically enforce itself. Nothing stops a developer from reaching straight past a port and calling a database client directly from inside the domain layer — the pattern makes that mistake more visible in code review, but it doesn't make it impossible without linting rules, dependency-direction checks, or module boundaries actively enforced in CI.

It solves a problem you need to actually have. If your application only ever talks to one database and one API for its entire lifetime, the "swap infrastructure without touching business logic" benefit of hexagonal architecture never gets cashed in — you paid the structural cost and never collected the payoff.


Benefits and Drawbacks, Pattern by Pattern

Layered (N-Tier) Architecture

Benefits:

  • Fastest to start — every mainstream framework nudges you toward this shape by default

  • Lowest onboarding cost — nearly every engineer already recognizes controller → service → repository

  • Fewer files and less indirection for small-to-medium codebases

  • Well-suited to CRUD-heavy applications where business logic is thin

Drawbacks:

  • Business logic and infrastructure code are easy to tangle together, since nothing structurally prevents it

  • Unit testing business rules in isolation often requires mocking a database layer, rather than substituting a clean interface

  • Swapping a database, queue, or framework tends to ripple through multiple layers

  • Boundaries are enforced by convention and code review, not by the compiler or the architecture itself

Hexagonal Architecture (Ports and Adapters)

Benefits:

  • Business logic is fully testable without a real database, queue, or external API — swap in an in-memory adapter for tests

  • Infrastructure is genuinely swappable — new database, new messaging system, new delivery mechanism — without touching domain code

  • Forces an explicit, compiler-checked boundary between "what the business needs" and "how it's technically fulfilled"

  • Scales well when a system needs to support multiple delivery mechanisms at once — REST, gRPC, a CLI, a scheduled job — against the same core logic

Drawbacks:

  • More files, more interfaces, more indirection for a benefit you may never actually use

  • The pattern doesn't prescribe structure inside the domain layer itself — teams still need to agree on internal conventions

  • Real payoff only appears once infrastructure actually changes; until then, the extra structure is a standing cost with no return

  • Junior engineers unfamiliar with the pattern often need real onboarding time before contributing confidently

Clean Architecture

Benefits:

  • Explicit, named layers give large teams a shared, well-documented structure to align around

  • Strong separation between entities, use cases, and delivery mechanisms — very clear ownership boundaries

  • Widely documented, with an established body of reference material and case studies to draw from

Drawbacks:

  • The most ceremony-heavy of the group — more layers, more files, more boilerplate per feature

  • Can feel like over-engineering on small teams or early-stage products where requirements are still shifting fast

  • The strict layering can slow down straightforward changes that don't need that much structure

Onion Architecture

Benefits:

  • Domain model sits at the true center, protected from every other concern by design

  • Excellent fit when modeling complex business rules and relationships is the actual hard problem

  • Naturally discourages anemic domain models, since domain logic has nowhere else to live but the center

Drawbacks:

  • Less well-known than Clean or Hexagonal, meaning smaller hiring pool of engineers already familiar with it

  • Benefits are concentrated specifically around domain complexity — offers little extra value if your domain logic is genuinely simple

  • Like the others, still depends on team discipline to keep the domain layer free of infrastructure leakage


Rather than treating this as a single either/or choice, walk through it as a short sequence of questions about your actual system:

Is this a genuinely simple application with minimal business logic? If yes — stop here. Use layered architecture. Don't add structural ceremony a simple CRUD app will never need.

Do you have real, non-trivial business rules that need to be independently testable, separate from any framework or database? If yes, continue. If no, layered architecture remains the right call even for a moderately-sized app.

Will you need to swap vendors, integrations, or infrastructure components more than once over this system's lifetime — a different payment processor, a different message queue, a different database engine? If this is a genuine expectation, not a hypothetical, hexagonal architecture is built specifically for this. The ports-and-adapters boundary is the direct answer to "we don't yet know what we'll be integrating with in two years."

Is the domain model itself the hardest, highest-value part of the problem — complex business rules, rich entity relationships, logic that needs to evolve independently of any particular delivery mechanism? Onion architecture's domain-first structure is purpose-built for this shape of problem.

Do you need a more prescriptive, well-documented structure for a large team, where consistent internal organization across many contributors matters more than minimizing ceremony? Clean Architecture's named layers give a bigger team a shared, well-documented map to work from.


What This Looks Like in a Real Codebase

A pattern that shows up repeatedly in mature production systems isn't "pick one architecture and apply it uniformly everywhere" — it's applying structural rigor where the system's complexity actually justifies it, and staying simple everywhere else.

Hexagonal boundaries around the core domain and its genuinely volatile integrations — the specific parts of the system where infrastructure really is likely to change, such as payment processing, third-party notification providers, or a data store the team is deliberately keeping swappable during an active migration.

Straightforward layered structure for the surrounding CRUD-heavy administrative and internal-tooling surface — user management screens, internal dashboards, simple reporting endpoints — where the architectural payoff of strict boundaries would never actually get collected.

This mirrors the same principle that shows up in backend language selection or AI model routing: match the structural investment to where the system's actual complexity and actual rate of change live, instead of applying one pattern uniformly across a system that doesn't have uniform needs.


Key Takeaways

  • Hexagonal, Clean, and Onion architecture all share the same core principle — dependencies point inward toward business logic, never outward toward infrastructure — and differ mainly in how prescriptive they are about organizing what's inside that boundary.

  • Hexagonal architecture's specific strength is making external dependencies fully swappable through explicit ports (interfaces the business logic defines) and adapters (concrete implementations plugged in from the outside).

  • Layered architecture isn't wrong by default — it's wrong for the wrong problem. For simple CRUD applications without deep business complexity, its simplicity and framework alignment are genuine advantages worth keeping.

  • The architectural boundary in any of these patterns still depends on team discipline and enforcement, not just the pattern's existence — nothing about naming a layer "domain" stops a developer from reaching straight through it without linting or CI checks actively enforcing the boundary.

  • The real decision hinges on one question: how often will your infrastructure genuinely change? If the honest answer is "rarely, if ever," hexagonal architecture's core benefit never gets collected, and its added indirection is pure cost.

  • Mature systems increasingly apply structural rigor selectively — hexagonal boundaries around genuinely volatile integrations and complex domain logic, simpler layered structure everywhere else — rather than committing to one architecture uniformly across a codebase with non-uniform needs.


Conclusion

The choice between hexagonal architecture and its alternatives was never really a contest to be won by one "correct" pattern. It's a question of matching structural investment to where your system's actual complexity and actual rate of change genuinely live. A team that reaches for hexagonal architecture on a simple internal tool pays real complexity tax for a swappability benefit it will likely never use. A team that stays in a plain layered structure while juggling three payment providers and a database migration is quietly building the exact fragility that architecture patterns exist to prevent.

The engineers who get the most value from this decision aren't the ones who picked a side in the online debate. They're the ones who looked honestly at their own system's actual volatility — what's likely to change, what's genuinely complex, and what's just standard CRUD — and applied the right amount of structure to each part, rather than a uniform answer to a non-uniform problem.


References


At Kynodex, we architect production systems where structural decisions match actual system complexity — not a template applied uniformly regardless of fit. If your team is scoping a new service's architecture or untangling a codebase that's outgrown its original structure, talk to us.

Powered by Synscribe

Comments

No comments yet. Be the first to start the conversation.

Ready to build?

Turn your AI vision into a production system

We build the AI infrastructure that powers your next stage of growth.

Book a Strategy Call