Software Journal
Software Architecture Updated Aug 10, 2026 5 min read

Clean Architecture: What It Actually Means

Clean Architecture is not about layers and folders — it's about dependency direction. A practical explanation of the pattern, the dependency rule, and when it's worth the ceremony.

Benmalek Zohir

Contributor

Share
Abstract concentric circles representing clean architecture layers

Clean Architecture is one of the most misunderstood ideas in software. Ask ten developers and you’ll get ten descriptions: “It’s those circles,” “it’s layers with controllers and use cases,” “it’s over-engineering that slows teams down.” The truth is simpler and more useful: Clean Architecture is a rule about which way dependencies point. Once you understand that, the rest is detail.

The core idea: the dependency rule

Uncle Bob’s original formulation is deceptively short:

Source code dependencies must point only inward, toward the higher-level policies.

That’s it. The famous diagram is a circle with four rings — Entities, Use Cases, Adapters, Frameworks — and the rule is that nothing on the outside may be referenced by code on the inside.

         ┌─────────────────────────────┐
         │   Frameworks & Drivers      │   ← HTTP, DB, CLI, UI
         │  ┌───────────────────────┐  │
         │  │ Interface Adapters    │  │   ← Controllers, presenters, repos
         │  │  ┌─────────────────┐  │  │
         │  │  │ Use Cases       │  │  │   ← Application logic
         │  │  │  ┌───────────┐  │  │  │
         │  │  │  │ Entities  │  │  │  │   ← Business rules
         │  │  │  └───────────┘  │  │  │
         │  │  └─────────────────┘  │  │
         │  └───────────────────────┘  │
         └─────────────────────────────┘

In practice, most teams only need two layers in their head: the core (domain + application logic) and the edges (everything that touches the outside world). The question for every new file is: does this depend on something concrete from the outside world, or does it depend on a contract?

The problem it solves

The concrete problem: frameworks and infrastructure change, and business logic shouldn’t have to change with them.

If your OrderService imports DjangoORM, express.Router, or KafkaProducer directly, then your business logic is welded to your infrastructure. Swap the database and you rewrite the domain. Swap the message broker and you rewrite the domain. Over five years, your codebase becomes a pile of happy-path glue between frameworks, and the actual business rules are untestable without a database and a running server.

Clean Architecture decouples the two with interfaces.

A concrete example

Consider an order service in TypeScript. The core defines the business rule — an order can be placed if the customer is not blocked and the total is under the credit limit:

// domain/order.ts
export class Order {
  constructor(
    public id: string,
    public customerId: string,
    public total: number,
  ) {}
}

export interface OrderRepository {
  findById(id: string): Promise<Order | null>;
  save(order: Order): Promise<void>;
}

The application layer implements the use case, depending only on the interface:

// application/place-order.ts
import type { OrderRepository } from "../domain/order";

export class PlaceOrder {
  constructor(private orders: OrderRepository) {}

  async execute(customerId: string, total: number): Promise<Order> {
    const order = new Order(crypto.randomUUID(), customerId, total);
    await this.orders.save(order);
    return order;
  }
}

The infrastructure layer adapts to the framework. It depends inward — it implements the repository contract:

// infrastructure/postgres-order-repository.ts
import { Order, type OrderRepository } from "../domain/order";

export class PostgresOrderRepository implements OrderRepository {
  constructor(private db: PgPool) {}

  async findById(id: string): Promise<Order | null> {
    const { rows } = await this.db.query(
      "SELECT * FROM orders WHERE id = $1",
      [id],
    );
    return rows[0] ? new Order(rows[0].id, rows[0].customer_id, rows[0].total) : null;
  }

  async save(order: Order): Promise<void> {
    await this.db.query(
      "INSERT INTO orders (id, customer_id, total) VALUES ($1, $2, $3)",
      [order.id, order.customerId, order.total],
    );
  }
}

Notice what just happened: PlaceOrder doesn’t know Postgres exists. You can test it with a fake repository in milliseconds:

// test/place-order.test.ts
const fakeRepo: OrderRepository = {
  findById: async () => null,
  save: async (o) => { saved.push(o); },
};
const service = new PlaceOrder(fakeRepo);

The trade-off that nobody mentions

The honest caveat: this costs some ceremony. Interfaces, dependency injection, mapping between layers — for a CRUD app that will never change its database, it’s overhead. The dependency rule is most valuable when:

  • The business logic is genuinely complex and long-lived
  • You have multiple delivery mechanisms (web, CLI, jobs, API)
  • The team is large enough that modular boundaries keep people from stepping on each other
  • You’re reasonably sure the infrastructure will change (it usually will)

It is least valuable when the “business rules” are a thin wrapper around a CRUD API. Start simple, extract boundaries when they earn their keep — architecture is a continuous decision, not a certificate.

Testing and the rule of testability

The deepest payoff is testability. When domain logic depends only on interfaces, you can run the entire test suite in seconds with in-memory fakes, and your tests mean something: they test your actual business rules, not a mock-festooned simulation of them.

Conclusion

Clean Architecture, at its core, is one sentence: make your business logic depend on contracts, and make the outside world implement those contracts. If you keep dependencies pointing inward, the framework becomes a detail. Learn to see the distinction between a file that implements a rule and a file that ships a rule — then keep the first category free of infrastructure.

Share
Portrait of Benmalek Zohir

Written by

Benmalek Zohir

Founder, AI Engineer & Full Stack Developer

Benmalek Zohir is an AI Engineer, Full Stack Developer, and technology enthusiast focused on artificial intelligence, software development, and emerging technologies. He is the founder of SoftwareJournal.blog, where he shares practical insights, software discoveries, AI tools, and the latest developments in technology.

The Software Journal Dispatch

One excellent engineering read, every week.

A concise digest of our best new essays on architecture, tooling, databases, and the craft of software. No spam, no noise — unsubscribe anytime.