← Back to list

Why I Stopped Using Clean Architecture in My Android Apps

Three layers, five packages, eight interfaces all to fetch a list of users from an API.

Himanshugaur · 2026-06-03 05:19 · 51 claps · 11.9 min read
#android-app-development #android #software-engineering #android-development #programming
Open on Medium ↗
Wiki topics: 💻 · Programming 📱 · Mobile Development 🏛️ · Architecture

Why I Stopped Using Clean Architecture in My Android Apps

Three layers, five packages, eight interfaces all to fetch a list of users from an API.

I was a Clean Architecture believer. A devoted one.

I drew the concentric circles on whiteboards. I quoted Uncle Bob in pull request reviews. I had a base project template with domain, data, and presentation layers so neatly separated that you could almost hear the SOLID principles humming in harmony.

Every repository had an interface. Every use case had a single method. Every mapper had a counterpart going the other direction. My packages were pristine. My architecture diagrams were beautiful.

My productivity was terrible.

It took me three years and half a dozen production apps to admit something uncomfortable: Clean Architecture, as commonly practiced in Android, was costing me more than it was giving me. And the day I let go of it was the day I started shipping better software, faster.

This is the story of what changed and why.

What Clean Architecture Promises

Before I explain why I walked away, let me acknowledge what drew me in.

Clean Architecture, as described by Robert C. Martin, promises a system where business logic is completely independent of frameworks, databases, and UI. Dependencies point inward. The domain layer knows nothing about the outside world. Everything is testable. Everything is replaceable.

On paper, it’s beautiful. In Android blog posts and conference talks, it sounds like the cure for every messy codebase you’ve ever inherited.

The typical Android interpretation looks like this:

Presentation Layer Activities, Fragments, Composables, ViewModels Domain Layer Use cases, repository interfaces, domain models Data Layer Repository implementations, API services, local databases, data models, mappers

Each layer has its own models. Mappers convert between them. Use cases sit between ViewModels and repositories, encapsulating “business logic.” Repository interfaces live in the domain layer. Implementations live in the data layer.

It’s organized. It’s principled. And for most Android apps, it’s a massive over-engineering trap.

The Moment I Started Questioning Everything

The turning point came on a Tuesday afternoon. I was building a feature nothing exotic. Fetch a list of articles from an API, cache them locally, display them in a list.

I sat down to write the code. Here’s what Clean Architecture told me I needed:

data/
├── remote/
│   ├── ArticleApi.kt
│   ├── ArticleDto.kt
│   └── ArticleDtoMapper.kt
├── local/
│   ├── ArticleDao.kt
│   ├── ArticleEntity.kt
│   └── ArticleEntityMapper.kt
└── repository/
    └── ArticleRepositoryImpl.kt

domain/
├── model/
│   └── Article.kt
├── repository/
│   └── ArticleRepository.kt
└── usecase/
    └── GetArticlesUseCase.kt

presentation/
├── ArticleListViewModel.kt
├── ArticleListScreen.kt
└── ArticleUiModel.kt

Thirteen files. For a list that fetches from a network and shows on screen.

I had three different representations of the same articleArticleDto, ArticleEntity, and Article with two mappers to shuttle data between them. I had a use case class whose entire body was a single line delegating to a repository. I had a repository interface with exactly one implementation that would never, ever be swapped.

And somewhere between writing ArticleDtoMapper and ArticleEntityMapper, I stopped and asked myself a question I'd been avoiding for years.

Who is this for?

Not for the user. They see a list of articles. They don’t care how many layers it passed through.

Not for testability. I could test a repository directly. I didn’t need a use case wrapper to make that happen.

Not for future flexibility. In three years of maintaining production apps, I had never once swapped a repository implementation, replaced a data source through a domain interface, or benefited from having a “framework-independent” domain layer in a project that is, by definition, an Android app.

The architecture was for the architecture. And I was done.

The Five Lies Clean Architecture Tells Android Developers

Lie #1: “You Might Need to Swap Your Data Source”

This is the foundational argument. Keep your domain layer ignorant of the data layer so you can switch from REST to GraphQL, from Room to SQLDelight, from Firebase to your own backend without touching business logic.

In theory, compelling. In practice, I’ve worked on over a dozen production Android apps across different companies. The number of times someone swapped a data source through a clean domain interface: zero.

When teams switch from REST to GraphQL, they rewrite the data layer entirely. When they migrate databases, the schema changes ripple through every layer regardless. When they move off Firebase, it’s a multi-sprint project that touches everything.

