Clean and hexagonal architecture
Keep business rules independent of frameworks, databases and the web: the dependency rule, layers, and ports and adapters.
On the roadmap:Software Design · step 3: Architecture styles
Lesson 6 of 7 · 11 min read · quiz · exercise
The problem
In many applications, business rules are tangled with framework code, SQL and HTTP details. Then:
- You cannot test a rule without a database and a web server.
- Changing a framework or database means rewriting business logic.
- The code is organised around technology, not around what the system does.
Several architectures (hexagonal, onion, clean) solve this with the same idea: put business rules at the centre and keep details at the edges.
The dependency rule
Figure SDA-SDE-06-1 · Clean architecture: dependencies point inwards · © forcsguy.com
Source code dependencies point inwards only. Entities know nothing about use cases; use cases know nothing about the web or the database. Outer layers depend on inner ones, never the reverse.
| Layer | Contains | Knows about |
|---|---|---|
| Entities | Core business objects and rules that would exist whatever the technology | Nothing else |
| Use cases | Application-specific actions ("place order", "cancel subscription") that coordinate entities | Entities and the ports they define |
| Interface adapters | Controllers, presenters, repositories' implementations, mappers | Use cases |
| Frameworks and drivers | The web framework, database, message queue, UI | Adapters |
Before: logic tangled in the controller
After: business rules in the middle
What changed
- Business rules can be tested without a web server or database
- The database or framework can be replaced at the edge
- There are more files and layers to maintain
Ports and adapters (hexagonal)
The application is the hexagon. It talks to the outside only through ports: interfaces it owns. Adapters translate between the outside world and the ports.
- Driving (inbound) ports: how the world calls the application (
PlaceOrder). Adapters: REST controller, CLI, message listener, test. - Driven (outbound) ports: what the application needs (
OrderRepository,PaymentGateway,Clock). Adapters: SQL implementation, payment provider client, fake for tests.
// Port owned by the application
interface OrderRepository { save(order: Order): Promise<void>; byId(id: string): Promise<Order | null> }
interface PaymentGateway { charge(cents: number, token: string): Promise<boolean> }
// Use case: pure application logic
class PlaceOrder {
constructor(private orders: OrderRepository, private payments: PaymentGateway) {}
async run(input: { items: Item[]; token: string }) {
const order = Order.create(input.items); // entity enforces rules
if (!(await this.payments.charge(order.totalCents, input.token))) throw new Error("Payment failed");
await this.orders.save(order);
return order.id;
}
}
// Adapters live outside, implementing the ports
class SqlOrderRepository implements OrderRepository { /* uses a database */ }
class StripeGateway implements PaymentGateway { /* calls the provider */ }
// Composition root wires everything together at start-up
const placeOrder = new PlaceOrder(new SqlOrderRepository(db), new StripeGateway(key));
This is the dependency inversion principle from SOLID applied to a whole system.
Benefits
- Testable: run use cases with in-memory fakes, fast and without infrastructure.
- Replaceable details: change the database, queue or web framework by writing an adapter.
- Focus on the domain: the central code reads like the business process.
- Deferred decisions: you can postpone choosing a database.
Costs and cautions
- More structure: interfaces, mappers and wiring add code. For a small CRUD app it can be overkill.
- Mapping between layers (domain objects, database rows, API shapes) is repetitive; do it deliberately and avoid leaking database entities into the API.
- Do not create layers "because the diagram says so". Add a boundary where a real change or testing need exists.
- It is about dependencies, not folder names: a good folder layout helps but does not guarantee the rule is followed. Enforce it with tests or lint rules.
Related ideas
- Domain-driven design supplies the language for the centre of the hexagon (next lesson).
- Microservices are a deployment choice; each service can use this internal structure.
- Composition root: one place, at start-up, where concrete classes are chosen and injected.
Real-world reading
Related reading
- Cockroach Labs BlogCockroach Labs engineers · Cockroach LabsBlogs
- Fivetran BlogFivetran engineers · FivetranBlogs
- ClickHouse BlogClickHouse engineers · ClickHouseBlogs
- InfluxData BlogInfluxData engineers · InfluxDataBlogs
Show 6 moreShow fewer
- Elastic BlogElastic engineers · ElasticBlogs
- AWS Database BlogAWS database teams · Amazon Web ServicesBlogs
- How SQLite WorksSQLite team · SQLiteDocumentation
- AWS Big Data BlogAmazon Web Services engineers · Amazon Web ServicesBlogs
- Viget Engineering BlogViget engineers · VigetBlogs
- Meituan Blog (Chinese)Meituan engineers · MeituanBlogs
Check your understanding
Question 1 of 3
0 correct so far
What is the dependency rule in clean architecture?
Go deeper
- The Clean Architecture (blog), Hexagonal Architecture by Robert C. Martin; Alistair Cockburn: search for both articles; short, free introductions to the same family of ideas
Practise it: URL shortener
Everything below is already set up for this lesson: change the design, run the traffic, and see whether you meet the goal.
- 1The task
- 2Build and test
Read-heavy, a few links are very popular.
Add or remove parts, move traffic up and down, and break something to see what gives way first.
Try it yourself
A Place Order function lives inside a web controller and runs SQL directly. Redesign it with clean architecture: which pieces exist, and which depends on which?
Click what a strong design here would need:
More in Design practice
© 2026 forcsguy.com. Original lesson text and diagrams. Books and sites linked above belong to their authors.