The Cult of Clean Architecture: When Dependency Inversion Becomes a Liability

Clean Architecture is often hailed as the gold standard for mobile development, but is it always the right choice? This article critically analyzes the cost of strict adherence to Dependency Inversion, the myth of the "stable domain," and why traditional Layered Architecture might actually be the pragmatic winner for agile teams.

Apr 23, 2025 - 18:02
Nov 21, 2025 - 23:27
 0  2
The Cult of Clean Architecture: When Dependency Inversion Becomes a Liability

In the world of mobile development, particularly within the Android ecosystem, Clean Architecture has evolved from a software design pattern into something resembling a cult. It dominates conference talks, articles, and community debates. The prevailing sentiment suggests that if you aren't strictly following Clean Architecture, your code is inherently flawed, unscalable, or "dirty."

This article aims to strip away the dogma and take a pragmatic look at Clean Architecture. We will analyze where it provides genuine value and where it introduces unnecessary complexity. The goal is not to attack the methodology, but to challenge the default assumption that it is the only correct way to build software.

Defining the Terms: What is "Clean"?

One of the biggest hurdles in discussing architecture is the semantic ambiguity. Every developer has a slightly different definition of "Clean Architecture." For many, it simply implies code that is readable, decoupled, and interface-based.

Robert C. Martin (Uncle Bob) chose an incredibly effective marketing name. Unlike Onion Architecture (which implies a shape) or Hexagonal Architecture (which implies a structure), Clean Architecture implies a value judgment. If you aren't using it, does that mean your architecture is dirty? This psychological framing often stifles objective technical discussion.

For this analysis, we will define Clean Architecture strictly as the approach described by Robert Martin, characterized by concentric layers and the Dependency Rule.

The Core Mechanism: Dependency Inversion

The defining characteristic of Clean Architecture in mobile apps is Dependency Inversion. While separation of concerns exists in many patterns, strict dependency inversion is what sets Clean Architecture apart.

The rule states that source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle. Ideally:

  • The Domain Layer sits at the center. It contains pure business logic and has no dependencies.
  • Outer layers (UI, Data, Frameworks) depend on the Domain.

Contrast with Layered Architecture

To understand the cost of Clean Architecture, we must compare it with the standard Layered Architecture. In a classic layered approach, dependencies flow strictly from top to bottom: Presentation → Domain → Data.

In this traditional setup, a ViewModel calls a UseCase, which in turn calls a Repository implementation directly (or via an interface defined in the data layer).

The criticism of this approach is valid: if the Data Layer changes (e.g., switching from SQL to Realm, or changing a DTO), those changes can ripple up to the Domain Layer. To prevent this, Clean Architecture flips the arrow.

By applying Dependency Inversion, the Domain Layer defines an interface (e.g., UserRepository), and the Data Layer implements it. The Domain no longer depends on Data; Data depends on Domain.

The Practical Cost in Mobile Development

In theory, this isolation is perfect. In practice, specifically for Android/iOS development, it introduces friction that often outweighs the benefits.

1. The Multi-Module Nightmare

Modern mobile projects are often modularized to speed up build times and enforce separation. A typical structure suggested by Google looks like this:

When you try to enforce strict Clean Architecture across these modules, circular dependencies become a risk. If you have a shared :data:user module and feature modules :feature:home and :feature:profile, applying Dependency Inversion becomes tricky.

If you place repository interfaces in the Feature Domain, but the implementation lives in a Shared Data module, how do you wire them up without the Shared Data module depending on every single Feature module? You end up having to create a separate "Shared Domain" module just to hold interfaces, or duplicating definitions.

// Complications arising from strict inversion
package com.example.feature1.domain

// If we put the interface here...
interface UserRepository { /* ... */ }

class GetMenuUseCase(
    private val userRepository: UserRepository,
) { /* ... */ }

// ...then the Shared Data module must depend on Feature 1 to implement it.
// This breaks the architecture if Feature 2 also needs User Data.

You are forced to create complex dependency graphs just to satisfy the rule that "Domain must not depend on Data," even when the Data layer (e.g., a stable User Repository) is actually one of the most stable parts of your app.

Developers often resort to creating Granular Data modules per feature or a massive Shared Domain module, both of which increase build complexity and maintenance overhead.

2. The Myth of the Stable Domain

Uncle Bob’s premise is that Business Logic is stable and Infrastructure is volatile. Therefore, we protect the stable logic from the volatile DB/Network frameworks.

In mobile development, the reality is often the opposite. We work in Agile environments. The "Business Logic" (how a screen behaves, A/B tests, flow variations) changes every sprint. Meanwhile, your Retrofit client, your Room database, and your User object structure might remain unchanged for years.

// Logic that changes frequently due to product requirements
class CheckoutUseCase(
    private val cartRepository: CartRepository,
    private val discountService: DiscountService,
    private val abTestManager: AbTestManager,
) {
    fun execute(): CheckoutResult {
        val items = cartRepository.getItems()
        // Business logic changes here constantly based on marketing needs
        val discounts = if (abTestManager.isFeatureEnabled("new_discount")) {
            discountService.calculateNew(items)
        } else {
            discountService.calculateLegacy(items)
        }
        return CheckoutResult(items, discounts)
    }
}

If the Domain is the most volatile part of your app, heavily insulating it from a stable Data layer using strict Dependency Inversion is over-engineering. You are paying a high architectural tax to protect a component that is going to be rewritten next week anyway.

The Case for Pragmatic Layered Architecture

For many mobile applications, the "classic" Layered Architecture is not a mistake—it is the optimal choice. It aligns with how Android and iOS SDKs are structured.

Layered Architecture does not mean "Spaghetti Code." You still use SOLID principles. You still use UseCases to encapsulate logic. You still use interfaces where they facilitate testing. But you remove the dogmatic restriction that the Domain layer cannot know about the Data layer.

Conclusion

Clean Architecture is a powerful tool for complex, enterprise-grade applications with long lifecycles and varying infrastructure. However, treating it as a default requirement for every mobile project is a mistake.

When business requirements change faster than technical infrastructure, and when modularization becomes a headache solely to satisfy dependency rules, it is time to step back. A well-structured Layered Architecture often provides 80% of the benefits with 20% of the friction.

What's Your Reaction?

Like Like 0
Dislike Dislike 0
Love Love 0
Funny Funny 0
Angry Angry 0
Sad Sad 0
Wow Wow 0
trants I'm a Fullstack Software Developer focusing on Go and React.js. Current work concentrates on designing scalable services, reliable infrastructure, and user-facing experiences.