Skip to content
Bitloops - Git captures what changed. Bitloops captures why.
HomeAbout usDocsBlog
ResourcesSoftware ArchitectureClean Architecture: The Dependency Rule and Concentric Layers

Clean Architecture: The Dependency Rule and Concentric Layers

Dependencies point inward: frameworks depend on business logic, not the reverse. Business rules know nothing of databases or web frameworks. Test logic without infrastructure. Swap databases without rewriting the domain. Concentric layers enforce this discipline.

9 min readUpdated March 4, 2026Software Architecture

Definition

Clean Architecture, introduced by Robert C. Martin (Uncle Bob), organizes a system into concentric layers. The core contains domain entities and business rules. Outer layers contain frameworks, databases, and UI. The fundamental rule: source code dependencies point inward only. Inner layers never depend on outer layers.

This isn't new in concept, but Clean Architecture makes the pattern explicit and provides a practical framework for implementation. It answers a specific question: how do you structure a system so that business logic is independent of technical choices?

Why This Matters

Most systems start with tight coupling: the business logic depends on the database library, which depends on the web framework, which depends on everything. Changes ripple outward and inward unpredictably. Testing requires spinning up the entire system. Replacing the database means rewriting business logic.

Clean Architecture decouples the problem. The domain (entities, business rules, use cases) is pure logic with zero dependencies on frameworks. The database, web framework, and UI are details that can change without touching the core. You test business logic without touching any infrastructure. These layer boundaries must be enforced as architectural constraints for AI agents.

This solves three concrete problems:

Testability. Your business rules don't know they're being called from a web controller or a CLI. You test logic directly, fast, without a database.

Independence from frameworks. You're not locked into Spring Boot or Django forever. You can change frameworks without rewriting the domain.

Longevity. Business rules are stable—they don't change because a new database came out. Infrastructure is volatile—it gets replaced. Separating them buys you years before a rewrite.

The Layers: Inside Out

Clean Architecture defines four layers, from innermost to outermost:

Entities

Entities encapsulate the highest-level business rules. They're not database tables or DTOs. They're objects that represent concepts that would still exist even if you weren't building software.

A User entity might have:

  • Properties: email, createdAt, status
  • Methods: activate(), canAccessPremium(), hasExpired()

The entity knows about business rules (a user can only be activated once, premium access expires) but nothing about persistence or HTTP.

// Entity - pure business logic, no dependencies
class User {
  private id: string;
  private email: string;
  private status: 'pending' | 'active' | 'suspended';
  private premiumExpiration: Date | null;

  constructor(id: string, email: string) {
    this.id = id;
    this.email = email;
    this.status = 'pending';
    this.premiumExpiration = null;
  }

  activate(): void {
    if (this.status !== 'pending') {
      throw new Error('User is already activated');
    }
    this.status = 'active';
  }

  canAccessPremium(): boolean {
    if (this.status !== 'active') return false;
    if (!this.premiumExpiration) return false;
    return new Date() < this.premiumExpiration;
  }
}
typescript

Entities are the most stable part of the system. They change slowly, if at all, across the lifetime of the product.

Use Cases

Use cases (also called Application Services or Interactors) encode business logic specific to the application. They orchestrate entities to produce an outcome.

A CreateUserUseCase might:

  1. Validate the email format
  2. Check if email is unique
  3. Create a User entity
  4. Persist it
  5. Send a welcome email
  6. Return the created user

Use cases depend on entities (they use them) but not on web frameworks or databases

// Use Case - orchestrates business logic
class CreateUserUseCase {
  constructor(
    private userRepository: UserRepository,
    private emailService: EmailService
  ) {}

  execute(request: CreateUserRequest): CreateUserResponse {
    // Validate
    if (!this.isValidEmail(request.email)) {
      throw new InvalidEmailError('Email format is invalid');
    }

    // Check uniqueness
    const existing = this.userRepository.findByEmail(request.email);
    if (existing) {
      throw new DuplicateUserError('User already exists');
    }

    // Create entity
    const user = new User(generateId(), request.email);
    user.activate();

    // Persist via repository (interface, not concrete)
    this.userRepository.save(user);

    // Notify
    this.emailService.sendWelcome(user.email);

    return { userId: user.id, email: user.email };
  }

