Do You Still Believe in Clean Architecture? Here's Why It's a Mistake

by Max KachApril 22nd, 2025
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Clean Architecture has become more than just an architectural style. It feels like if you’re not using Clean Architecture, you're doing something wrong. In this article, I want to take a clear and honest look at Clean Architecture. I’ll go through situations where it really helps, and where it’s just not necessary.

People Mentioned

Mention Thumbnail

Company Mentioned

Mention Thumbnail
featured image - Do You Still Believe in Clean Architecture? Here's Why It's a Mistake
Max Kach HackerNoon profile picture
0-item

In mobile development, especially on Android, Clean Architecture has become more than just an architectural style. It’s turned into a kind of cult. You see it in community discussions, articles, and conference talks. It feels like if you’re not using Clean Architecture, you’re doing something wrong. If your project doesn’t follow it, people assume it’s badly designed, hard to scale, and so on.


In this article, I want to take a clear and honest look at Clean Architecture. I’ll go through situations where it really helps, and where it’s just not necessary.


I’m not here to attack it. But I’ll show how sometimes it adds too much complexity or doesn’t make sense at all. And if you're a big fan of Clean Architecture, I hope by the end of this article you’ll think twice before pushing it everywhere.


Let’s dive in!

What is Clean Architecture?

From my experience working and talking with different developers, I’ve noticed that everyone seems to have their own idea and understanding of what Clean Architecture means. Some think that if your code is easy to read, and you use interfaces, then that’s already “clean” architecture, simply because it looks nice and makes sense.

Robert Martin made a smart move. He chose a great name: Clean Architecture. Even if you haven’t read the book or don’t know what it’s about, the name already sounds good. "Clean" means something positive. So, if you’re not using Clean Architecture, then what is your architecture? Dirty? Messy? Bad? You can’t say the same about Onion Architecture. Some people like onions, some don’t.


And for example, Hexagonal Architecture. It’s just a shape. How can you like or dislike a hexagon? But when you hear "Clean", anything that isn’t "clean" starts to feel wrong.


That’s why it’s important to agree on what Clean Architecture actually means, at least for this article. We’ll look at what Robert Martin had in mind when he wrote about it, and how it’s used in real mobile apps.

Robert Martin's Clean Architecture

In this article, we will treat Clean Architecture as the approach described by Robert Martin. Now, I’ll quickly and clearly explain what Clean Architecture means according to him.


Clean Architecture is built around layers, like circles inside each other. The rules say that all dependencies should point inward, from the outside layers to the inner ones. One of the main ideas is called Dependency Inversion. This means we try to keep business logic separate from things like the user interface, databases, and network frameworks.


Here are the main ideas of Clean Architecture:

  • The app is split into inner and outer layers.
  • At the center is the domain layer. It does not depend on anything, but other layers depend on it. The business logic inside this layer usually changes less often than frameworks, so it stays stable over time.
  • Dependency Inversion means that outer layers depend on inner layers.
  • SOLID principles are used a lot, especially Single Responsibility and Dependency Inversion.
  • Layers communicate with each other through interfaces.
  • The business logic is easy to test.

Dependency Inversion: The Key Idea

In mobile apps, the most important and unique part of Clean Architecture is Dependency Inversion. Other features can also be found in different architectural styles, but this one really stands out.


Dependency Inversion changes how different parts of the app depend on each other. Let’s compare it with the standard Layered Architecture.

Comparison With Layered Architecture

In my opinion, Layered Architecture is the easiest one to understand. In classic Layered Architecture, dependencies go from top to bottom.


In mobile apps, we usually have three main layers:

  • Presentation Layer
  • Domain Layer
  • Data Layer


The Presentation Layer depends on the Domain Layer. The Domain Layer depends on the Data Layer. This is the most common setup. In some cases, there might be fewer layers (like two) or more layers (like four or even five, although I’ve never seen more than five).

