Clean Architecture / Hexagonal Architecture
Keeping business logic independent of frameworks, databases, and UI, so any of those can be swapped without touching the core logic.
What is it?
You already know layered architecture: presentation, business logic, data access, stacked so each one talks to its neighbor. Clean Architecture (and its close cousin, Hexagonal Architecture, also called "ports and adapters") takes the same underlying idea — keep concerns separated, keep dependencies pointing one direction — and makes one thing explicit and non-negotiable: the business logic sits at the center, and absolutely nothing about frameworks, databases, or the UI is allowed to leak into it. Every dependency has to point inward, toward the business logic, never outward from it.
Concretely: your core logic defines abstractions for what it needs (often called ports — "I need something that can save an order," "I need something that can look up a user"). Outside that core, you write adapters that implement those ports using real technology — a Postgres adapter, a REST API adapter, a command-line adapter. The web framework, the database driver, the UI toolkit — all of that lives in the adapters, not the core. If you swapped your entire web framework for a different one, or moved from REST to a CLI, the business logic in the center wouldn't need to change at all, because it never depended on any of that in the first place.
This is dependency inversion (which you've already learned) applied consistently as an organizing principle for an entire application, not just one class.
Explain like I'm 10
Think of a board game's rules booklet: the rules (business logic) don't care whether you're playing with a physical board, an app, or over video call reading moves aloud. Each of those is just a different 'adapter' for playing the same underlying game.
Examples
Business logic entangled with a framework
// Express-specific request/response objects leak into the core logic
app.post("/orders", async (req, res) => {
if (!req.body.items || req.body.items.length === 0) {
return res.status(400).json({ error: "Order must have items" }); // business rule, tangled with HTTP
}
const total = req.body.items.reduce((s, i) => s + i.price, 0);
await pgPool.query("INSERT INTO orders (total) VALUES ($1)", [total]); // tangled with Postgres
res.json({ total });
});The rule 'an order must have items' and the pricing calculation are stuck inside an Express route handler, directly wired to a specific database client. None of this logic could run outside a live HTTP + Postgres setup.
Business logic isolated behind ports
// core/placeOrder.js — no Express, no Postgres, no imports from either
export function placeOrder(items, orderRepository) {
if (!items || items.length === 0) {
throw new Error("Order must have items");
}
const total = items.reduce((s, i) => s + i.price, 0);
orderRepository.save({ items, total });
return { total };
}
// adapters/expressOrderController.js — the HTTP adapter
app.post("/orders", async (req, res) => {
try {
const result = placeOrder(req.body.items, pgOrderRepository);
res.json(result);
} catch (e) {
res.status(400).json({ error: e.message });
}
});placeOrder is plain JavaScript with no knowledge of HTTP or Postgres — it could run behind a CLI, a message queue, or a test with equal ease. The Express handler is now just a thin adapter translating HTTP into a call to the core.
How it works
Picture concentric circles: business rules (often called entities) at the very center, application-specific logic (use cases) around them, and everything volatile — web frameworks, databases, external APIs, the UI — on the outside, in the adapters. The strict rule is the Dependency Rule: source code dependencies can only point inward. Outer layers know about inner layers; inner layers know nothing about outer ones. The core defines the interfaces (ports) it needs; the outer adapters implement them — dependency inversion, applied at the scale of the whole application.
┌─────────────────────────────────────┐
│ Adapters (web, DB, CLI) │
│ ┌───────────────────────────────┐ │
│ │ Use Cases (application logic)│ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ Entities (core rules) │ │ │
│ │ └─────────────────────────┘ │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
Dependencies always point inward →Why does it exist?
It exists because business logic is usually the most valuable, longest- lived, and most-tested part of a system, while frameworks, databases, and UI technologies churn constantly — a web framework might be replaced in three years, but the rule "an order needs at least one item" probably won't change. Entangling the durable part with the disposable parts means every framework upgrade or database migration risks the one part of the system you can least afford to break, and makes the core logic nearly impossible to test without spinning up the whole stack.
When to use it
It earns its cost in systems with real, evolving business logic that outlives any particular framework or database choice — the kinds of systems expected to be maintained for years, ported across technologies, or tested extensively at the logic level without spinning up a full stack.
When not to use it
For a small CRUD app or an internal tool where the 'business logic' is close to nonexistent — mostly just moving data in and out of a database — imposing a full ports-and-adapters structure adds significant ceremony (interfaces, adapters, wiring) for very little actual protection, since there's barely any core logic to protect.
Common mistakes
Adding the ports-and-adapters structure but still importing a framework type (like an Express
Request) into the core logic, quietly breaking the Dependency Rule.Treating this as identical to plain layered architecture and missing the key difference: the strict inward-only dependency direction and the framework-independence of the center.
Over-applying it to simple applications, producing many interfaces and adapter classes that wrap almost no real logic.
Practice exercises
- Easy:
Take an Express route handler that both validates a request body and contains a business rule, and extract the business rule into a plain function with no Express dependency.
- Medium:
Design the ports (interfaces) a 'library book checkout' use case would need — e.g., looking up a book, checking a member's borrowing limit, recording the checkout — without referencing any specific database or framework.
- Hard:
Take a small existing project (or a hypothetical to-do app with a REST API and a database) and describe how you'd restructure it into entities, use cases, and adapters, explaining what would need to change if you replaced the database.
Interview questions
What is the core idea behind Clean Architecture / Hexagonal Architecture?
Business logic sits at the center of the application and has zero dependency on frameworks, databases, or UI; those volatile pieces live in outer 'adapters' that depend inward on the core, never the reverse.
What is a 'port' and what is an 'adapter' in this style of architecture?
A port is an abstraction the core logic defines describing what it needs (e.g., 'something that can save an order'); an adapter is a concrete implementation of a port using real technology (e.g., a Postgres-backed order repository, or an HTTP controller).
Why keep business logic independent of the web framework?
So the business logic can be tested and evolved without a live framework or server running, and so the framework can be upgraded or replaced without risking changes to the core rules of the application.