  private isValidEmail(email: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }
}
javascript

Notice: the use case depends on abstractions (UserRepository, EmailService) defined as interfaces, not concrete implementations. The database and email provider are details injected in.

Interface Adapters

This layer translates between the external world (HTTP requests, database queries, file systems) and the use cases.

It contains:

Controllers. Receive HTTP requests, extract parameters, call use cases, format responses.

// Controller - HTTP adapter
class CreateUserController {
  constructor(private createUserUseCase: CreateUserUseCase) {}

  async handle(req: Request, res: Response): Promise<void> {
    try {
      const response = this.createUserUseCase.execute({
        email: req.body.email
      });
      res.status(201).json(response);
    } catch (error) {
      if (error instanceof DuplicateUserError) {
        res.status(409).json({ error: error.message });
      } else if (error instanceof InvalidEmailError) {
        res.status(400).json({ error: error.message });
      } else {
        res.status(500).json({ error: 'Internal server error' });
      }
    }
  }
}
javascript

Gateways/Repositories. Implement persistence. They convert entities to database records and back. They implement the interfaces that use cases depend on.

// Repository - database adapter
class PostgresUserRepository implements UserRepository {
  constructor(private db: Database) {}

  async save(user: User): Promise<void> {
    await this.db.query(
      'INSERT INTO users (id, email, status) VALUES ($1, $2, $3)',
      [user.id, user.email, user.status]
    );
  }

  async findByEmail(email: string): Promise<User | null> {
    const row = await this.db.query(
      'SELECT * FROM users WHERE email = $1',
      [email]
    );
    if (!row) return null;
    return new User(row.id, row.email);
  }
}
typescript

Presenters. Format data for display (JSON, HTML, etc.). In modern systems these are often simple—return a DTO and let the framework handle serialization.

This layer knows about the outside world. It doesn't care about business rules. Its job is translation.

Frameworks & Drivers

The outermost layer contains frameworks (Express, Django, React), libraries (database drivers), and configuration. It's minimal—mostly setup and configuration to wire everything together.

// Framework layer - composition and setup
const db = new PostgresConnection('postgres://...');
const userRepository = new PostgresUserRepository(db);
const emailService = new SendgridEmailService(process.env.SENDGRID_KEY);
const createUserUseCase = new CreateUserUseCase(userRepository, emailService);
const createUserController = new CreateUserController(createUserUseCase);

const app = express();
app.post('/users', (req, res) => createUserController.handle(req, res));
app.listen(3000);
javascript

The Dependency Rule

