1. Introduction
Clean Architecture, popularized by Robert C. Martin, is a set of principles for organizing software so that business rules are central, frameworks are peripheral, and every layer is independently testable. It draws on decades of ideas — Hexagonal Architecture (Alistair Cockburn, 2005), Onion Architecture (Jeffrey Palermo, 2008), and the Dependency Inversion Principle (Martin, 1996).
This guide goes beyond theory: it provides full code examples, concrete folder structures, testing strategies and a step-by-step migration path so you can apply Clean Architecture today.
2. Why Architecture Matters
Architecture is the set of design decisions that are expensive to change later. Good architecture:
- Reduces cost of change — New features require localized modifications, not system-wide rewrites.
- Enables independent deployability — Teams can release components without coordinating a monolithic deploy.
- Maximizes testability — Business rules can be tested without databases, HTTP servers or UI frameworks.
- Delays irreversible decisions — You can defer choice of database, framework or cloud provider until the last responsible moment.
- Improves team onboarding — A well-layered codebase is navigable by intent, not by framework familiarity.
3. SOLID Principles Refresher
Clean Architecture is built on SOLID. A quick refresher on each principle and how it supports the architecture:
S — Single Responsibility Principle (SRP)
A module should have one and only one reason to change — meaning it should be responsible to one actor. In Clean Architecture, each layer changes for a different reason: domain rules change for business, adapters change for technology, frameworks change for infrastructure.
O — Open-Closed Principle (OCP)
Software entities should be open for extension but closed for modification. Use cases define ports (interfaces); new implementations extend behavior without modifying existing code.
L — Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types. Repository interfaces in the domain layer can be implemented by SQL, NoSQL, in-memory or mock versions — all interchangeable.
I — Interface Segregation Principle (ISP)
Clients should not depend on interfaces they don't use. Keep port interfaces small and focused: OrderReader and OrderWriter instead of a monolithic OrderRepository.
D — Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules — both should depend on abstractions. This is the foundation of the Dependency Rule.
4. Core Principles of Clean Architecture
4.1 Separation of Concerns
Group code by what it does (business rules, orchestration, persistence, presentation) rather than by technical type (models, controllers, services). Each concern lives in its own layer.
4.2 The Dependency Rule
Source code dependencies always point inward. Outer layers know about inner layers; inner layers know nothing about the outer world. This keeps the core stable and technology-independent.
4.3 Framework Independence
The architecture should not be dictated by Express, Spring, Django, or any other framework. Frameworks are tools — they belong at the edges, imported only by the outermost layer.
4.4 Testability by Design
Business rules can be tested without UI, database, web server or any external element. If you need a running database to test a business rule, the architecture has a boundary violation.
4.5 UI and Database as Details
The UI is a detail. The database is a detail. They can be replaced without affecting the core system. Clean Architecture delays these decisions and makes them replaceable.
5. The Concentric Layers
Clean Architecture organizes code into concentric circles, from innermost (most stable, most abstract) to outermost (most volatile, most concrete):
┌───────────────────────────────────────────────┐
│ Frameworks & Drivers (Web, DB, UI, Devices) │
│ ┌───────────────────────────────────────────┐ │
│ │ Interface Adapters (Controllers, Gateways)│ │
│ │ ┌───────────────────────────────────────┐ │ │
│ │ │ Use Cases / Application Business Rules│ │ │
│ │ │ ┌───────────────────────────────────┐ │ │ │
│ │ │ │ Entities / Enterprise Business │ │ │ │
│ │ │ │ Rules │ │ │ │
│ │ │ └───────────────────────────────────┘ │ │ │
│ │ └───────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────┘ │
└───────────────────────────────────────────────┘
Dependencies always point INWARD →
Each layer communicates through ports (interfaces defined by the inner layer) and adapters (implementations provided by the outer layer).
6. The Dependency Rule (in depth)
The single most important rule: nothing in an inner circle can know anything about something in an outer circle.
- Names declared in outer circles must not appear in inner circles — not in code, type annotations, or import statements.
- Data formats crossing boundaries should be simple DTOs or value objects — never framework-specific request/response objects.
- To cross boundaries against the dependency direction (e.g., a use case calling a database), use the Dependency Inversion Principle: the use case defines an interface, and the outer layer provides the implementation.
// ✅ Use Case defines a PORT (interface)
// File: src/domain/ports/OrderRepository.ts
export interface OrderRepository {
findById(id: string): Promise<Order | null>;
save(order: Order): Promise<void>;
}
// ✅ Outer layer provides the ADAPTER (implementation)
// File: src/infrastructure/persistence/PostgresOrderRepository.ts
import { OrderRepository } from '../../domain/ports/OrderRepository';
export class PostgresOrderRepository implements OrderRepository {
async findById(id: string) { /* SQL query */ }
async save(order: Order) { /* SQL insert/update */ }
}
// ✅ Composition root wires everything together
// File: src/main.ts
const repo = new PostgresOrderRepository(pool);
const useCase = new CreateOrderUseCase(repo);
7. Layer 1 — Entities / Domain
Entities encapsulate enterprise-wide business rules — the rules that would exist even if there were no software system. They are the most stable part of the system.
// Entity example: Order
export class Order {
constructor(
public readonly id: string,
public readonly customerId: string,
private items: OrderItem[],
private status: OrderStatus = 'draft'
) {}
addItem(item: OrderItem): void {
if (this.status !== 'draft') throw new Error('Cannot modify a confirmed order');
if (item.quantity < 1) throw new Error('Quantity must be at least 1');
this.items.push(item);
}
confirm(): void {
if (this.items.length === 0) throw new Error('Cannot confirm an empty order');
this.status = 'confirmed';
}
get total(): number {
return this.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
}
}
// Value Object example: Money
export class Money {
constructor(public readonly amount: number, public readonly currency: string) {
if (amount < 0) throw new Error('Amount cannot be negative');
}
add(other: Money): Money {
if (other.currency !== this.currency) throw new Error('Currency mismatch');
return new Money(this.amount + other.amount, this.currency);
}
}
Rules for Entities:
- No imports from any outer layer (no framework, no database, no HTTP).
- Contains only business logic, validation and domain events.
- Uses only the language's standard library.
- Fully testable with plain unit tests — no mocks needed.
8. Layer 2 — Use Cases / Interactors
Use cases contain application-specific business rules. They orchestrate entities, call ports, and implement the workflows that make the application useful.
// Use Case: CreateOrder
import { OrderRepository } from '../ports/OrderRepository';
import { EventBus } from '../ports/EventBus';
import { Order, OrderItem } from '../entities/Order';
interface CreateOrderInput {
customerId: string;
items: { productId: string; quantity: number; price: number }[];
}
export class CreateOrderUseCase {
constructor(
private readonly orderRepo: OrderRepository,
private readonly eventBus: EventBus
) {}
async execute(input: CreateOrderInput): Promise<string> {
// 1. Create entity with business rules
const order = new Order(generateId(), input.customerId, []);
for (const item of input.items) {
order.addItem(new OrderItem(item.productId, item.quantity, item.price));
}
order.confirm();
// 2. Persist through port
await this.orderRepo.save(order);
// 3. Publish domain event through port
await this.eventBus.publish({ type: 'OrderCreated', orderId: order.id });
return order.id;
}
}
Rules for Use Cases:
- Depend on entities and port interfaces — never on concrete implementations.
- Receive input DTOs and return output DTOs — never framework objects (no
Request,Response). - One use case per application action (CreateOrder, CancelOrder, GetOrderDetails).
- Testable by providing mock implementations of ports.
9. Layer 3 — Interface Adapters
Adapters translate between the external world and the application. They convert data from the format most convenient for use cases and entities to the format required by external agencies.
9.1 Controllers (Input Adapters)
// HTTP Controller: translates HTTP → Use Case input
import { CreateOrderUseCase } from '../../application/CreateOrderUseCase';
export class OrderController {
constructor(private readonly createOrder: CreateOrderUseCase) {}
async handlePost(req: HttpRequest): Promise<HttpResponse> {
try {
const orderId = await this.createOrder.execute({
customerId: req.body.customerId,
items: req.body.items
});
return { status: 201, body: { orderId } };
} catch (err) {
if (err.message.includes('Cannot')) return { status: 400, body: { error: err.message } };
return { status: 500, body: { error: 'Internal error' } };
}
}
}
9.2 Presenters (Output Adapters)
Transform use case output into the format required by the delivery mechanism (JSON for REST, HTML for web pages, Protocol Buffers for gRPC).
9.3 Gateways / Repositories (Persistence Adapters)
Implement the port interfaces defined by the domain. These are the concrete database, cache or external API integrations.
10. Layer 4 — Frameworks & Drivers
The outermost layer contains glue code and framework configuration: Express/Fastify routes, Sequelize/TypeORM setup, React components, dependency injection containers, and the composition root that wires everything together.
// Composition Root (main.ts) — wires all layers
import express from 'express';
import { Pool } from 'pg';
import { PostgresOrderRepository } from './infrastructure/persistence/PostgresOrderRepository';
import { RabbitEventBus } from './infrastructure/messaging/RabbitEventBus';
import { CreateOrderUseCase } from './application/CreateOrderUseCase';
import { OrderController } from './adapters/http/OrderController';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const repo = new PostgresOrderRepository(pool);
const eventBus = new RabbitEventBus(process.env.AMQP_URL);
const createOrder = new CreateOrderUseCase(repo, eventBus);
const controller = new OrderController(createOrder);
const app = express();
app.use(express.json());
app.post('/api/orders', (req, res) => controller.handlePost(req).then(r => res.status(r.status).json(r.body)));
app.listen(3000);
The composition root is the only place that knows about all layers. It is the "dirty" spot where concrete classes meet — and that is by design.
11. Hexagonal Architecture (Ports & Adapters)
Alistair Cockburn's Hexagonal Architecture (2005) is the direct ancestor of Clean Architecture. The core application defines ports — input ports (driving) and output ports (driven). Adapters connect the outside world to these ports.
- Driving adapters (left side) — HTTP controllers, CLI commands, message consumers. They call the application.
- Driven adapters (right side) — Database repositories, email services, external APIs. The application calls them through port interfaces.
The hexagonal shape is a metaphor: there is no fixed number of ports — the application can have as many as needed, each with its own adapter.
12. Onion Architecture
Jeffrey Palermo's Onion Architecture (2008) emphasizes concentric layers where dependencies flow inward. It is nearly identical to Clean Architecture but uses different terminology:
- Domain Model (center) — Entities and value objects.
- Domain Services — Business logic that spans multiple entities.
- Application Services — Use cases and orchestration.
- Infrastructure (outer ring) — Persistence, messaging, UI.
Clean Architecture, Hexagonal and Onion are variations of the same idea: protect the core from the details.
13. Alignment with Domain-Driven Design
Clean Architecture and DDD are highly complementary:
- Entities → DDD Entities and Value Objects.
- Use Cases → DDD Application Services.
- Ports → DDD Repository and Domain Service interfaces.
- Bounded Contexts define the boundaries of Clean Architecture modules in larger systems.
- Aggregates enforce transactional consistency boundaries within the Entity layer.
DDD's strategic patterns (Bounded Contexts, Context Maps) guide how to decompose a system; Clean Architecture's tactical patterns guide how to organize code within each bounded context.
14. Example: Create User Flow (Full)
// --- Entity ---
export class User {
constructor(public id: string, public email: string, public name: string) {
if (!email.includes('@')) throw new Error('Invalid email');
}
}
// --- Port ---
export interface UserRepository {
findByEmail(email: string): Promise<User | null>;
save(user: User): Promise<void>;
}
// --- Use Case ---
export class CreateUserUseCase {
constructor(private readonly repo: UserRepository) {}
async execute(email: string, name: string): Promise<User> {
const existing = await this.repo.findByEmail(email);
if (existing) throw new Error('Email already registered');
const user = new User(generateId(), email, name);
await this.repo.save(user);
return user;
}
}
// --- Adapter (Controller) ---
export class UserController {
constructor(private readonly createUser: CreateUserUseCase) {}
async handlePost(req: HttpRequest): Promise<HttpResponse> {
const user = await this.createUser.execute(req.body.email, req.body.name);
return { status: 201, body: { id: user.id, email: user.email, name: user.name } };
}
}
// --- Adapter (Repository) ---
export class PostgresUserRepository implements UserRepository {
constructor(private readonly db: Pool) {}
async findByEmail(email: string) {
const { rows } = await this.db.query('SELECT * FROM users WHERE email = $1', [email]);
return rows[0] ? new User(rows[0].id, rows[0].email, rows[0].name) : null;
}
async save(user: User) {
await this.db.query('INSERT INTO users (id, email, name) VALUES ($1, $2, $3)', [user.id, user.email, user.name]);
}
}
// --- Test (pure unit test, no database) ---
const mockRepo: UserRepository = {
findByEmail: async () => null,
save: async () => {}
};
const uc = new CreateUserUseCase(mockRepo);
const user = await uc.execute('test@example.com', 'Test');
assert(user.email === 'test@example.com');
15. Example: Decoupling an External API
Suppose your application needs to send emails. Never import the email SDK in your use case:
// Port (defined in domain layer)
export interface EmailSender {
send(to: string, subject: string, body: string): Promise<void>;
}
// Adapter 1: SendGrid implementation
export class SendGridEmailSender implements EmailSender {
async send(to: string, subject: string, body: string) {
await sgMail.send({ to, from: 'no-reply@example.com', subject, html: body });
}
}
// Adapter 2: In-memory (for tests)
export class InMemoryEmailSender implements EmailSender {
public sent: { to: string; subject: string }[] = [];
async send(to: string, subject: string) {
this.sent.push({ to, subject });
}
}
// Use Case — knows nothing about SendGrid
export class ResetPasswordUseCase {
constructor(private readonly email: EmailSender, private readonly users: UserRepository) {}
async execute(userEmail: string) {
const user = await this.users.findByEmail(userEmail);
if (!user) return; // don't reveal user existence
const token = generateResetToken();
await this.email.send(user.email, 'Password Reset', `Token: ${token}`);
}
}
If SendGrid is replaced by AWS SES, only the adapter changes — use case and entity code remain untouched.
16. Example: CQRS with Clean Architecture
Command-Query Responsibility Segregation separates write operations (commands) from read operations (queries). Each has its own use case and potentially its own data model:
// Command: CreateOrder (writes)
export class CreateOrderCommand {
constructor(private readonly repo: OrderRepository) {}
async execute(input: CreateOrderInput): Promise<string> { /* ... */ }
}
// Query: GetOrderDetails (reads — can use optimized read model)
export class GetOrderDetailsQuery {
constructor(private readonly readModel: OrderReadModel) {}
async execute(orderId: string): Promise<OrderView> {
return this.readModel.findById(orderId);
}
}
// Read model interface (may hit a denormalized view, cache, or search index)
export interface OrderReadModel {
findById(id: string): Promise<OrderView | null>;
search(filters: OrderFilters): Promise<OrderView[]>;
}
CQRS fits naturally into Clean Architecture because commands and queries are separate use cases with different ports.
17. Folder Structure Patterns
17.1 Layer-First (traditional)
src/
├── domain/
│ ├── entities/ # Order.ts, User.ts, Money.ts
│ ├── ports/ # OrderRepository.ts, EmailSender.ts
│ └── events/ # OrderCreated.ts
├── application/
│ ├── commands/ # CreateOrderUseCase.ts
│ └── queries/ # GetOrderDetailsQuery.ts
├── adapters/
│ ├── http/ # OrderController.ts, UserController.ts
│ ├── persistence/ # PostgresOrderRepository.ts
│ └── messaging/ # RabbitEventBus.ts
├── infrastructure/
│ ├── config/ # database.ts, env.ts
│ └── di/ # container.ts (composition root)
└── main.ts
17.2 Feature-First (modular)
src/
├── modules/
│ ├── orders/
│ │ ├── domain/ # Order.ts, OrderRepository.ts (port)
│ │ ├── application/ # CreateOrderUseCase.ts, GetOrderQuery.ts
│ │ ├── adapters/ # OrderController.ts, PostgresOrderRepo.ts
│ │ └── index.ts # module public API
│ └── users/
│ ├── domain/
│ ├── application/
│ ├── adapters/
│ └── index.ts
├── shared/
│ ├── domain/ # Money.ts, shared value objects
│ └── infrastructure/ # logging, error handling
└── main.ts
Feature-first is recommended for larger systems because it aligns with DDD Bounded Contexts and enables independent module development.
18. Testing Strategies
18.1 Entity Unit Tests
Test business rules with plain assertions — no mocks, no infrastructure:
describe('Order', () => {
it('does not allow confirming an empty order', () => {
const order = new Order('1', 'customer-1', []);
expect(() => order.confirm()).toThrow('Cannot confirm an empty order');
});
it('calculates total correctly', () => {
const order = new Order('1', 'c1', [
new OrderItem('p1', 2, 10.0),
new OrderItem('p2', 1, 25.0)
]);
expect(order.total).toBe(45.0);
});
});
18.2 Use Case Tests (with mock ports)
describe('CreateOrderUseCase', () => {
it('creates and persists an order', async () => {
const mockRepo = { save: jest.fn(), findById: jest.fn() };
const mockBus = { publish: jest.fn() };
const uc = new CreateOrderUseCase(mockRepo, mockBus);
const orderId = await uc.execute({ customerId: 'c1', items: [{ productId: 'p1', quantity: 2, price: 10 }] });
expect(mockRepo.save).toHaveBeenCalledTimes(1);
expect(mockBus.publish).toHaveBeenCalledWith(expect.objectContaining({ type: 'OrderCreated' }));
expect(orderId).toBeDefined();
});
});
18.3 Adapter / Integration Tests
Test real database interactions, HTTP endpoints or message queues. Use containers (Testcontainers) for reproducible environments:
describe('PostgresOrderRepository', () => {
let repo: PostgresOrderRepository;
let pool: Pool;
beforeAll(async () => {
// Start Postgres container via Testcontainers
pool = await setupTestDatabase();
repo = new PostgresOrderRepository(pool);
});
it('saves and retrieves an order', async () => {
const order = new Order('test-1', 'c1', [new OrderItem('p1', 1, 10)]);
await repo.save(order);
const found = await repo.findById('test-1');
expect(found?.id).toBe('test-1');
});
});
18.4 The Testing Pyramid in Clean Architecture
- Many entity unit tests (fast, no dependencies).
- Several use case tests (mock ports, test orchestration).
- Few integration tests (real infrastructure, slow).
- Fewer end-to-end tests (full stack, slowest).
19. Design Patterns that Help
- Dependency Inversion — Define interfaces in inner layers; implement at edges. The backbone of Clean Architecture.
- Repository Pattern — Abstract persistence behind a collection-like interface.
- Command / Use Case Objects — One class per application action, explicit input/output.
- DTO (Data Transfer Object) — Simple data structures that cross layer boundaries without carrying behavior.
- Factory Pattern — Encapsulate complex entity creation.
- Strategy Pattern — Swap algorithms (e.g., pricing strategies) via port interfaces.
- Observer / Event Bus — Decouple domain events from their handlers across layers.
- Decorator — Add cross-cutting concerns (logging, caching, retry) around port implementations without modifying use cases.
20. Common Pitfalls & Anti-Patterns
20.1 Business Logic in Controllers
Controllers should only translate HTTP → use case input and use case output → HTTP. If your controller contains if statements about business rules, extract them into a use case.
20.2 Anemic Domain Model
Entities with only getters/setters and no behavior are not entities — they are data bags. Business logic scattered in services (outside the entity) violates SRP and makes the domain layer meaningless.
20.3 Tight Coupling to Framework APIs
If your use case imports express.Request or HttpServletRequest, you have a boundary violation. Convert framework objects into plain DTOs at the adapter layer.
20.4 Leaky Abstractions
A repository interface that returns ORM entities instead of domain entities leaks infrastructure details into the domain. Map ORM models to domain objects in the adapter.
20.5 Over-Engineering
Not every project needs full Clean Architecture. For a simple CRUD app with no complex business rules, the overhead of multiple layers may not be justified. Apply the architecture proportionally to the complexity of the domain.
20.6 Circular Dependencies
If module A imports from module B and vice versa, extract the shared concept into a separate inner module, or use Dependency Inversion to break the cycle.
21. Migrating a Legacy System
Refactoring toward Clean Architecture is best done incrementally:
- Identify one feature — Pick a well-bounded area with clear business rules.
- Extract entities — Move domain logic out of controllers/services into entity classes. Write unit tests.
- Define ports — Create interfaces for external dependencies (database, APIs).
- Wrap existing code in adapters — Implement port interfaces by wrapping (not rewriting) existing persistence/API code.
- Create use cases — Orchestrate entities through port interfaces. Write tests.
- Repeat — Expand to adjacent features. The old code continues to work while the new code grows around it.
This is the Strangler Fig Pattern applied to architecture: new, well-structured code gradually replaces old code without a risky big-bang rewrite.
22. Performance & Trade-offs
Clean Architecture adds layers of indirection. In the vast majority of applications, the overhead is negligible compared to I/O (database, network). However:
- Mapping overhead — Converting between DTOs, domain objects and persistence models adds CPU time. Profile before optimizing; in most cases it is not the bottleneck.
- Number of abstractions — More files and interfaces mean more code to navigate. Use IDE features (Go to Implementation, Find Usages) to stay productive.
- Localized optimization — If a specific hot path needs raw performance, you can bypass the abstraction locally without contaminating the rest of the architecture.
- "Right-size" the architecture — Apply full layering to complex domains; use simpler structures for CRUD-heavy modules.
23. Team Adoption Tips
- Start with one module — Don't refactor the entire codebase. Pick a new feature and build it with Clean Architecture. Let the team experience the benefits firsthand.
- Create a template — Provide a starter project or module template with the folder structure, ports and a sample use case.
- Enforce boundaries with linting — Use tools like
eslint-plugin-boundaries,ArchUnit(Java), ordependency-cruiserto prevent accidental boundary violations. - Pair on the first use case — Walk through the first implementation together so the entire team understands the flow.
- Document decisions — Use Architecture Decision Records (ADRs) to explain why you chose Clean Architecture and what trade-offs you accepted.
- Iterate on naming — The exact folder and file names matter less than consistent conventions. Agree on names and stick with them.
24. Practical Checklist
- ☐ Entities contain business rules and no framework imports.
- ☐ Use cases depend only on port interfaces and entities.
- ☐ Adapters implement port interfaces and handle translation.
- ☐ The composition root is the only place concrete classes are wired.
- ☐ DTOs cross boundaries — never framework-specific objects.
- ☐ Entity unit tests run without infrastructure.
- ☐ Use case tests use mocked ports.
- ☐ Integration tests verify adapters against real infrastructure.
- ☐ No circular dependencies between layers.
- ☐ Domain events decouple side effects from core logic.
- ☐ Boundary enforcement is automated (linting/architecture tests).
- ☐ New team members can navigate the codebase by following the layers.
25. FAQ
Is Clean Architecture only for large systems?
No, but the level of formality should match the complexity. A simple CRUD app might use a simplified two-layer version (domain + infrastructure). The principles — dependency direction, separation of concerns, testability — apply at any scale.
What's the difference between Clean, Hexagonal and Onion Architecture?
They are variations of the same idea: protect core business logic from external dependencies. Hexagonal uses "ports and adapters" terminology; Onion emphasizes concentric layers; Clean Architecture combines both with explicit layers and the Dependency Rule. In practice, the resulting code structure is nearly identical.
Should every class have an interface?
No. Create interfaces at architectural boundaries — where inner layers need to communicate with outer layers. Entities don't need interfaces. Use cases typically don't expose interfaces (they are consumed, not implemented). Reserve interfaces for ports.
How does Clean Architecture relate to microservices?
Clean Architecture organizes code within a service. Microservices determine how services communicate. Each microservice can use Clean Architecture internally. The domain layer of one service never imports from another — they communicate through APIs or events.
Can I use an ORM with Clean Architecture?
Yes, but keep ORM models in the adapter layer. The domain entity should be a plain class with business logic. The adapter maps between ORM models and domain entities. The use case never calls entity.save() — it calls repository.save(entity).
What about cross-cutting concerns (logging, auth, caching)?
Use the Decorator pattern to wrap port implementations with cross-cutting behavior. For example, a LoggingOrderRepository wraps the real repository and logs calls without modifying the use case.
26. Glossary
- Adapter
- A component that translates between the application's internal interfaces (ports) and external systems (databases, HTTP, messaging).
- Bounded Context
- A DDD concept: a boundary within which a particular domain model is defined and applicable.
- Composition Root
- The place in the application where all concrete implementations are instantiated and wired together.
- CQRS
- Command-Query Responsibility Segregation — separating read and write operations into distinct models.
- Dependency Rule
- The rule that source code dependencies must always point toward the center (inner layers), never outward.
- DTO
- Data Transfer Object — a simple data structure used to pass information across layer boundaries without behavior.
- Entity
- An object defined by its identity rather than its attributes, containing core business rules.
- Interactor
- Another name for a Use Case in Clean Architecture — it orchestrates entities and ports.
- Port
- An interface defined by the inner layer that the outer layer must implement (e.g., a repository interface).
- Use Case
- An application-specific business rule that orchestrates entities and ports to fulfill a user's intent.
- Value Object
- An object defined by its attributes rather than identity (e.g., Money, DateRange). Immutable and replaceable.
27. References & Further Reading
- Robert C. Martin — Clean Architecture (book, 2017)
- Alistair Cockburn — Hexagonal Architecture (Ports and Adapters)
- Jeffrey Palermo — The Onion Architecture
- Dependency Inversion Principle — Wikipedia
- Martin Fowler — Articles on Architecture and Refactoring
- Robert C. Martin — The Clean Coder Blog
- Eric Evans — Domain-Driven Design Reference
28. Conclusion
Clean Architecture provides a pragmatic, battle-tested framework for organizing software so that business rules are central, frameworks are replaceable, and every layer is independently testable. It is not a rigid template — it is a set of principles to apply proportionally to your system's complexity.
Start by applying these principles to a single module: extract an entity, define a port, write a use case, and test it without infrastructure. Once the team sees the benefits — fast tests, localized changes, framework independence — the approach will spread naturally.
Pick one feature in your codebase, extract its business rules into an entity, wrap its database access behind a repository interface, and write a unit test. That single step will teach you more than theory ever could.