MVC, Hexagonal, and Clean Architecture
Concept
These three patterns all answer the same underlying question — how should application code be arranged so that business logic doesn't get welded to infrastructure (databases, HTTP frameworks, external APIs) — but they draw the boundary differently and were born at different points in that idea's evolution.
MVC (Model-View-Controller), formalized in Smalltalk-80 in the late 1970s, splits an application into three roles: the Model holds state and business rules, the View renders that state, and the Controller takes input and updates the Model. In its original desktop-UI form, the Model could notify Views directly of changes (observer-style). Web-framework MVC (Rails, Django, Spring MVC, ASP.NET MVC) reshaped it into a per-request cycle: a Controller receives an HTTP request, calls into the Model, and selects a View to render the response — there's no persistent observer relationship, just a request/response pass through the three roles. This is the pattern most working developers meet first, and also the one most often criticized for how "Model" tends to swell into a dumping ground for everything that isn't routing or rendering ("fat model" or, worse, "fat controller" when business logic leaks upstream instead).
Hexagonal Architecture (Ports & Adapters), coined by Alistair Cockburn in 2005, generalizes the same instinct into an explicit rule: the application's core logic defines ports — interfaces stating what it needs from or offers to the outside world — and everything external (a database, an HTTP API, a message queue, a CLI, a test harness) plugs in through an adapter that implements a port. The core never imports a database driver or an HTTP framework; it only depends on the ports it defined. The name comes from drawing the core as a hexagon with ports on its faces, purely to avoid privileging any one side (top/bottom, like layered diagrams do) — there's no architectural meaning to the number six.
Clean Architecture, popularized by Robert C. Martin in 2012, layers the same idea into concentric rings — Entities (enterprise-wide business rules) at the center, Use Cases (application-specific business rules) around them, then Interface Adapters, then Frameworks & Drivers at the outermost ring — governed by the Dependency Rule: source-code dependencies may only point inward, never outward. An inner ring must never know the name of anything in an outer ring; a database schema change in the outermost ring cannot force a change to a Use Case.
The three aren't competitors so much as three points on the same axis of "how strictly is business logic isolated from infrastructure": MVC (as commonly practiced) draws the boundary loosely and leaves it easy to violate; Hexagonal draws it as a hard interface boundary but doesn't prescribe internal layering of the core itself; Clean Architecture prescribes both the hard boundary and an internal layering discipline for the core. In practice, many real Hexagonal or Clean systems still use MVC within their web-adapter layer — a Controller receiving an HTTP request and translating it into a call against a Use Case port is a completely normal way to implement the "driving adapter" side of a hexagon.
Tradeoffs
| Approach | Benefit | Cost |
|---|---|---|
| MVC (framework-default) | Minimal ceremony, matches what most web frameworks scaffold out of the box, fast to start | No enforced boundary between business logic and framework/DB — logic tends to leak into Controllers or bloat the Model, and testing business rules often drags in the framework or a real database |
| Hexagonal (Ports & Adapters) | Core logic testable with fake adapters, infra genuinely swappable (swap Postgres for an in-memory adapter in tests) | More upfront interfaces/wiring; overkill for a small CRUD app whose "business logic" is thin enough that the boundary adds ceremony without protecting much |
| Clean Architecture | Same benefits as Hexagonal, plus an explicit internal layering that keeps Entities/Use Cases from depending on delivery mechanism at all | Most ceremony of the three — more files, more indirection, and teams new to it often over-apply the ring structure to genuinely simple features |
| No explicit boundary (business logic directly calls the DB/framework APIs) | Fastest to write initially | Logic and infrastructure become inseparable; any infra change (swap ORMs, add a new delivery channel) risks touching business rules, and unit-testing logic in isolation becomes difficult or impossible |
The real cost that all three "boundary" patterns share is discipline over time, not implementation difficulty at the start: a hexagon or a clean-architecture ring is easy to draw correctly on day one and easy to violate by month six, the first time someone reaches for a shortcut ("just call the repository directly from the controller, it's faster") under deadline pressure. The pattern only pays for itself if the boundary is actually enforced in code review, not just documented in a diagram.
When to use / when not to
- Reach for plain MVC when the application genuinely is thin CRUD glued to a UI — the boundary-enforcing patterns add real ceremony that a small, low-complexity app doesn't recoup.
- Reach for Hexagonal or Clean Architecture once business logic is complex enough that testing it currently requires spinning up a real database, HTTP server, or other infrastructure — that's the concrete signal the boundary is worth the ceremony, not a stylistic preference.
- Also reach for them when infrastructure genuinely needs to be swappable — multiple delivery mechanisms (REST API + CLI + message-queue consumer) calling the same business logic, or a real need to swap a datastore.
- Don't adopt Clean Architecture's full four-ring structure for a service with one thin Use Case per endpoint — the ring boundaries add files and indirection with nothing meaningful to protect; Hexagonal's single core/adapter boundary is often enough on its own.
- Don't treat any of these as a testing strategy substitute — the boundary makes business logic easier to unit-test, it doesn't test anything by itself.
Common pitfall
Drawing the ports/interfaces correctly at the start, then letting a "just this once" shortcut (a Use Case reaching directly into an ORM model instead of going through its repository port, a Controller embedding a business rule instead of delegating to a Use Case) become normal, until the boundary exists only in the diagram and no longer in the dependency graph. Because nothing enforces the Dependency Rule automatically without tooling (import-linters, architecture tests like ArchUnit, or code review vigilance), the violation is invisible until an infrastructure change unexpectedly ripples into business logic — which is exactly the failure the pattern was adopted to prevent.
Engineering Lens
The strong design-review answer isn't naming the pattern ("we use Hexagonal Architecture") — it's being able to point at the actual dependency direction in the import graph and show that inner/core code has zero imports from outer/infrastructure code, ideally backed by an automated check rather than convention alone. The pattern's value is fully proportional to how consistently the boundary is enforced; a codebase that nominally follows Clean Architecture but has Use Cases importing an ORM model directly has paid the ceremony cost of the pattern without collecting its actual benefit (independent testability, swappable infrastructure).
Related
- Adapter Pattern — Hexagonal's "adapters" are a direct application of the Adapter pattern at an architectural scale, translating between the core's ports and a specific external technology
- SOLID, DRY, KISS, YAGNI — the Dependency Inversion Principle (the "D" in SOLID) is the same idea Hexagonal and Clean Architecture apply at the whole-application level: depend on abstractions, not concrete infrastructure