The interface between domain and data doesn’t protect you from these changes. It just gives you an extra file to update when they happen.

You’re paying a daily complexity tax for an insurance policy that never pays out.

Lie #2: “Use Cases Encapsulate Business Logic”

In theory, use cases are where your business rules live. In practice, in most Android apps, they look like this:

class GetArticlesUseCase(
    private val repository: ArticleRepository
) {
    suspend operator fun invoke(): List<Article> {
        return repository.getArticles()
    }
}

One line. Delegating to a repository. Adding nothing.

Defenders say “but it could have logic later.” Sure. And my kitchen could need a commercial ventilation system later. I’m not installing one now.

The honest truth is that most Android apps are not business-logic-heavy. They fetch data from an API. They display it. They let the user interact with it. They send changes back. The “business logic” if you can call it that is validation, formatting, and maybe some filtering. That logic lives comfortably in a ViewModel or a repository. It doesn’t need its own layer.

When you do have genuine, complex business logic pricing calculations, scheduling algorithms, rule engines then yes, isolating it makes sense. But that’s maybe 10% of the screens in a typical app. Clean Architecture asks you to build the infrastructure for all of them.

Lie #3: “Each Layer Needs Its Own Models”

This one costs the most time for the least return.

Clean Architecture purists insist on separate models per layer. Your API gives you ArticleDto. Your database uses ArticleEntity. Your domain has Article. Your UI might have ArticleUiModel.

So you write mappers. Lots of mappers. And every time you add a field say, a thumbnailUrlyou add it to all four models and update all the mappers. One field change becomes four file changes.

The justification is decoupling. Your domain model shouldn’t “know” about JSON annotations or database column names.

But Kotlin data classes with libraries like Moshi, Kotlinx Serialization, or Room already handle this cleanly with annotations. Your Article class can be a Kotlin data class that works with your serializer, your database, and your UI without needing three doppelgangers.

@Serializable
@Entity(tableName = "articles")
data class Article(
    @PrimaryKey val id: String,
    val title: String,
    val content: String,
    @SerialName("thumbnail_url")
    @ColumnInfo(name = "thumbnail_url")
    val thumbnailUrl: String,
    @SerialName("published_at")
    @ColumnInfo(name = "published_at")
    val publishedAt: Long
)

One model. Works everywhere. Is it “pure”? No. Does it violate separation of concerns? Technically. Will it cause a real problem in a real app? Almost never.

The purity costs you time every single day. The “impurity” costs you nothing until a scenario arises that, in my experience, rarely does.

Lie #4: “It Makes Testing Easier”

Clean Architecture claims that isolating business logic in a framework-free domain layer makes testing trivial.

Here’s what actually makes testing easy: constructor injection and interfaces at boundaries.

class ArticleViewModel(
    private val repository: ArticleRepository
) : ViewModel() {
    // testable with a fake repository, no use case needed
}

You can test this ViewModel by passing a fake repository. You don’t need a use case layer in between. You don’t need the domain layer to be “framework-free.” You need your classes to accept their dependencies as parameters.

The use case layer doesn’t add testability. It adds another thing to test and that “thing” is usually one line of delegation that doesn’t need a test.

I’ve seen test suites bloated with tests like GetArticlesUseCaseTest that verify... the use case calls the repository. That's not testing behavior. That's testing plumbing.

Lie #5: “It’s What Professional Android Teams Use”

There’s an unspoken pressure in the Android community: if you’re not using Clean Architecture, you’re not “serious.” It’s the architecture of Real Apps built by Real Engineers.

But talk privately to senior Android developers at companies shipping apps to millions of users, and you hear a different story. Many have simplified away from strict Clean Architecture. Some never adopted it fully. Others use a pragmatic subset a data and UI layer, maybe a shared model, no use cases unless warranted.

The most successful codebases I’ve seen aren’t the most layered. They’re the most readable.

What I Do Instead

I didn’t replace Clean Architecture with chaos. I replaced it with something simpler that solves the same problems without the overhead.

Two Layers, Not Three

My apps have two layers: UI and Data.

ui/
├── articles/
│   ├── ArticleListScreen.kt
│   └── ArticleListViewModel.kt
└── settings/
    ├── SettingsScreen.kt
    └── SettingsViewModel.kt

data/
├── ArticleRepository.kt
├── ArticleApi.kt
└── ArticleDao.kt