This is the core principle: source code dependencies can only point inward.

  • Entities know about nothing
  • Use cases know about entities and interfaces, but not about controllers or databases
  • Controllers and repositories know about everything (they're at the boundary)
  • Frameworks know about everything

Layered architecture

Frameworks & Drivers

Express, PostgreSQL, etc.

Interface Adapters

Controllers, Repositories

Use Cases

Business logic orchestration

Entities

Business rules, pure logic

Why? Because inner layers are more abstract and stable. Outer layers are concrete and volatile. Dependencies should flow from volatile to stable.

When you violate this rule—when a use case imports from a controller, or an entity imports from a database—you've created a dependency on something that will change. Your business logic becomes tangled with implementation details.

Common Mistakes

Putting too much in entities. Entities should encapsulate business rules. They shouldn't have repositories, loggers, or API clients. If an entity has dependencies, they're in the wrong layer.

Confusing use cases with controllers. A controller takes HTTP input, calls a use case, returns HTTP output. A use case is pure business logic. They're adjacent but different. Many frameworks blur this line.

Skinny repositories. Your repository should be more than a thin wrapper over db.query(). It should handle the translation between entities and persistence format. If your repository is just calling the ORM, you haven't decoupled the domain from persistence.

Not using interfaces. If a use case imports a concrete PostgresUserRepository, you've tied the use case to PostgreSQL. Use interfaces. Inject implementations. This is what makes testing (and database replacement) possible.

Too many layers. Clean Architecture is a pattern, not a law. You don't need exactly four layers. A small app might need two. A large system might need seven. The principle matters: dependencies point inward. Use as many layers as that principle requires, no more.

When Clean Architecture Works

Large systems with complex business logic. When your business rules are worth protecting from infrastructure changes, Clean Architecture is worth the effort.

Teams with multiple backends. Build the domain once. Attach a REST API, a GraphQL API, a CLI, a gRPC service. All use the same use cases.

High-value test coverage. When your business logic is critical, you want fast, reliable tests. Clean Architecture tests run without a database.

Systems with volatile infrastructure. If you know you'll replace the database, experiment with ORMs, or pivot storage layers, Clean Architecture's independence pays off.

When It's Overkill

Small, CRUD-heavy applications. If your app is mostly "accept input, save to database, return the row," Clean Architecture is overhead. A simpler layered structure works fine.

Prototypes and MVPs. When you're learning the domain and the requirements are unclear, architecture overhead slows you down. Build first, refactor once you know what matters.

Truly static systems. If the system won't change (unlikely, but it happens), architecture doesn't matter.

AI Agents and Clean Architecture

AI agents can violate Clean Architecture at machine speed. An agent might write a use case that imports a controller, or an entity with a database dependency. The code works locally but breaks the structure.

Bitloops helps here. You can define architectural constraints: "Use cases can't import from controllers." "Entities must have zero external dependencies." When an agent generates code, Bitloops checks those constraints before the code lands. Violations are caught immediately, not in code review months later.

This is where Clean Architecture and AI tooling intersect: explicit boundaries (Clean Architecture) + automated enforcement (Bitloops) = architecture that scales with agent-assisted development.

FAQ

Isn't Clean Architecture a lot of boilerplate?

For small systems, yes. But the boilerplate is the price of independence. You write a repository interface, then an implementation. That feels redundant until you need to swap databases. Then you're grateful the use cases didn't know about PostgreSQL.

Should every use case have its own class?

Usually, yes. A CreateUserUseCase is different from GetUserUseCase. They have different inputs, outputs, and logic. Lumping them together creates a god object. One use case per class is a good rule of thumb.

What about queries (read-only use cases)?

Treat them the same as mutations. A GetUserQuery is still a use case—it orchestrates business logic to retrieve data. It depends on repositories, same as CreateUserUseCase. Some systems separate commands and queries (CQRS), but Clean Architecture doesn't require it.

How do I handle cross-cutting concerns like logging or transactions?

Through interfaces and decorators. Your repository interface defines save(entity). You implement it in a database repository. You wrap it in a logging repository, a transaction repository, whatever you need. This is dependency injection in action.

Can I use ORMs with Clean Architecture?

Yes, but be careful. ORMs like Hibernate or Sequelize make it easy to accidentally couple the domain to the database. If your entities inherit from an ORM base class, you've violated the dependency rule. Keep ORMs in the repository layer, not in entities.

What about web frameworks—how do they fit?

Frameworks are in the outermost layer. You write minimal controller code that calls use cases. The framework (Express, Django, Spring) is a detail. If you need to switch frameworks, you rewrite controllers, not use cases.

Primary Sources

  • Robert C. Martin's canonical text on dependency rules, component boundaries, and framework-independent business logic. Clean Architecture
  • Martin's foundational principle that source code dependencies point inward toward stable business logic. Dependency Rule
  • Fowler's comprehensive guide to application architectural patterns and design principles. Architectural Patterns
  • Evans' domain-driven design methodology for structuring complex software systems effectively. Domain-Driven Design
  • Bass, Clements, and Kazman's guide to software architecture decision-making and tradeoffs. Architecture in Practice
  • Richards and Ford's practical introduction to software architecture fundamentals and decision frameworks. Fundamentals of Architecture

Get Started with Bitloops.

Apply what you learn in these hubs to real AI-assisted delivery workflows with shared context, traceable reasoning, and architecture-aware engineering practices.

curl -sSL https://bitloops.com/install.sh | bash