Architecture

Clean Architecture in Android: Beyond the Buzzword

A practical guide to implementing Clean Architecture in production Android apps without over-engineering. How separating concerns across data, domain, and presentation layers reduced critical bugs by 25% in our team.

2025 9 min read

Why Most Clean Architecture Implementations Fail

Clean Architecture is the most over-discussed and under-practiced pattern in Android development. Every team claims to follow it, but most implementations fall into one of two traps: either the architecture is so simplified that it provides no real benefit, or it is so over-engineered that it slows down development without proportional gains.

The problem is not with Robert Martin's original concepts. The problem is with how we translate them into Android's specific constraints — Activity lifecycles, configuration changes, dependency injection scoping, and the constant tension between reactive data flows and imperative UI updates.

The Three-Layer Contract

In production, I enforce a strict three-layer separation with clear dependency rules:

The critical rule: the Domain layer has no dependencies. Not on Retrofit. Not on Room. Not on Android framework classes. If your use case imports android.content.Context, your architecture is broken.

Use Cases: The Misunderstood Middle Layer

The most common question I get from junior developers: "Do I really need a use case that just calls the repository?" The answer is yes, but not for the reason you think.

Use cases are not about adding logic today. They are about creating a stable API surface that your ViewModel depends on. When business requirements change — and they always do — you add the new logic inside the use case without touching the ViewModel or the Repository. The use case is a firewall against change propagation.

A use case that "just calls the repository" today is a use case that will contain critical business logic in three months. The cost of creating it now is one class. The cost of adding it later is a refactor.

Practical Implementation

Dependency Injection Setup

With Dagger-Hilt, each layer gets its own module. The Domain module provides use case bindings. The Data module provides repository implementations and binds them to Domain interfaces. The Presentation module is scoped to ViewModel lifecycles.

This scoping means that when a feature module is removed, its entire dependency subtree disappears cleanly — no orphaned bindings, no runtime crashes from missing providers.

Error Handling Across Layers

Each layer has its own error type. The Data layer throws DataException (network errors, parse failures, database conflicts). The Domain layer translates these into DomainError sealed classes that represent business-meaningful failures. The Presentation layer maps DomainError to user-visible messages.

This prevents UI code from containing network error string parsing — a pattern I have seen in too many production codebases.

Results in Practice

After migrating our team's primary app to this structure, we measured:

When Not to Use It

Clean Architecture is not a universal answer. For a simple CRUD app with three screens and no complex business logic, it is genuine over-engineering. Use it when your app has business rules that exist independently of the UI, when multiple data sources need coordination, or when your team is larger than two developers.

MVVMUse CasesRepository Pattern Dependency InjectionDagger-HiltKotlin