Hexagonal Architecture Abuse: When Organization Becomes Obfuscation

Hexagonal architecture (also known as Ports and Adapters) is a powerful way to design software that is independent of frameworks, databases, or UI layers. Its main goal is to isolate the core business logic from the outside world by enforcing clear boundaries. However, in many projects, this pattern is over-engineered to the point where the supposed simplicity turns into a maze of indirection, deeply nested folders, and needless abstractions.

The Problem: Overstructuring a Simple Concept

Many developers (including me) start with the intent of using hexagonal architecture but quickly spiral into the following:

/invoice
  /application
    /services
    /mappers
  /domain
    /entities
    /events
    /errors
    /services
  /infrastructure
    /adapters
    /persistence
    /external

At first glance, this looks “well-organized,” but in practice it causes:

Context Switching Overload

Navigating from a service to its related domain entity or mapper means jumping across multiple directories. The physical separation slows you down and fragments understanding.

Boilerplate Inflation

Every small concept demands its own file and folder. A single feature ends up spread across dozens of microfiles. This is organization for the sake of organization, not efficiency.

Loss of Clarity

The deeper and more granular the folder structure, the less obvious the intent becomes. You know where things are, but not why they exist.

It Hides the Business Purpose

This might be the most important flaw: the structure doesn’t tell you what the module is actually doing. You open the domain, application, and infrastructure folders — and you’re still unsure. The only clue you have is the module name which most of the time does not reflect the actual module behaviour.

When opening a module directory, you should immediately see:

  • What is this module responsible for?
  • What are the key business concepts?
  • Who is supposed to use its functionality, and how?

Deeply abstracted structure pushes this knowledge out of reach.

Keep the Domain Front and Center

Your domain is the heart of the application. It shouldn’t be buried under layers or directories like domain/entities, domain/services, and domain/errors.

A more approachable structure is:

/invoicing
  customer.ts              // recipient of the invoice
  invoice.ts               // entity representing the invoice
  issuer.ts                // entity issuing the invoice
  invoicing.facade.ts      // Public API of this module
  tax-policy.ts            // Tax calculation policy
  due-date-calculator.ts   // Domain service
  infrastructure/                // Less important things
    invoicing.module.ts          // NestJS module wiring
    knex-invoice.repository.ts   // Knex adapter
    knex-issuer.repository.ts    // Knex adapter

No unnecessary directories. No needless ceremony. This just tells the whole story.

This aligns with hexagonal principles:

  • Business logic is in dedicated concepts
  • External interaction is through knex-invoice.repository.ts
  • Communication with the outside world is handled by infrastructure components like controllers and NestJS modules.
  • The facade orchestrates domain logic by coordinating aggregates, policies, and business rules..
  • Infrastructure components call into the application layer via the module’s facade..

You may be wondering where all the supporting pieces went — the errors, events, identity objects, and so on. Here’s what a fully expressed Invoice domain concept might look like, with all related elements defined in a single file:

// invoicing/invoice.ts

// Value object for entity (aggregate) identity
class InvoiceId extends Uuid<'Invoice'> {}

// Entity domain events
type InvoiceEvents =
  | Event<'InvoiceIssued', { issuedAt: string }>
  | Event<'InvoicePaid', { paidAt: string }>
  | Event<'InvoiceOverdue', { ... }>

// Business rule
class InvoiceDateCannotBeEarlierThanBillingPeriodStart extends BusinessRuleViolation {}

// Entity (aggregate) itself
class Invoice {
  readonly id: InvoiceId;
  // ...
}

interface InvoiceRepository {
  load(invoiceId: InvoiceId): Promise<Invoice>
  save(invoice: Invoice): Promise<void>
}

At first glance, grouping all these definitions together might seem unconventional — even “messy.” But in fact, this structure reflects a key modeling principle: each of these types participates in defining the behavior and boundaries of the Invoice aggregate. They are part of a single cohesive concept.

Splitting them across multiple folders like domain/errors/, domain/events/, or domain/repositories/ introduces artificial distance between parts that are semantically close. Instead of improving clarity, it scatters the model and makes it harder to see the whole picture. By keeping them together, we reinforce the idea that the domain is organized by business meaning, not technical category.

The Facade as the Application Layer

Instead of creating an entire application/ folder with subfolders for use cases, services, DTOs, and so on – use a facade class as the boundary of your application layer. This facade can orchestrate domain logic and act as a public API for your module. One class, one file, clear purpose.

The Infrastructure Should Wrap Itself

Framework modules (e.g., NestJS modules), external adapters, and database persistence logic belong in the infrastructure. You can group them together – but don’t let them spill into your core. They’re replaceable, and your domain logic should never depend on them.

This keeps the framework where it belongs – on the edge.

Guiding Principles

  • Flat is better than nested: Unless nesting adds real clarity, avoid it.
  • Structure follows concept, not convention: Let each domain concept be its own file or directory.
  • Only abstract when needed: Don’t introduce DTOs, mappers, or layers “just in case.” Add them when there’s actual complexity to manage – not before.
  • Architecture should serve clarity: Hexagonal architecture is a tool, not a goal. If your structure doesn’t help you understand what the module does, it’s working against you.

Conclusion: Return to the Spirit of Hexagonal Architecture

The goal of hexagonal architecture is to reduce coupling and increase testability, not to impose a rigid directory structure. Simplicity is not the enemy of robustness – and overcomplicating your structure defeats the very purpose of this architectural pattern.

Stick to what matters: your domain and its behavior. Build from there.

Solutions architect, Node.JS developer, Knowledge Analyst

https://www.linkedin.com/in/bartosz-pasinski/

Leave a Reply

Your email address will not be published. Required fields are marked *