In Layered Architecture, the top layers call the ones below them. For example, a ViewModel in the Presentation Layer calls a GetUserUseCase in the Domain Layer, and that use case then calls a UserRepository in the Data Layer.

  • Today, we might store data in a file, and tomorrow, we might use a database. If we are not careful with how layers depend on each other, data-related classes can end up in the Domain Layer, where they should not be.
  • Today, we use Retrofit, but in the future, we might switch to Ktor. When this happens, DTO classes might also end up in the Domain Layer.
  • If the structure of the data changes, it can cause changes in the Domain Layer, too. This often happens because we keep mappers or converters between data models and domain models there. But really, only the data changed, not the business logic.


If we look at all these situations together, we see that the layers become too coupled. This is bad because it makes the code harder to change in the future.


Clean Architecture uses the idea of Dependency Inversion to solve this. It says that the Domain Layer should not depend on the Data Layer. Instead, the Data Layer should depend on interfaces from the Domain Layer.


Let’s go back to our example. To apply Dependency Inversion, we separate the UserRepository interface and its UserRepositoryImpl class, if we haven't done that yet. Then we move the UserRepository interface to the Domain Layer, and keep the UserRepositoryImpl class in the Data Layer.

This way, the Domain Layer does not need to know where the data is stored or how it is received. It just defines an interface, saying, "I need data in this format." Then it is the job of another layer, like the Data Layer, to provide that data.


How does this help?


  • Business logic no longer depends on how the data is received. It does not care which network or database library is used.
  • If the format of the data changes but the business logic stays the same, then only the Data Layer needs to be updated. The Domain Layer does not need any changes.
  • The code in the Domain Layer becomes less coupled to other parts and easier to test. This is very useful when the domain has important business logic that you want to test without touching other layers.

Cons of Clean Architecture in Mobile Apps

Clean Architecture with Dependency Inversion sounds like a great idea that should make any project better. But does it have any downsides? In theory, everything looks perfect, but in real projects, things can get more complex than expected.

Cross-Modular Inversion Is Not Easy

Let’s look at a common example of splitting an Android app into modules. Just to be clear, there is no one correct way to organize modules. In this case, I’ll use one of the popular methods, which is also explained in the official documentation.

Classic approach to creating a multi-module project from the official docs

Notice that :feature:home and :feature:reviews depend on :data:books and :data:reviews. In this setup, the layers are not clearly separated. The Domain Layer is either missing or mixed with the Presentation Layer inside the feature module. Let’s redraw the diagram to show the typical three-layer structure.

This is an example of Layered Architecture. Dependencies go from top to bottom. Each feature’s Domain Layer might have its own Data Layer module, or it might not. But both features still use shared modules from the Data Layer. I did not change the structure. I just showed the layers in a different way.


Let’s say the common.data modules include two repository interfaces:

package com.example.common.data

interface UserRepository {
    fun getUser(id: String): User
}

interface MenuRepository { 
    fun getMenu(): List<Menu> 
}

Each feature (feature1 and feature2) has its own UseCase that depends on these repositories. In the code below, look at the package and import lines to understand which module we are in and what we are depending on.

package com.example.feature1.domain

import com.example.common.data

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

---------------------------------------
package com.example.feature2.domain

import com.example.common.data

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

Now, let’s think about how to apply Dependency Inversion between the Data Layer and the Domain Layer. In the previous example, we just changed the direction of the arrows by moving the repository interface into the Domain Layer. This worked well for feature1, and we were able to split the Data Layer into a separate module. But now, it is not clear how to do the same with the shared common.data modules.

If we try the same trick (moving the interface), it's unclear where it should go. The common2 module would need to implement two interfaces: one from feature1 and one from feature2.


If we place UserRepository inside feature1.data, then GetMenuUseCase from feature1 will have correct access to it, because they are in the same feature.

package com.example.feature1.data

interface UserRepository { /* ... */ }

package com.example.feature1.domain

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


But in that case, the Domain Layer of feature2 would end up depending on feature1.data, just to access the UserRepository placed there.

package com.example.feature2.domain

import com.example.feature1.data // <- Feature 2 depends on Feature 1 

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

Let’s look at how we can solve this. One option is to create separate Data modules for each feature. These modules will use the common Data modules, like in the picture below. Dependency Inversion will happen inside each feature, between its own modules. In this setup, the new Data classes might be very small and just forward the calls to the shared code.

The second option is to create shared Domain modules, like in the picture below. In this case, we apply Dependency Inversion between the Data Layer and the Domain Layer in all parts of the project. But feature-specific Domain modules will still depend on the shared Domain modules.

