Ports & Adapters: The Hexagonal Architecture Pattern in Node.js
Hexagonal Architecture (Ports & Adapters) makes a Node.js application completely agnostic of its delivery mechanism and persistence technology. By defining Ports as interfaces in the domain and implementing Adapters in infrastructure, the same billing engine can be driven by an HTTP controller, a Kafka consumer, or a CLI script without changing a single domain class.
Backend Clean Architecture & Domain-Driven Design
Ports & Adapters: The Hexagonal Architecture Pattern in Node.js
Domain objects are autonomous state machines with enforced invariants — they are not passive bags of data passed between controller functions. By Part 6, we have a billing engine where the domain model is genuinely isolated from infrastructure — all communication flows through IOrderRepository, IPaymentProcessor, and IEventBus. What we have not yet named, precisely, is the architectural pattern that makes this possible. That pattern is called Ports and Adapters — or Hexagonal Architecture — and naming it precisely gives us a vocabulary that makes the design decisions in Parts 2–6 reproducible, teachable, and verifiable.
This article makes the pattern concrete: defines Ports and Adapters precisely, shows how three different delivery mechanisms (HTTP, Kafka, CLI) drive the same application core without modification, demonstrates swapping infrastructure implementations (Stripe → PayPal, Prisma → TypeORM), and shows how to verify the hexagonal constraint at the TypeScript level.
Prerequisites: Parts 2–6 — this article names and systematizes the structure already built. No new code is introduced from scratch; every example extends existing classes from previous parts.
1. What Hexagonal Architecture Actually Is
Alistair Cockburn coined the term in 2005 with one motivating insight: applications should work equally well when driven by automated test suites, human UIs, batch programs, or message queues — without requiring different application code for each.
The metaphor is a hexagon (the shape is arbitrary — what matters is that it implies no "top" or "bottom"):
- Inside the hexagon: the application core — Domain layer + Application layer. It contains all business logic. It knows nothing about HTTP, Kafka, Prisma, or any delivery mechanism.
- Outside the hexagon: driving adapters (who trigger the application) and driven adapters (who the application triggers).
- The hexagon's boundary: ports — interfaces defined inside the hexagon that describe what the outside world must provide.
The hexagonal shape has no top or bottom because HTTP is not "above" the application, and Prisma is not "below" it. They are both outside — symmetrically. This reframing eliminates the conceptual bias that makes engineers think infrastructure is somehow foundational to business logic.
1.1 The Two Sides: Driving and Driven
| Side | Who Initiates | Examples | Called |
|---|---|---|---|
| Driving (Left) | External actor triggers the application | HTTP controller, Kafka consumer, CLI command | Driving Adapters |
| Driven (Right) | Application triggers the external system | Database, payment gateway, email service, event broker | Driven Adapters |
Both sides communicate with the application core exclusively through Ports — interfaces.
2. Ports: Interfaces at the Hexagon Boundary
2.1 Driving Ports (Input Ports)
A Driving Port is the interface that the application core exposes to the driving side. In our architecture, Use Cases are the driving ports:
Alternatively — and more commonly — the Use Case class itself is the Port. Driving adapters depend directly on the Use Case class. Either approach is valid; what matters is that the adapter imports the Use Case, not the other way around.
2.2 Driven Ports (Output Ports)
Driven Ports are defined in the Domain or Application layer. They describe what external systems the application needs, using domain vocabulary only:
The word "Prisma" does not appear. The word "Stripe" does not appear. The word "SendGrid" does not appear. These are pure domain-language interfaces — they describe capabilities the application needs, not implementations it depends on.
3. Adapters: Connecting the Outside World to the Ports
3.1 Driven Adapters (Right Side)
Driven Adapters implement Driven Ports. They live in src/infrastructure/ and import both the Domain interfaces they implement AND the external library they adapt:
Swapping from Stripe to PayPal in production requires changing one line in the DI container: bind(IPaymentProcessor).to(PayPalPaymentProcessor) instead of StripePaymentProcessor. The application core — PlaceOrderUseCase, Order, Money — changes nothing.
3.2 Driving Adapters (Left Side)
Driving Adapters receive external input and translate it into a Command DTO that the Use Case understands:
HTTP Adapter:
Kafka Adapter:
CLI Adapter:
All three adapters call the same PlaceOrderUseCase.execute() with a PlaceOrderCommand. The use case implementation is identical in all three contexts. If the business adds a GraphQL API or a gRPC endpoint, it is a new adapter — not a new use case.
4. The Symmetric Architecture
The full hexagonal structure for the billing engine:
The hexagon has no privileged entry point. HTTP is not more important than Kafka. PostgreSQL is not more fundamental than in-memory storage. They are all outside the boundary, all connected through ports.
5. Verifying the Hexagonal Constraint
The hexagonal constraint — that the application core imports nothing from adapters — can be verified mechanically:
5.1 TypeScript Project References (Already Set Up in Part 2)
The src/domain/tsconfig.json has no references → the Domain layer cannot import from anywhere. The src/application/tsconfig.json references only src/domain → the Application layer cannot import from Infrastructure or Interface.
Any violation produces a compiler error before a single test runs.
5.2 Dependency Audit with ts-prune or eslint-plugin-import
These four shell commands are an effective CI gate. Add them to .github/workflows/lint.yml to catch boundary violations on every pull request.
5.3 The Dependency Direction Test
For any file F in src/domain/ or src/application/:
- Open
F - List every import statement
- Verify every import points to
src/domain/orsrc/application/(inner circles only) - Any import pointing to
src/infrastructure/,src/interface/, or an external npm package in Infrastructure is a violation
The automated version of this test is a custom ESLint rule using eslint-import-resolver-typescript with no-restricted-imports configured per directory.
6. The Test Pyramid Through the Hexagonal Lens
Hexagonal Architecture makes the testing strategy self-evident:
| Test Type | What It Tests | Adapter Used | Speed |
|---|---|---|---|
| Domain Unit | Order, Money, LineItem methods |
None (pure domain) | < 1ms each |
| Application Unit | Use Cases with in-memory doubles | InMemoryOrderRepository, MockPaymentProcessor |
< 5ms each |
| Adapter Integration | PrismaOrderRepository contract |
Real Postgres (Docker) | ~100ms each |
| Driving Adapter E2E | HTTP controller → Use Case → In-Memory repo | supertest + In-Memory adapters |
< 20ms each |
| Full Integration | All real adapters assembled | Postgres + Stripe sandbox | ~500ms each |
The majority of tests live in the first two tiers — fast, deterministic, no external dependencies. The hexagonal structure is the architectural reason this is possible: the application core is independent of all adapters, so it can be tested with the fastest possible doubles.
Summary
| Concept | Domain Rule |
|---|---|
| Port | An interface defined inside the hexagon; describes a required capability in domain language |
| Driving Adapter | Translates external input (HTTP, Kafka, CLI) into a Command DTO; calls the Use Case |
| Driven Adapter | Implements a Driven Port using a specific technology; lives in Infrastructure |
| Symmetry | HTTP and Kafka are equally "outside" the hexagon; neither is privileged |
| Constraint Verification | TypeScript project references + grep audit + CI gate enforce the dependency direction |
| Test Pyramid | Domain units are fastest; adapter integration tests are fewest; the hexagonal structure makes this natural |
What's Next
In Part 8, we revisit all five SOLID principles through a backend-first lens — showing how each principle manifests specifically in Express.js middleware, repository adapters, and event handler registration, with before/after refactoring examples for each. Part 8: SOLID in Node.js & Express →
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.