According to a 2024 vFunction survey, over 50% of companies watch more than a quarter of their entire IT budget vanish into technical debt.
This isn't happening because developers are bad at writing code. It's happening because our architectural boundaries are eroding. We're letting business rules bleed into database models, and HTTP controllers bleed into core logic.
The primary culprit? Modern framework marketing and the "tutorial culture" that dictates how we learn to code.
Here's a deep look into why your architecture is bleeding, the engineering principles we're violating, and how Hexagonal Isolation offers a structural cure — with a full working TypeScript codebase, FlowBank, so you can see it applied rather than just described.
1. The Trap: "Time-to-Hello-World" vs. Architectural Debt
Most developers learn to build applications through 15-minute video tutorials or framework quickstarts. To win adoption, frameworks optimize for rapid onboarding — showing you how to build a Todo app in three steps using "magic" shortcuts.
This creates three architectural traps:
- The Active Record Trap — ORMs couple database schemas directly to business entities. Change your table, and your core business model breaks.
- The Controller-to-DB Shortcut — Framework boilerplate encourages writing validation, business rules, and SQL directly inside HTTP handlers.
- The "Magic" Annotation Bleed — Framework-specific decorators injected straight into domain objects make the entire application hostage to that vendor.
Follow these shortcuts long enough and you accumulate Architectural Technical Debt (ATD). Three years later, upgrading your framework or switching database providers means rewriting the foundational business rules of the company.
2. The Solution: What Is Hexagonal Isolation?
Originally coined by Alistair Cockburn as Ports and Adapters, Hexagonal Architecture solves layer bleed by enforcing strict structural isolation.
[External World] ----> ( Port / Interface ) ----> [Core Business Logic]
(HTTP/DB/CLI) (Strict Abstraction) (Pure Domain Rules)
Enter fullscreen mode Exit fullscreen mode
- The Core Domain — Contains nothing but pure business logic. Zero knowledge of the web, databases, file systems, or frameworks.
- Ports (Abstract Interfaces) — The hexagon's edge is built entirely of abstract interfaces. The core only talks to these.
- Adapters (Concrete Implementations) — Postgres, REST APIs, GraphQL, S3 — all live outside the hexagon, implementing the ports to feed data in and out.
By isolating the core through abstraction, you get true plug-and-play architecture. Swap MongoDB for PostgreSQL without touching a single line of your core application rules.
A minimal TypeScript example
This is a trimmed-down version of the pattern used throughout FlowBank, a small open-source banking API built specifically to demonstrate this architecture end to end — transfers, deposits, notifications, and payment processing, all with the core fully isolated from Postgres, Stripe, and Express.
// core/ports/user-repository.port.ts
// The PORT — an abstract interface the core depends on, not a concrete DB.
export interface UserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<void>;
}
// core/domain/user.ts
// The CORE — pure business logic, no framework or DB imports.
export class User {
constructor(public readonly id: string, private email: string) {}
changeEmail(newEmail: string) {
if (!newEmail.includes("@")) {
throw new Error("Invalid email");
}
this.email = newEmail;
}
}
// adapters/postgres-user-repository.ts
// The ADAPTER — concrete implementation, swappable without touching the core.
import { UserRepository } from "../core/ports/user-repository.port";
import { User } from "../core/domain/user";
import { db } from "./db-client";
export class PostgresUserRepository implements UserRepository {
async findById(id: string): Promise<User | null> {
const row = await db.query("SELECT * FROM users WHERE id = $1", [id]);
return row ? new User(row.id, row.email) : null;
}
async save(user: User): Promise<void> {
await db.query("UPDATE users SET email = $1 WHERE id = $2", [
user, // simplified for brevity
user,
]);
}
}
Enter fullscreen mode Exit fullscreen mode
The User class and its business rules never import db-client, Express, or anything Postgres-specific. Tomorrow, swapping PostgresUserRepository for a MongoUserRepository or an InMemoryUserRepository (for tests) requires zero changes to the core.
3. The Theoretical Pillars: SOLID & DDD
Hexagonal isolation isn't an arbitrary design choice — it's the concrete implementation of foundational computer science principles.
The SOLID matrix:
- Dependency Inversion Principle (DIP) — High-level business logic must not depend on low-level frameworks; both depend on abstractions. Hexagonal architecture enforces that all source-code dependencies point inward, toward the core.
- Single Responsibility Principle (SRP) — Domain models gain a single reason to change: a change in business requirements. They're stripped of framework plumbing entirely.
- Open/Closed Principle (OCP) — Open for extension, closed for modification. Need a new SMS provider? Write a new adapter. The core stays untouched.
Domain-Driven Design & component coupling:
Hexagonal isolation belongs to the same family as Clean Architecture (Robert C. Martin) and Onion Architecture (Jeffrey Palermo). It enforces the Acyclic Dependencies Principle (ADP), eliminating circular references, and honors the Stable Dependencies Principle (SDP) by making the most stable component — your business rules — the target of all dependencies.
4. The Engineering Payoff
Implementing hexagonal isolation means more boilerplate upfront, but the long-term ROI is substantial:
- Bulletproof testability — Since the core only depends on abstract ports, you can mock databases and external APIs instantly. Test your entire business logic in milliseconds, no Docker containers or live servers required.
- Framework agnosticism — Your framework becomes a delivery mechanism, not the definition of your application.
- Future-proof evolution — When a faster database or cheaper cloud provider shows up, migration cost drops from a full rewrite to building a single new adapter.
5. See It in a Real Codebase: FlowBank
Reading about a pattern and watching it hold up under a real dependency graph are two different experiences. That's why this article ships with FlowBank — an open-source TypeScript banking API built to demonstrate hexagonal architecture through actual money-movement workflows: transfers, deposits, notifications, and payment processing.
src/
core/ <- the hexagon's interior, zero framework imports allowed
domain/ Account, Money, Transaction — pure business rules
ports/ interfaces the core depends on, never concretes
use-cases/ application logic, orchestrates domain + ports
errors/ domain-specific error types
adapters/ <- everything outside the hexagon
postgres/ real DB implementation of the repository ports
stripe/ real payment gateway implementation
email/ real notification implementation
http/ Express router — a driving adapter, calls IN to use cases
in-memory/ fakes used by tests, swap-in for Postgres/Stripe/email
main.ts <- composition root: the ONLY file allowed to wire
concrete adapters into the use cases
Enter fullscreen mode Exit fullscreen mode
A couple of things worth actually trying rather than just reading about:
-
npm testruns the core's business-rule tests with no database, no network call, and no Docker container — just the in-memory adapters standing in for Postgres and Stripe. - Grep the codebase for proof, not promises:
grep -rl "from.*adapters" src/corereturns nothing. The dependency arrows in the diagram earlier in this article aren't aspirational — they're enforced by what actually got imported. - Try swapping
StripePaymentGatewayfor a hypotheticalPaystackPaymentGateway, orPostgresAccountRepositoryfor a Mongo equivalent. Neither touchesAccount,Money, or the use cases — which is the entire argument of this article, made concrete.
Clone it, break it, extend it. The repo is the proof that this isn't just a diagram — it's a codebase that ships, tests fast, and survives a provider swap.
Conclusion
Technical debt doesn't usually arrive as one bad decision — it accumulates one framework shortcut at a time, until the business logic and the delivery mechanism are so entangled that neither can change without the other breaking. Hexagonal isolation is a direct, structural answer to that: push every framework, database, and vendor SDK to the edges, and keep a small, dependency-free core that only knows about the business it serves.
The payoff isn't theoretical. It shows up as tests that run in milliseconds instead of minutes, migrations that take a sprint instead of a quarter, and a codebase new engineers can actually reason about because the business rules aren't buried inside ORM hooks and route handlers.
Stop building software like a 15-minute tutorial. Isolate your core, protect your boundaries, clone FlowBank and see the pattern for yourself, and claw back your engineering budget.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.