Opening

If you have been in Android development for more than a year, you have almost certainly encountered Clean Architecture. It is everywhere. Tutorials, conference talks, blog posts, GitHub samples. The promise is beautiful: separation of concerns, testability, independence from frameworks. And honestly, the theory is solid. Uncle Bob's original ideas make a lot of sense. But here is the thing I want to be honest with you about today. In practice, on real Android projects, Clean Architecture often causes more problems than it solves. Not because the idea is wrong, but because the way most teams implement it is a cargo cult. They copy the structure without understanding the reasoning behind it. And that is what we are going to dig into.

The Promise

Let me start with what Clean Architecture promises, because the promises are genuinely appealing. The core idea is dependency inversion. Your business logic should not depend on your framework. Your domain layer should not know about Android, or Retrofit, or Room. This means you could theoretically swap out your database, your network library, or even your UI framework, and your core logic would not change. You also get testability for free, because your domain logic has no external dependencies. It sounds perfect. And for certain types of software, it genuinely is perfect. The problem is that most Android apps are not that type of software.

The Reality: A Simple "Get User" Operation

Let me show you what Clean Architecture typically looks like in an Android codebase. You want to display a user profile. Simple, right? Here is what you end up building. A UserEntity in your data layer representing the API response. A UserDto also in your data layer for the database row. A User domain model in your domain layer. A UserUiModel in your presentation layer. A UserEntityMapper to convert from entity to domain model. A UserDtoMapper to convert from DTO to domain model. A UserUiMapper to convert from domain model to UI model. A UserRemoteDataSource. A UserLocalDataSource. A UserRepository interface. A UserRepositoryImpl. A GetUserUseCase. And finally your ViewModel. That is thirteen classes for a single "get user" feature. Thirteen. And most of them are doing nothing interesting — just copying fields from one object to another.

The Boilerplate Explosion

Let us look at what one of those mappers actually does. You have a function that takes a UserEntity and returns a User. Inside, it creates a new User object, setting name equals entity dot name, email equals entity dot email, avatar equals entity dot avatarUrl. That is it. It is just copying fields. Now multiply that by every model in your app. Users, posts, comments, products, orders. Each one needs at least two or three mappers. Each mapper is a class file that someone has to create, maintain, review in pull requests, and debug when the field mapping is wrong. And the most painful part? When you add a field to your API response, you have to update the entity, the domain model, the UI model, and every mapper in between. A one-line change in your API contract becomes a ten-file pull request. That is not separation of concerns. That is a boilerplate explosion.

Use Cases That Do Nothing

Now let us talk about use cases. In Clean Architecture, the use case is supposed to encapsulate your business logic. GetUserUseCase, CreateOrderUseCase, ValidatePaymentUseCase. In theory, this is where the interesting stuff lives. In practice, on most Android apps, the use case looks like this: a class with a single invoke function that calls repository dot getUser and returns the result. That is it. No business logic. No transformation. No orchestration. Just a pass-through. Why does it exist? Because the architecture says it should. I have reviewed hundreds of Android codebases, and I would estimate that seventy to eighty percent of use cases are pure delegation. They add a layer of indirection with zero benefit. Every layer you add has a cost: more files to navigate, more indirection to trace, more cognitive load for the next developer.

The Original Intent vs. the Cargo Cult

Here is where I want to be fair to Uncle Bob. His original Clean Architecture was designed for enterprise systems with complex business rules. Think banking software where the domain logic is genuinely intricate — interest calculations, regulatory compliance, multi-step workflows. In those systems, isolating domain logic from infrastructure absolutely pays for itself. But somewhere along the way, the Android community took these ideas and applied them to apps that display lists of items from a REST API. That is cargo cult architecture. You are mimicking the structure without having the problem it was designed to solve. The original intent was: if your domain logic is complex enough to warrant isolation, isolate it. The cargo cult version is: every app must have these layers regardless of complexity.

When Clean Architecture IS Right

So when should you actually use Clean Architecture? Let me give you real criteria. First: your team has more than five developers working in the same codebase. The layer boundaries create clear ownership and reduce merge conflicts. Second: your domain logic is genuinely complex — not just CRUD operations, but real business rules that need to be tested independently. Think financial calculations, complex state machines, or multi-step workflows with rollback logic. Third: you have realistic expectations of framework changes. If you are genuinely considering migrating from Room to SQLDelight, or from Retrofit to Ktor, those abstraction layers will save you weeks. If you have used Retrofit for five years and have no plans to change, the abstraction is just ceremony.

When It Is Wrong

And when is Clean Architecture the wrong choice? First: you are a solo developer or a team of two or three. The coordination overhead that layers prevent does not exist at this scale. Second: your app is primarily a thin client — it fetches data from an API and displays it. Your domain layer will be nothing but pass-through use cases and one-to-one model mappings. Third: you are in the early stages of a project and you do not yet know what your domain looks like. Prematurely imposing layer boundaries will force you to refactor those boundaries repeatedly as your understanding evolves. In these cases, a simpler architecture — ViewModel talks to Repository, Repository talks to API and database, maybe two model types instead of four — will get you further, faster, with less frustration.

The Cost of Indirection

I want to drive this point home because it is subtle but critical. Every layer of indirection has a cognitive cost. When a new developer joins your team and needs to fix a bug in the user profile screen, they have to trace through the ViewModel, to the use case, to the repository interface, find the repository implementation, go to the data source, look at the mapper, look at the entity. That is a long chain of files to follow. Compare that with: ViewModel calls repository, repository calls API. The bug is in one of three places. You can hold the entire flow in your head. Code is read far more often than it is written, and every layer you add makes reading harder. The question is never "should I add this layer?" It is "does the benefit of this layer exceed its cost in my specific situation?"

What to Do Instead

So what do I actually recommend? Start simple. ViewModel, Repository, data sources. Two model types: the API or database model, and your domain or UI model. One mapper if the shapes differ significantly. No use cases unless you have genuine business logic to encapsulate. No interfaces for classes with a single implementation. Then evolve. When you find yourself copying business logic between ViewModels, extract a use case. When you find that your UI model and your domain model have genuinely different shapes, add that layer. When your team grows and you need enforcement boundaries, add interfaces. Let your architecture grow from real needs, not from a template you copied from a blog post. This is not being lazy. This is being disciplined about complexity.

The Pragmatic Middle Ground

There is a pragmatic middle ground that I see working well in production. Keep the Repository pattern — it is genuinely useful for abstracting data sources and managing caching logic. Skip use cases by default, add them only when they contain real logic. Use a single model in your domain and presentation if they happen to be identical — you do not need separate models for the sake of purity. Use interfaces for repositories only if you have multiple implementations or you need to swap them in tests. And please, avoid mappers that just copy fields. If your API model and your domain model are the same shape, just use one class. You can always split them later if they diverge.

Closing

Here is the takeaway I want you to walk away with. Architecture is not a blueprint you apply. It is a set of trade-offs you evaluate for your specific situation. Clean Architecture is a powerful tool, but like any tool, it can be misused. A screwdriver is great for screws and terrible for nails. If your domain is complex and your team is large, Clean Architecture will save you. If your app is a thin client built by a small team, it will drown you in boilerplate. Do not let conference talks or blog posts or even this video tell you what architecture to use. Look at your app, your team, your domain, and make an honest assessment. The best architecture is the simplest one that solves your actual problems. In the next video, we are going to zoom in on layers specifically — when they help, when they are pure overhead, and how to figure out which is which for your project.