The UI layer contains screens and ViewModels. The data layer contains repositories, API services, and local storage.

There’s no domain layer. ViewModels talk directly to repositories. Repositories are concrete classes, not interfaces with a single implementation.

Shared Models

I use one model where possible. If the API response shape and the database shape and the UI shape are all close enough, it’s one data class. If the API response is genuinely different nested JSON, extra fields, different naming I’ll use a DTO and map it to the shared model. But I don’t create models preemptively for layers that don’t need their own.

// One model, used in the repository, ViewModel, and UI
data class Article(
    val id: String,
    val title: String,
    val content: String,
    val thumbnailUrl: String,
    val publishedAt: Instant
)

// The API response is different enough to warrant its own model
@Serializable
data class ArticleResponse(
    val id: String,
    val title: String,
    val body: String,
    @SerialName("thumb") val thumbnailUrl: String,
    @SerialName("date") val publishedAt: String
) {
    fun toArticle() = Article(
        id = id,
        title = title,
        content = body,
        thumbnailUrl = thumbnailUrl,
        publishedAt = Instant.parse(publishedAt)
    )
}

One mapper. Created only because the API shape genuinely differs. Not because a diagram told me I need one.

Use Cases Only When They Earn Their Place

I don’t ban use cases. I just don’t create them by default.

When a piece of logic involves coordinating multiple repositories, applying non-trivial business rules, or is reused across multiple ViewModels, I extract it into its own class.

// This earns its existence — real logic, multiple dependencies
class SyncArticlesWithBookmarks(
    private val articleRepo: ArticleRepository,
    private val bookmarkRepo: BookmarkRepository
) {
    suspend operator fun invoke(): List<Article> {
        val articles = articleRepo.getAll()
        val bookmarkedIds = bookmarkRepo.getBookmarkedIds()
        return articles.map { article ->
            article.copy(isBookmarked = article.id in bookmarkedIds)
        }
    }
}

But I don’t create GetArticlesUseCase just because a use case layer "should" exist. If the ViewModel can call the repository directly and the logic is straightforward, it does.

Concrete Classes Over Premature Interfaces

If there’s one implementation, there’s no interface.

// Just the class. No ArticleRepository interface sitting in a domain package.
class ArticleRepository(
    private val api: ArticleApi,
    private val dao: ArticleDao
) {
    suspend fun getArticles(): List<Article> {
        return try {
            val remote = api.fetchArticles().map { it.toArticle() }
            dao.insertAll(remote)
            remote
        } catch (e: IOException) {
            dao.getAll()
        }
    }
}

“But how do you test the ViewModel without a fake repository?”

I create a fake when I need one. The fake implements the same class or, if I genuinely need polymorphism for testing, I extract an interface at that point. Not before. Not preemptively.

Extracting an interface from a concrete class in Kotlin takes 30 seconds with any modern IDE. There’s no reason to create one “just in case.”

The Objections

I’ve had these conversations enough times to know the pushback by heart. Let me address the big ones.

“This won’t scale”

Define scale. If you mean a 200-screen app with 30 developers, you’re right you’ll need more structure. But you probably need it differently than Clean Architecture prescribes anyway. You’ll need modularization by feature, not by layer. You’ll need API contracts between teams, not use case classes.

If you mean a 15–50 screen app with a team of 2–8 developers which describes the vast majority of Android projects a two-layer architecture scales beautifully. I’ve maintained apps in this range for years with this approach, and the codebase stays navigable.

“Uncle Bob says…”

Uncle Bob designed Clean Architecture for enterprise systems with interchangeable components swap your database, swap your UI framework, swap your delivery mechanism. That makes sense when your system might run as a web app today and a CLI tomorrow.

Your Android app will always be an Android app. It will always use the Android framework. The innermost circle of “framework independence” provides no value when the framework is a permanent fixture.

Respecting the principles behind Clean Architecture dependency inversion, separation of concerns, testability doesn’t require following its specific structural prescription.

“You’ll regret it when requirements change”

I’ve been doing this for years. The regret hasn’t arrived.

What has arrived, many times, is the regret of over-engineering. The regret of spending a day adding a feature that should have taken an hour because I had to touch nine files across three layers instead of three files in two layers.

When requirements change dramatically, every architecture gets rewritten. When they change incrementally, a simpler architecture adapts faster because there’s less ceremony to update.

“It’s the industry standard”

