Good architecture is a key to build a modular, scalable, maintainable and testable application. Uncle Bob Clean Architecture (CA) is a core architecture for many application nowadays, mainly because it allows to separate business logic and scales well.

It is advised to be at least a bit familiar with Uncle Bob Clean Architecture before reading this article further.

Brief into to Clean Architecture

Without going into too many details about CA we will define classic 3-layer architecture (we could have more layers). Each layer has a distinct set of responsibilities:

Presentation layer - presents data to a screen and handle user interactions

- presents data to a screen and handle user interactions Domain layer - contains business logic

- contains business logic Data layer - manages application data eg. retrieve data from the network, manage data cache

This articles focuses on different aproaches of applying CA. If want to see contents of each layer check my Android-Showcase project.

The core aspect of CA is proper layer separation (dependency rule) where the domain layer is independent of any other layers:

This means that inside the domain layer we should not access any classes defined in other (outer) layers. By preserving this rule (boundary) we gain confidence that any change applied to data or presentation layers would not affect the domain layer ( this is the core idea behind CA) eg.

If we update our data layer by migrating database engine from SQLite to Realm domain and presentation layers will not be affected

and presentation layers will not be affected If we change a way we display the same data in the presentation layer the domain and data layers will not be affected.

Ideally, the domain layer should be independent of libraries and frameworks. It is fine to have Kotlin Standard Library and some dependency injection library dependency, but we should always try and avoid other libraries and frameworks in the domain layer (especially the Android framework).

Dependency injection setup should be implemeented per clean architecture layer (avoid having one hudge setup in the main app module)

The road from theory to practical application

Clean Architecture is a quite abstract concept, so you probably wonder how can we implement this concept in our (Android) application? There are actually multiple ways to do this, so let’s take a look at a few of them and discuss the potential pros and cons of each approach.

Approach 1 — CA layers in a single module

The simplest approach would be to have 3 packages (presentation, domain, data) inside the app module.

This approach works, but it has a few serious flaws. First of all, there is no mechanism of layer dependency verification, meaning that we can access (usually by accident) classes from the presentation or data layers inside the domain layer breaking the core assumption of CA.

Another issue is that this solution does not scale well. As our app grown whole code is stored inside a single module. There is no division by feature and it is easy to end up coupling code (we will get back to this later).

Approach 2— One CA layer per module

To gain more confidence and enforce layer separation we could create separate modules (Gradle subprojects) for each CA layer and define dependencies between them.

This solution looks similar to the first one, so let’s look at Android Studio project view to see the difference:

Projects like android-clean-architecture-boilerplate and Android-CleanArchitecture utilize this approach.

In this configuration presentation module usually substitute app module, however it still servers as the main application module.

By separating layer into different modules we gain the layer separation confidence and a bit of scalability, but the approach still has scalability problems — while implementing or removing features we would have to jump across multiple modules. Code may be accidentally coupled between features and this adds hidden cross-feature dependencies that may be hard to track and maintain.

Over time each of the layers would contain more and more code leading to more confusion and increase application compilation time. To solve this issue (and potentially splitting work across team-members or multiple even multiple teams) many projects take advantage of feature modules.

When working with features ideally, we should be able to delete most of the feature related code by deleting single package.

We have modules, but we have to find a better way of utilizing them — separate things by feature, not by the CA layer.

Approach 3— CA layers inside the feature module

To solve the above problems we can take one step back and move all CA layers inside the (feature) module, but this time each feature will contain its own set of CA layers:

This time app module wires everything together.

Usually package structure of the app module is a bit different than feature modules, because it mostly contains “fundamental app configuration” (dependency injection, application class, retrofit configurations, etc.) and code that wire multiple module together (eg. some internal event bus).

Looking at Android Studio project view we will see something like this:

With this approach, we gained proper code separation from a feature perspective and more solid cross-feature boundaries. It is much easier to define cross-feature dependencies and certain features may depend on 3rd party libraries that are not needed for other features. On top of that Gradle adds some caching where only some of thee modules are may be recompiled instead of the whole project and using feature modules you can take advantage of Android dynamic delivery. Now we can also have proper feature ownership per team (eg. the team X can work on code stored within a single feature module) and unit tests separation(unit tests for a feature are contained within feature module).

While a feature module approach has more benefits it still has the initial issue that each feature can break the dependency rule. To solve this problem we can implement a custom lint check rule or ArchUnit test that would check all the imports (layer dependencies).

We can check if imports of each class defined in the domain layer and flag and error if there are imortas that point to other layers.

Personally I think that approach 3 is suitable for most of the projects (including my personal showcase project), but let’s briefly look at how we could scale it even further.

We should also consider having additional shared modules eg. feature_base module containing common classes (BaseActivity, BaseFragment etc.) or modules that contain some common resources/code to avoid duplication (thx Stoycho Andreev for pointing this in the comment).

Approach 4— CA layers per feature modules

We could have 3 modules for each feature, each one representing one of the layers:

This approach is similar to approach 2, however this time, feature module layers are top modules of our architecture (not just layer modules). Android Studio would present these modules like this:

There are multiple problems with this approach. First of all, it’s quite easy to end up with dependency hell as the number of modules goes quite fast (especially when the domain layer of one module depends on the domain layer of another module). Another issue is that Gradle slows down a bit with each new module.

Compare approaches

Below table presents a brief summary of all the approaches:

Summary

As we can see there are multiple ways of defining the CA layer within the Android project. Personally I usually prefer to go with approach 3, because it is sufficient for most projects and scales quite well.

It’s tempting to start with approach 1 and overtime slowly migrate project into approach 2, approach 3 however, migration is not always that straight forward (due to hidden dependencies). It’s much more cost (dev time) effective to start a project with properly separated features.

👉 Follow me on Twitter and check my other articles.