Blog

Clean architecture dependency rule: why dependencies point inward

Robert C. Martin's dependency rule: source code dependencies only point inward, toward business logic. What it means, common violations, and how to enforce it.

Alex Mercer

The clean architecture dependency rule says that source code dependencies can only point inwards, toward the business rules. Nothing in an inner circle, such as entities and use cases, can know anything at all about something in an outer circle, such as controllers, databases or web frameworks. Robert C. Martin set out the rule in his 2012 post The Clean Architecture, where he calls it "the overriding rule that makes this architecture work."

Most systems don’t become messy overnight. It happens slowly. Code that once had clear boundaries starts to blur as teams move faster and deadlines pile up. A database query ends up in a controller. Business logic leaks into the UI. Infrastructure code reaches straight into the domain.

Over time, everything becomes tightly coupled. A small change breaks something unrelated. Tests are painful to write. New developers can’t tell where logic is supposed to live.

This is what happens when dependency rules aren’t enforced. Clean architecture relies on dependencies flowing in one direction. Once that flow breaks, the structure degrades.

Maintaining clean architecture means understanding these rules and actively enforcing them as the codebase grows. Security flaws are a separate problem, and our guide to AI code security covers the ones that show up in AI-written code.

TLDR

  • Clean architecture uses dependency rules to maintain clear boundaries between layers. The core rule is simple: dependencies only point inward toward business logic, never outward toward infrastructure. 

  • This means the domain code never depends on databases, frameworks, or external services. Violating this rule creates tight coupling that makes code hard to test and modify. Enforcing dependency rules requires static analysis tools, automated code review, and team discipline. 

  • Tools like ArchUnit, dependency-cruiser, import-linter, and AI-powered code review platforms can automatically catch violations during pull requests. When dependency rules are enforced consistently, systems stay maintainable as they grow.

What is clean architecture?

Clean architecture is the name Robert C. Martin (Uncle Bob) gave, in a 2012 blog post, to his attempt at combining earlier designs such as hexagonal architecture (ports and adapters) and onion architecture into a single idea. It organizes code into layers with strict rules about how those layers interact.

The four main layers:

  • Entities (Domain layer): Core business rules and domain logic. This is the heart of your application. It contains the fundamental business concepts that don't change based on technology choices.

  • Use Cases (Application layer): Application-specific business rules. These orchestrate the flow of data to and from entities and direct entities to use their business rules.

  • Interface Adapters: Convert data between the format most convenient for use cases and the format required by external systems. Controllers, presenters, and gateways live here.

  • Frameworks and Drivers: External tools and frameworks. Databases, web frameworks, UI components. These are details that can be swapped out without affecting business logic.

The arrangement creates a dependency structure where inner layers don’t know about outer layers. Martin draws these layers as concentric circles and says the four are schematic. You may need more than four, but the dependency rule always applies.

The dependency rule explained

The dependency rule is the core principle that makes clean architecture work. It states:

Source code dependencies only point inward

Nothing in an inner circle can know anything about an outer circle. Inner layers don't import, reference, or mention anything from outer layers.

What this means in practice:

  • Domain code never imports database libraries.

  • Business logic never references web frameworks.

  • Use cases don’t know about HTTP frameworks or SQL implementations.

  • Entities have no idea whether they're being used in a web app or a mobile app.

Direction of dependencies:

Frameworks & Drivers → Interface Adapters → Use Cases → Entities

Dependencies flow toward the center. The domain (entities and use cases) is independent. Everything else depends on it.

This is backwards from how many developers naturally code. It feels easier to have business logic call the database directly. But that creates dependencies that point outward, violating the rule.

Why dependency rules matter

Following dependency rules creates specific benefits that become more valuable as systems grow. Here’s how:

1. Better testability
When business logic doesn’t depend on databases or frameworks, it’s easy to test. No database setup. No HTTP mocks. Tests run faster and focus on rules, not plumbing.

2. Replaceable technology
If the domain is isolated, changing tools stays contained. Switching databases or moving from REST to GraphQL affects only the outer layers. What feels permanent early on often isn’t, and clean boundaries make change safer later.

3. Localized changes
With dependencies pointing inward, updates don’t ripple across the system. Framework or schema changes don’t break core logic. Work stays predictable, which helps teams move faster without breaking things. When changes are contained, the usual Speed vs quality in code tradeoff becomes easier to manage, because teams know exactly what a change will affect.

4. Clear boundaries for new developers
Dependency rules remove guesswork. Business logic lives in the domain. Data access goes in repositories. HTTP stays in controllers. The architecture itself guides decisions.

Common dependency rule violations

Even teams that understand clean architecture slip into patterns that violate dependency rules.

1. Direct database calls from business logic

typescript

// Violation: Business logic depends on the database

class OrderService {

  async processOrder(orderId: string) {

    const order = await database.query('SELECT * FROM orders WHERE id = ?', orderId);

    // business logic here

  }

}

The business logic now depends on SQL and database libraries. Testing requires a database. Switching databases means changing business code.

Correct approach: Business logic depends on an interface. The outer layer implements it.

typescript

// Business logic depends on the interface, not the implementation

interface OrderRepository {

  findById(id: string): Promise<Order>;

}

class OrderService {

  constructor(private orderRepo: OrderRepository) {}

  

  async processOrder(orderId: string) {

    const order = await this.orderRepo.findById(orderId);

    // business logic here

  }

}

2. Framework references in the domain code

python

# Violation: Domain code imports web framework

from flask import request

class UserRegistration:

    def register(self):

        email = request.form['email']

        # registration logic

The domain now depends on Flask. You can't use this code in a CLI tool or background job. Testing requires mocking HTTP requests.

Correct approach: Domain accepts data as parameters.

python

# Domain code has no framework dependencies

class UserRegistration:

    def register(self, email: str):

        # registration logic

The controller (outer layer) extracts data from the request and passes it to the domain.

3. Use cases importing infrastructure

java

// Violation: Use case imports logging library

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

class CreateInvoice {

    private Logger logger = LoggerFactory.getLogger(CreateInvoice.class);

    

    public void execute(InvoiceData data) {

        logger.info("Creating invoice");

        // business logic

    }

}

The use case depends on a specific logging framework. Changing logging libraries requires modifying business code.

Correct approach: Use case depends on an interface. Infrastructure implements it.

How to enforce dependency rules

Understanding rules is one thing. Enforcing them consistently is another. Teams need mechanisms that catch violations automatically.

1. Static analysis tools

Code review tools that analyze code structure can detect dependency violations.

ArchUnit (Java): Defines architectural rules as unit tests.

java

@ArchTest

static final ArchRule domainShouldNotDependOnInfrastructure =

    noClasses()

        .that().resideInAPackage("..domain..")

        .should().dependOnClassesThat()

        .resideInAPackage("..infrastructure..");

ArchUnitNET or NetArchTest (.NET): Architecture rules written as unit tests.

dependency-cruiser (JavaScript and TypeScript): Validates dependencies against rules you define.

import-linter (Python): Enforces rules you define on the imports between your Python modules.

go-cleanarch (Go): Validates layer dependencies in Go projects.

These tools run as part of builds. Dependency violations fail the build, preventing them from merging.

2. Code review with architectural awareness

Manual code review often misses dependency violations, especially as teams and codebases grow.

AI code review platforms analyze every pull request for architectural patterns and flag when dependency rules are broken. cubic's ready-made maintainability standards agent, for example, flags pull requests that put logic in the wrong layer when another module already owns the concept. For code that's already merged, cubic's codebase scans look for bugs and security issues across the whole repository, on demand or on a schedule.

3. A package organization that reflects layers

Structure your codebase so layers are visible in the directory structure.

/domain

  /entities

  /use-cases

/infrastructure

  /database

  /external-services

/presentation

  /controllers

  /views

This makes violations obvious. If you see /domain importing from /infrastructure, something's wrong.

4. Dependency injection frameworks

Frameworks like Spring, .NET Core DI, or manual dependency injection help enforce rules by making dependencies explicit.

When dependencies are injected, it's clear what each layer needs. It's harder to accidentally create dependencies that point in the wrong direction.

Maintaining rules as systems scale

Small systems can maintain clean architecture through discipline. Large systems need automation.

1. Automated checks in CI/CD

Run architectural validation as part of continuous integration. Treat violations the same as failing tests: they block merging.

This prevents incremental degradation. Each violation gets caught immediately rather than accumulating over months.

2. Documentation and team training

New team members need to understand why dependency rules exist and how to follow them. Documentation should explain:

  • What each layer is responsible for.

  • In which direction can dependencies flow?

  • Common patterns that violate rules.

  • How to structure code correctly.

Training reduces violations by making rules clear before code is written.

3. Regular architectural reviews

Looking at the architecture from time to time helps catch problems that single pull requests don’t show.

Pay attention to repeated issues. If the same dependency rules are being broken again and again, the design may need a rethink, or the team may need clearer guidance.

Checking dependency health should be part of technical debt analysis. When architectural rules slowly slip, technical debt builds up without anyone noticing.

How AI code review helps maintain a clean architecture

AI code review platforms can enforce dependency rules automatically across pull requests. They look at how code is structured as well as how it’s written, and flag cases where dependencies point in the wrong direction.

When a pull request introduces a dependency violation, AI review flags it immediately with specific feedback about which rule was broken and how to fix it.

Architecture problems also show up in AI-written code. On cubic's Bug Bench, which ranks AI coding models by the bugs cubic finds in the code they write, the four models ranked for architecture and design issues run from 0.46 for GPT-5.6 Sol to 2.05 for GPT-6 Astra, which is statistically tied with Claude Opus 5.5 and Claude Opus 5. A rate of 1.00 is the all-model rate, and lower is better. Bug Bench is observational: teams choose their models, so a rate reflects the code each model was used for as well as the model.

cubic reviews every pull request, and you can add your dependency rules as a custom agent: describe the rule in plain language or link the architecture doc you already keep in the repo, and limit it to paths with glob patterns such as domain/**. cubic then flags pull requests that break it. The consistency helps teams maintain clean architecture even as the codebase and team grow.

For enterprise teams with strict architectural requirements, automated enforcement ensures compliance without slowing development velocity.

Building maintainable, scalable software systems

Clean architecture, when backed by enforced dependency rules, helps systems stay maintainable as they grow. Dependencies flow inward toward business logic, keeping core layers independent while outer layers depend on them.

Problems start when shortcuts break this flow. Business logic reaches into databases, domain code imports frameworks, and use cases rely on infrastructure. This is where AI code review adds real value. Automated reviews continuously check pull requests for dependency violations, catching issues early before architectural drift sets in.

Over time, enforcing dependency rules pays off. Code is easier to test, technology choices remain flexible, and changes stay contained. New developers can understand boundaries quickly and contribute with confidence.

Clear separation between responsibilities allows software systems to grow and adapt without becoming fragile or hard to change.

FAQ

Where did Robert C. Martin define the dependency rule?

He first set it out in his blog post The Clean Architecture on blog.cleancoder.com, dated 13 August 2012. He covers it again in chapter 22 of his book Clean Architecture: A Craftsman's Guide to Software Structure and Design (Pearson, 2017), and InformIT has that chapter online as The Clean Architecture Dependency Rule.

How can control flow outward if dependencies point inward?

Through dependency inversion. In Martin's example, control starts in the controller, moves through the use case and ends in the presenter, so the use case has to call something in an outer circle. It calls an interface that lives in the inner circle, which Martin labels the Use Case Output Port, and the presenter implements that interface. The call goes outward at runtime while the source code dependency still points inward.

Is the dependency rule the same as dependency injection?

No. The dependency rule says which way source code dependencies may point, and dependency injection is one common way to follow it. Code in an outer layer creates the concrete class, such as a SQL-backed repository, and passes it to a use case that only knows the interface. The OrderService example above takes its OrderRepository through the constructor for this reason.

Ready to enforce architectural rules automatically?

Book a demo and see how AI code review catches dependency violations in every pull request.

Table of contents