So was XML layouts. So was AsyncTask. So was MVP with contracts. The Android ecosystem reinvents its “standards” every few years. The constant isn’t any specific pattern it’s the willingness to use what works now, not what worked for the last era’s problems.

When You Should Use Clean Architecture

I don’t believe in absolutes, and I won’t pretend this one is.

Use Clean Architecture or something close to it when:

  • Your domain logic is genuinely complex. Financial apps, healthcare apps, logistics apps when the business rules are the hard part and the Android framework is just the delivery mechanism.
  • Multiple platforms share the same business logic. If you’re using KMP to share a domain layer between Android, iOS, and desktop, isolating it makes real sense.
  • Your team is large enough that structural enforcement matters. When 20+ developers need guardrails to prevent the codebase from becoming a dependency spaghetti, the overhead of Clean Architecture is an investment in coordination.
  • Regulatory or compliance needs demand traceability. When auditors need to see exactly where business rules live and how data flows through the system.

These are real scenarios with real benefits. If they describe your project, don’t let this article talk you out of what works.

The Uncomfortable Truth

The real reason Clean Architecture is so popular in Android isn’t because it solves problems most apps have. It’s because it looks impressive.

A project with domain, data, and presentation packages looks professional. It looks like the developer knows what they're doing. It looks like the kind of code that passes architecture reviews and gets approving nods in technical interviews.

And that social pressure the fear of looking like you don’t know “real” architecture keeps developers adding layers they don’t need, writing use cases that do nothing, and creating interfaces that will never have a second implementation.

I know because I was that developer. For years.

The hardest part of dropping Clean Architecture wasn’t the technical adjustment. It was giving myself permission to write less code. It felt wrong to have a ViewModel call a repository directly. It felt wrong to not have a domain layer. It felt like cutting corners.

It wasn’t. It was cutting waste.

The Takeaway

Architecture exists to solve problems. When the architecture creates more problems than it solves more files to maintain, more mappers to update, more indirection to navigate, more abstractions to explain it’s no longer serving you.

Clean Architecture is a brilliant idea designed for a specific class of problems. Most Android apps don’t have those problems. Most Android apps need a clear data layer, a clear UI layer, sensible separation within each, and the discipline to extract complexity when it appears not before.

Write the code your app needs today. Not the code that a theoretical future version of your app might hypothetically benefit from. Not the code that looks most impressive on a whiteboard.

The cleanest architecture is the one your team can read, understand, and modify without fighting the structure. Sometimes that’s three layers and use cases. More often than we admit, it’s two layers and a straightforward repository.

Simplicity isn’t the absence of architecture. It’s architecture that knows when to stop.

If you’ve been feeling the weight of too many layers in your own projects, permit yourself to simplify. And if you’ve found a project where Clean Architecture is genuinely earning its keep, I’d love to hear about it the nuance matters more than the dogma.

👋 Let’s Connect

If you enjoy content about Android development, Jetpack Compose, Kotlin, software architecture, and engineering best practices, I’d love to stay connected.

📺 **YouTube** In-depth Android tutorials, real-world projects, architecture discussions, and practical development tips.

💼 **LinkedIn** Professional updates, technical insights, articles, and lessons from my software engineering journey.

✍️ Medium More deep dives into Android development, clean architecture, testing, performance optimization, and modern engineering practices.

📚 Want to Go Deeper?

If you’re looking for structured, comprehensive learning resources, check out my Android development books, where I cover concepts in much greater depth than a typical article.

Support My Work

Creating free technical content, books, tutorials, and open educational resources takes considerable time and effort. If this article helped you learn something valuable, you can support my work by:

• Purchasing one of my books • Sharing my articles with others • Buying me a coffee

Every bit of support helps me continue creating high-quality content for the Android community.

Click here to support

Clean Architecture for Absolute Beginners— Learn how to write maintainable, scalable, and testable Android applications using Clean Architecture with real-world examples.

👉 Explore all my books: Book Link


메타데이터
post_id
130d2f89a892
slug
why-i-stopped-using-clean-architecture-in-my-android-apps-130d2f89a892
url
https://medium.com/@himanshugaur684/why-i-stopped-using-clean-architecture-in-my-android-apps-130d2f89a892
canonical_url
https://medium.com/@himanshugaur684/why-i-stopped-using-clean-architecture-in-my-android-apps-130d2f89a892
author_url
https://medium.com/@himanshugaur684
status
ok
fetched_at
2026-06-14 11:28:49