10 · Frontend Architecture
Hexagonal, Onion, and Clean Architecture, DDD, and Feature-Sliced Design — how the dependency rule applies to a codebase whose “database” is an HTTP API and whose “UI” is a reactive component tree. The goal: business logic that doesn’t know React exists.
Positioning
Most frontend codebases rot the same way: business rules leak into components, components leak into data fetching, and a framework upgrade or an API change ripples through everything. Architecture is the discipline of arranging dependencies so that the things that change for different reasons and at different rates are separated, and the stable things never depend on the volatile ones. None of the patterns below are React-specific; they are about dependency direction.
Foundations: the one rule that underlies all of them
The Dependency Rule (Robert C. Martin): source-code dependencies point inward, toward higher-level policy. Inner layers (domain/business rules) know nothing about outer layers (UI, frameworks, the network). The name on a thing in an outer circle must never be mentioned by an inner circle. Every pattern here — hexagonal, onion, clean — is a different drawing of this same rule.
The payoff is testability (test domain logic with no DOM, no network), replaceability (swap REST for GraphQL, React for Solid, without touching rules), and comprehensibility (the important logic lives in one place, not smeared across components).
Deep dive
1. Hexagonal Architecture (Ports & Adapters) — Alistair Cockburn
The application core sits in the middle. It communicates with the outside world only through ports (interfaces the core defines). Adapters implement those ports for specific technologies.
- Driving (primary) adapters call into the core — e.g., a React component, a CLI, a test harness. They depend on a driving port (an application service / use-case interface).
- Driven (secondary) adapters are called by the core — e.g., an HTTP client, localStorage, a WebSocket. The core depends on a driven port (e.g.,
UserRepository), and the adapter implements it. - Key insight: the core defines the interface; the adapter conforms. This is dependency inversion — the arrow points toward the core from both sides. The UI is “just another adapter,” which is liberating: your business logic doesn’t privilege the browser.
On the frontend: a CheckoutService (core) depends on a PaymentGateway port; a StripeAdapter implements it. The React component is a driving adapter that calls checkoutService.pay(). You can test the whole checkout flow with a fake gateway and no React.
2. Onion Architecture — Jeffrey Palermo
Concentric rings: Domain Model (entities, value objects) at the center → Domain Services → Application Services → Infrastructure / UI / Tests on the outside. Dependencies point inward only. It’s hexagonal’s idea expressed as layers rather than ports; the distinguishing emphasis is putting the domain model at the very center and treating persistence/UI as outer detail.
3. Clean Architecture — Robert C. Martin
Synthesizes hexagonal, onion, and others into four rings: Entities (enterprise-wide business rules) → Use Cases (application-specific rules) → Interface Adapters (presenters, controllers, gateways — translate between use cases and the outside) → Frameworks & Drivers (React, the HTTP library, the browser). The dependency rule governs crossings; to cross outward in control flow while keeping dependencies pointing inward, use the Dependency Inversion Principle (define an interface in the inner ring, implement it in the outer).
A frontend mapping that works in practice:
- Entities / domain: pure TS —
Order,Money, invariants. No React, no fetch. - Use cases:
placeOrder(cart, gateway)— orchestrate domain + ports. - Interface adapters: mappers (API DTO → domain), repository implementations, view-models/presenters that shape domain data for the UI.
- Frameworks & drivers: React components, TanStack Query, the
fetchclient, the router.
The component becomes thin: it calls a use case and renders a view-model. This is the antidote to the “3,000-line component that fetches, transforms, validates, and renders.”
3b. Hexagonal vs Onion vs Clean — the head-to-head
These three are variations on one idea — the Dependency Rule (dependencies point inward; the domain knows nothing about I/O/UI/frameworks). They are complementary, not competing; Clean explicitly synthesizes the other two. The differences are framing, emphasis, and ceremony — not philosophy.
| Aspect | Hexagonal (Ports & Adapters) | Onion | Clean |
|---|---|---|---|
| Author / year | Alistair Cockburn (2005) | Jeffrey Palermo (2008) | Robert C. Martin (2012) |
| Core metaphor | A core with ports; adapters plug in on both sides | Concentric rings around a domain model | Four concentric rings (Entities→Use Cases→Adapters→Frameworks) |
| Primary emphasis | Symmetry of I/O — UI and DB are both just adapters; the “driving vs driven” distinction | Domain model at the dead center; infrastructure is outermost | Explicit layering + the dependency rule + DIP for crossings; names the Use Case layer |
| Granularity of layers | Few (core + ports + adapters) | Several rings (model, domain services, app services, infra) | Four named rings with prescribed roles |
| What it’s best at communicating | ”Your UI is not special — it’s an adapter; swap REST/GraphQL/CLI freely" | "Protect the domain model; depend inward" | "Here is exactly where each kind of code goes and how control crosses boundaries” |
| Ceremony / cost | Lowest conceptual overhead | Medium | Highest (most prescriptive) |
How to choose (the pragmatic read):
- They produce the same runtime structure: pure domain in the middle, I/O behind interfaces, UI/framework at the edge. If you’ve done one well, you’ve effectively done all three.
- Hexagonal is the most useful mental model for frontend, because its headline insight — the UI is just one more adapter — is exactly what frees your logic from React. Reach for its vocabulary (ports/adapters/driving/driven) in design discussions.
- Clean is the most useful teaching/structure reference because it names the Use Case layer and is explicit about DIP at boundaries — handy when you need a shared, prescriptive blueprint across a team.
- Onion is the simplest to explain to someone new (“rings, depend inward, protect the domain”).
- In practice you don’t pick one dogmatically — you apply the shared dependency rule with ceremony proportional to the domain’s complexity (the synthesis in §7). The label matters far less than: is the domain pure, is I/O behind a port, is the UI thin?
4. Domain-Driven Design (DDD) — Eric Evans / Vaughn Vernon
DDD is about modeling the business domain in code and aligning software boundaries with business boundaries.
- Ubiquitous language — the same vocabulary in code, tests, and conversations with domain experts.
Order, notDataRow. - Bounded contexts — a model is valid only within a boundary; “Customer” in Billing ≠ “Customer” in Support. On the frontend this maps cleanly to feature modules and to micro-frontend boundaries (
09): each team owns a bounded context end to end. - Building blocks: Entities (identity over time), Value Objects (immutable, equality by value —
Money,EmailAddress), Aggregates (consistency boundary with a root), Domain Events, Repositories (collection-like access behind a port), Domain Services (logic that doesn’t belong to one entity). - Strategic vs tactical: strategic DDD (context mapping, where to draw boundaries) matters even when you skip the tactical patterns. On the frontend, context mapping is often more valuable than aggregates — it tells you how to split the app and the teams.
5. Feature-Sliced Design (FSD) — the frontend-native methodology
A pragmatic, convention-driven architecture built for frontend. Layers (top→bottom, strict downward imports): app → processes (deprecated) → pages → widgets → features → entities → shared. A higher layer may import from lower layers, never sideways within a layer and never upward. Within each layer, code is split into slices (by business domain), and each slice into segments (ui, model, api, lib). It gives teams a shared mental model and prevents the “everything imports everything” spaghetti without the ceremony of full clean architecture. Widely adopted in 2026 as the default “how do we structure a big React/Vue app” answer.
6. SOLID, applied to components
- Single Responsibility — a component renders or orchestrates or fetches, not all three; extract hooks and use cases.
- Open/Closed — extend via composition and props (render props, slots, compound components,
05), not by editing. - Liskov — a
<Button>variant must be substitutable wherever<Button>is expected (don’t break the contract). - Interface Segregation — small focused props/hooks, not a god-prop object.
- Dependency Inversion — components depend on abstractions (a
useOrders()hook, a port), not onfetchdirectly. This is what makes the UI swappable and testable.
7. The pragmatic synthesis (what seniors actually do)
You rarely build a full four-ring clean architecture in a CRUD app — it’s over-engineering. The durable, low-cost wins:
- Keep domain logic in pure functions/modules with zero framework imports. This alone pays for itself.
- Put I/O behind ports (a repository/gateway interface) so it’s mockable and swappable.
- Keep components thin — render + delegate. Push orchestration into hooks/use cases, transformation into mappers.
- Draw boundaries around bounded contexts / features (FSD), and let those boundaries become MFE/team seams if you scale.
- Apply ceremony proportional to the domain’s complexity — a dashboard doesn’t need aggregates; a trading or insurance app might.
Worked example: a use case behind a port
// domain/order.ts — pure, no framework
export type Money = { cents: number; currency: 'BRL' | 'EUR' };
export function total(items: { price: Money; qty: number }[]): Money { /* ... */ }
// application/ports.ts — the core DEFINES the interface
export interface OrderGateway {
place(order: DraftOrder): Promise<PlacedOrder>;
}
// application/place-order.ts — the use case depends on the PORT
export function makePlaceOrder(gateway: OrderGateway) {
return async (draft: DraftOrder): Promise<PlacedOrder> => {
if (draft.items.length === 0) throw new EmptyOrderError(); // domain rule
return gateway.place(draft);
};
}
// infrastructure/http-order-gateway.ts — adapter implements the port
export const httpOrderGateway: OrderGateway = {
async place(order) {
const res = await fetch('/api/orders', { method: 'POST', body: JSON.stringify(toDTO(order)) });
return fromDTO(await res.json()); // mapper at the boundary
},
};
// ui/useCheckout.ts — React is a driving adapter
function useCheckout() {
const placeOrder = useMemo(() => makePlaceOrder(httpOrderGateway), []);
return useMutation({ mutationFn: placeOrder }); // TanStack Query
}
The domain and use case are unit-testable with a fake gateway and zero React. React, fetch, and TanStack Query are all replaceable details at the edge.
Pitfalls & gotchas
- Over-engineering: a four-ring architecture around a settings page is a liability, not a flex. Match ceremony to complexity.
- Anemic domain: putting all logic in “services” and leaving entities as bags of data — you lose the benefits of modeling. (Sometimes acceptable on the frontend, but name the tradeoff.)
- Leaky DTOs: letting API response shapes flow straight into components couples your UI to the backend. Map at the boundary.
- Framework in the core: importing React/Next types into domain code defeats the entire purpose.
- Boundaries that don’t match the org: if your module boundaries cut across team boundaries, Conway’s Law (
09,13) will fight you. - Cargo-culting FSD layer names without understanding the import rule, producing folders that don’t actually constrain dependencies.
Interview questions
- State the Dependency Rule. Why must dependencies point inward?
- Difference between hexagonal, onion, and clean architecture? (Same rule, different framing/emphasis.)
- What are ports and adapters? Give a frontend example of each.
- How do you keep React out of your business logic — and why bother?
- Entity vs value object? Give examples.
- What is a bounded context, and how does it relate to micro-frontends?
- How would you apply Dependency Inversion to data fetching in a component?
- When is clean architecture over-engineering on the frontend?
- How does Feature-Sliced Design constrain imports?
- Map SOLID onto React components.
Recommendations
- Default to the pragmatic synthesis: pure domain modules + ports for I/O + thin components + feature boundaries (FSD).
- Use mappers at every boundary (API↔domain, domain↔view-model) so external shapes never leak inward.
- Let bounded contexts drive both module structure and (at scale) team/MFE seams.
- Reserve full clean architecture for genuinely complex domains; don’t pay the tax otherwise.
- Make “no framework imports in
domain/” a lint rule (e.g., dependency-cruiser / eslint-plugin-boundaries) so it’s enforced, not aspirational.
Books & references
- “Clean Architecture” — Robert C. Martin. The dependency rule, the rings, DIP. The canonical statement.
- “Domain-Driven Design” — Eric Evans (the “blue book”). The foundational text; dense but essential for strategic design.
- “Implementing Domain-Driven Design” — Vaughn Vernon (the “red book”). More practical/tactical than Evans.
- “Domain-Driven Design Distilled” — Vaughn Vernon. The short, approachable on-ramp — start here.
- “Patterns of Enterprise Application Architecture” — Martin Fowler. Repository, mapper, service layer, etc.
- “Get Your Hands Dirty on Clean Architecture” — Tom Hombergs. Concrete, code-first clean architecture.
- Alistair Cockburn’s hexagonal architecture (alistair.cockburn.us) and Jeffrey Palermo’s onion architecture posts — the primary sources.
- feature-sliced.design — the official FSD documentation.
- Khalil Stemmler’s articles (khalilstemmler.com) — DDD/clean architecture applied to TypeScript specifically.
Connections
03-typescript.md— interfaces/structural typing make ports and adapters ergonomic; types enforce boundaries.12-bff-and-data-enrichment.md— the BFF is itself an adapter; mappers translate enriched data into domain shapes.09-micro-frontends.md— bounded contexts become MFE/team boundaries.13-microservices-and-orchestration.md— DDD and Conway’s Law drive backend and frontend decomposition.06-state-management-and-stores.md— stores belong in the application/UI layer, not the domain core.16-testing.md— pure domain + ports is what makes high-value unit testing cheap.20-programming-principles.md— SoC/SRP/DIP/coupling-cohesion are the principles this file scales up.21-oop-foundations.md/22-functional-programming.md— adapters/abstraction (OO) and pure-core-effects-at-edge (FP) under the hood.25-architecture-decisions-and-tradeoffs.md— choosing how much of this structure a given app/team actually needs.