Blog

How to maintain clean architecture with dependency rules in your codebase

A practical guide

Alex Mercer

Feb 6, 2026

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.

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, NDepend, 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 a clean architecture?

A clean architecture is a software design approach created by Robert C. Martin (Uncle Bob) in 2012. 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.

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;

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().resideInPackage("..domain..")

        .should().dependOnClassesThat()

        .resideInPackage("..infrastructure..");

NDepend (.NET): Query language for detecting dependency violations.

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 goes a step further with full codebase scans. Along with pull request reviews, it scans the entire codebase to uncover existing violations and architectural drift.

By combining pull request checks with continuous codebase scans, cubic helps teams enforce dependency rules consistently and keep a clean architecture intact as systems scale.

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, not just 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.

cubic analyzes architectural patterns as part of every review. It catches dependency violations that would require deep code reading from human reviewers. 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.

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