There are other ways to do this. For example, shared Data classes can implement several interfaces. But this has some downsides. It can be hard to scale when many modules start depending on the same code. I’ll let you imagine how the code for this might look.


So, we can see that using Dependency Inversion with shared Data modules is not always simple. This is not a big problem with Clean Architecture, but it is something to think about when planning your project’s structure.

The domain is not always the most stable layer.

One of Robert Martin's key ideas was that business logic changes less often than the UI or data frameworks. Because of this, the Domain Layer should be isolated, so that changes in the outside parts of the app do not affect the core logic.


But let’s be honest. In many mobile apps, business logic changes a lot. I don’t know about you, but on every project I’ve worked on, we follow Agile. This means we often make changes, test new ideas, remove old features, and build new ones. These changes affect not only the UI but also the business logic. Every sprint brings new tasks, new scenarios, A/B tests, and feedback from analysts and designers. At the same time, the networking layer, repositories, and data models may stay the same for many months or even years.

// UseCase that changes frequently
class CheckoutUseCase(
    private val cartRepository: CartRepository,
    private val discountService: DiscountService,
    private val abTestManager: AbTestManager,
) {
    fun execute(): CheckoutResult {
        val items = cartRepository.getItems()
        val discounts = if (abTestManager.isFeatureEnabled("new_discount")) {
            discountService.calculateNew(items)
        } else {
            discountService.calculateLegacy(items)
        }
        // ... many more changes
        return CheckoutResult(items, discounts)
    }
}

How often do you switch from Realm to Room in one week? Or move from Retrofit to Ktor? Probably not very often. But you might be creating new UseCases or changing old ones in almost every sprint.


If your Domain Layer is stable, that’s great! In that case, Clean Architecture with Dependency Inversion might be a very good fit. But if you keep rewriting UseCases in every release because the logic changes all the time, maybe it’s worth asking if the Domain Layer is really that stable.


This doesn’t mean Dependency Inversion is bad. Like many things, it depends on the situation. My point is that one of the main reasons for using concentric layers in Clean Architecture, made possible by Dependency Inversion, may not be as strong in mobile development.

Layered Architecture: Not so Bad

If we look at everything we talked about so far, we can see that layered architecture is actually a good option, especially for mobile apps.

First, it is a simple and clear approach. It is easy to understand for both new and experienced developers. Second, it is a proven model that has been used for a long time. Even Android and OS X are mostly built using the idea of layers.

The main idea is that Layered Architecture does not go against writing good code. You can still use all the SOLID principles. For example, you can follow the Single Responsibility Principle (SRP), create loosely coupled components, and use interfaces for dependencies. But you don’t have to use the Dependency Inversion principle everywhere. Use it when it really helps. If you are creating interfaces only to follow the rule, without getting real value from it, and making shared modules harder to work with, then maybe it is not worth it.


Layered Architecture is still clear, flexible, and good enough for many mobile projects. It is also easy to test. You can write unit tests for UseCases and mock their dependencies.


Layered Architecture is not a bad practice. It is a stable and proven approach that might be exactly what your project needs.

Conclusion: Use Common Sense

In this article, we looked closely at Clean Architecture by Robert Martin, especially in mobile development. The main idea is to keep business logic in the center, separate it from everything else, and reduce connections between layers using Dependency Inversion.


This sounds clear and smart in theory, but when you use it in real mobile projects, some challenges appear.


First, in mobile apps, the Domain Layer often changes. A new screen, a new scenario, an A/B test, a discount, or new business rules can make an old UseCase useless, and you may need to write a new one from scratch. At the same time, repositories and network code might stay the same for a long time.


Second, shared data modules do not always work well with Dependency Inversion. Splitting dependencies between layers in this case can be difficult.


That’s why I think it is important to understand that Clean Architecture is not a one-size-fits-all solution. It is just one of many good ways to organize your code.


Yes, it works well for large, long-term projects. Yes, it helps make architecture clearer and lowers risks during refactoring. But if you are working on a small or medium project with fast development, frequent changes, and short deadlines, Clean Architecture can sometimes slow you down.

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks