Structural and navigation anti-patterns in multi-module and modularized applications: The case against “Android Clean Architecture” and the “domain” module

Gabor Varadi
ProAndroidDev
Published in
10 min readDec 25, 2020

--

Top-level “domain” module is a code smell.

Merry Christmas to all!

The subject of this article is one that has been on my mind for a very long time. After all, “Clean Architecture” is often seen as the end goal, the hallmark of the finest of Android application code structure — it is the best of the very best. The name itself, “Clean Architecture”, signals that it’s a good thing ~ after all, who wants to work with “messy” code in comparison?

Of course, the title of this article shows that this isn’t the conclusion we’ll come to. The following tweet is closer to our final evaluation — let us analyze why it might be accurate.

Clean Architecture + Android = a burning boat on top of a mountain

The history of “Android Clean Architecture”

The origins

To understand “Clean Architecture” as done on Android, we must trace it back to its roots, and where/when it was made popular in the first place.

As far as I’m aware, it dates back to 2014, the original proposition in “Fernando Cejas: Architecting Android… the clean way?”. It was a highly influential article, as it revolutionized the structure of Android apps.

Code that is actually separated across layers (back when Fragments were untrustworthy, and all code was in OS-level components), rather than just throwing everything into 2000+ line Activities? A pioneer of its time.

One interesting decision however in the representative sample code was that in order to signify the strict separation of layers, these layers were each separated from one another using top-level Gradle modules, each labeled data, domain, and presentation.

As we will see, this is the downfall of the sample, which has been perpetuated ad nauseam since.

Improvements: reactivity and dependency injection

In July 2015, a new version of the Clean Architecture sample emerged, this time heavily built on top of RxJava and Dagger 2— outlined by the article, “Fernando Cejas: Architecting Android… the evolution?”.

There’s an interesting note made in the article:

As you can see, my approach looks like packages organized by layer: I might have gotten wrong here (and group everything under ‘users’ for example) but […] this sample is for learning purpose and what I wanted to expose, were the main concepts of the clean architecture approach. DO AS I SAY, NOT AS I DO :).

So within the fine print, it was, in fact, explicitly stated that using a data/ domain /presentation top-level layering in terms of modules is less favorable than a feature-based one.

This statement is in line with Martin Fowler’s recommendation outlined in PresentationDomainDataLayering:

it’s usually not best to use presentation-domain-data as the higher level of modules. Often frameworks encourage you to have something like view-model-data as the top level namespaces; that’s OK for smaller systems, but once any of these layers gets too big you should split your top level into domain oriented modules which are internally layered.

Please make note of the term, “domain oriented modules which are internally layered”, as this is quite important in the future.

Follow-up: functional error handling and additional notes on modularization

Four years later, in 2019, a new version of the sample was created, this time using Kotlin — and there is a new article to accompany it: “Fernando Cejas: Architecting Android… Reloaded”, showing a way to create abstract use-cases with functional types that represent errors or success, and replacing MVP with MVVM using ViewModel and LiveData.

In general, for an approach that scales best with the lowest complexity in regards to use-cases, I’d advise to check out Vasiliy Zukanov’s article on How to Write UseCases (Interactors) in Kotlin.

The article outlines how to use sealed classes that represent UseCase results, while ensuring that we don’t fall into the trap of trying to “abstract out use-cases” as in general, that just adds more complexity than hides or removes it.

Inheritance is NOT the way to go for sharing this sort of behavior, especially not across separate domain-level concerns. And oftentimes, errors are just as “real” and just as important end results of a use-case as “success”. Success is not more important than errors.

But back to the original cited article, it outlines quite clearly, that top-level data / domain / presentation modules was an architectural mistake:

A recurring question in discussions was: Why [did I use android modules for representing each layer]? The answer is simple… Wrong technical decision. […]

[…] the sample was still a MONOLITH and it would bring problems when scaling up: when modifying or adding a new functionality, we had to touch every single module/layer (strong dependencies/coupling between modules).

In the new version of the sample, the two features, login (authentication) and movies, are significantly simplified in structure — in fact, movies are all found within the package, not “leaking” to “other parts” of the app.

Android Clean Architecture as layer-based compilation modules: an anti-pattern

In short, using layers as top-level modules, rather than implementation details of domain-oriented “feature” modules, creates a distributed monolith where to add a new feature, ALL modules must ALWAYS be touched — showing that despite best intentions, the modules are all tightly coupled, and each features’ own responsibilities are spread across these 3 modules, preventing scalability and reusability.

However, this does not mean we should forego layers altogether. We can use layers as implementation detail, to structure the code within a given feature ~ but rather than modules, we can use packages to convey the same structure. After all, why split up layers as modules, if we’re not swapping them out with different implementations, nor re-using them elsewhere?

Navigation Anti-Patterns in Multi-Module Applications

Notes on the need for modularization

Let me echo my previous statement:

why split [them] up as modules, if we’re not swapping them out with different implementations, nor re-using them elsewhere?

Modularization is quite popular, and when applied incorrectly, it can be quite deadly for a codebase.

How do we know if it’s incorrect? Well, a better question to ask is, “how do we know if it’s correct?”.

What ARE we trying to accomplish here? Are we building artificial boundaries between parts of our own code because we don’t trust that developers will follow the “correct” structure? (Does the top-level structure matter if it’s all just the implementation details of a single feature? Would imposing the existence of a “data” module even make sense for a module that has no data persistence or network requests of their own?)

Are we merely trying to “decrease build times and enable parallelization”? Are we slicing off tooling complexity so that we can prevent kapt from making our code compile for 2x the time?

What are the end goals? Surely establishing boundaries along the wrong seams (like, layers 😏) would introduce additional complexity, rather than simplify development.

Global shared module dependencies are a design smell

There are cases for using global shared modules, if we know that reusability is not a goal. However, even then, it is a smell. For example, one might extract an app’s string resources into its own module :localization, or drawable resources into its own module :resources, so that there aren’t duplicates — simplifying copytext updates and reducing APK size. Still, these should be applied on a case-by-case basis.

Not all shared dependencies are problematic: if modules are designed to be reusable, and they make no assumptions about what other modules may exist that it doesn’t actually know about, then they are the units with which code can be shared. Think of your favorite libraries, like Retrofit — they are effectively library modules written by others, and they can be used in many projects, freely.

However, if we were to create feature modules that, as modules, are designed to be self-contained and independent, that raises questions.

How do I navigate from one module to another, if I don’t see it — only the aggregator module :app does?

Cross-Module Navigation Anti-Pattern #1: Reflection

One particular approach could be to use the FQN of the target — as we don’t see them, but we might assume they exist. Why not?

Well, because we don’t know if it’s there, of course, we certainly don’t see it at a source code level: that’s why we’d need to use reflection in the first place.

Objectively, what we are doing here is bypassing the module boundaries that we had previously established, through raw force. Why do I know if that Activity or Fragment exists? What if the module is changed and refactored? Compile-time safety is thrown into the bin.

There are various variations of this pattern to counteract the clear violation of the module boundaries, masking it and hiding it, sweeping it under the rug with some additional tricks.

Cross-Module Navigation Anti-Pattern #2: Shared global module with Activity FQNs (and reflection for Fragments)

There are multiple issues with this approach.

First, one might think of using multiple Activities in an Android application, where a single-Activity approach would have been equally sufficient (or better). After all, MapFragment exposed by either Google Maps or HMS is a Fragment with no "MapActivity” to host it. So why would we need multiple Activities, just because the app has multiple modules?

Second, now every module is aware of the existence of every other module. Every screen is aware of the existence of every other screen. This in itself is generally not an issue for smaller apps (in fact, it’s the basis for navigation in many single-module apps), but is this really something we can afford, in case we were to want to re-use modules, rather than create a distributed monolith?

Yes, same rules apply even if you were to generate the FQNs from the merged AndroidManifest.xml.

Cross-Module Navigation Anti-Pattern #3: Shared global module with key objects and Dagger map-multibinding

Whenever map-multibinding is used, it’s a way to replace reflection (string-based, runtime errors) with a map lookup (class-based, runtime errors). In this case, :app aggregates all exposed @IntoMap bindings into a single map, and it can be looked up with a class key.

This approach is actually a significant improvement over #2, even if in principle, it is the same. We would still require knowledge of the shared keys, and thus be aware of every screen in the app — cross-module communication through a shared module would still result in coupling to a global shared dependency.

Still, it is possible to expose a “Fragment factory” (not a FragmentFactory!) bound to a class key, that would be looked up based on said key, and it would instantiate the fragment when asked. As long as the bindings are provided in a consistent manner, it would work. It just ends up to be non-reusable.

The theoretical correct way: mimicking exactly what you need to do in order to share Dagger subcomponents to child modules (as in: use interfaces exposed by the child modules)

Google has actually solved the multi-module navigation problem with a clear and conceptually sound way a long time ago. We’re just not aware of it. You see, the solution was outlined for how to share Dagger subcomponents to child modules, and not specifically in the context of Navigation.

Our child module can expose what navigation action it needs to have executed by someone who is capable of doing so:

This can be implemented by some form of navigator:

And if we used the getSystemService trick directly, we’d be able to look up LoginNavigationActionHandler::class.java.name from context.getSystemService(), but that’s a trick that’s fallen out of favor. So we might want to get access to the Navigator instance with a provider interface, just like they did with subcomponents.

Which could be implemented on an Application:

And afterwards, the child module can look up the navigation action handler through the shared application class:

And in theory, this could work! As long as we maintain the correct NavController instance within this Navigator class, anyway.

This way, we did not need reflection, we did not need map multi-binding, and most notably, we did not need a shared global module. What we did need is a reference to something that implements handling the child’s exposed events.

Using either Dagger or Hilt, this last step could easily be hidden within app as @Binds abstract fun loginNavigationActions(navigator: Navigator): LoginNavigationActions. That way, we can ditch the LoginNavigationActionsProvider, as Dagger’s generated component would serve this purpose (which, in the case of Hilt, is hidden within the entry point accessed via the child class generated over classes marked with @AndroidEntryPoint).

The importance of this approach is that this way, if one were to use Jetpack Navigation, one could <include the graphs that belong to the features into a single NavGraph in :app, and still handle multi-module navigation without deeplinks or reflection.

A minimal example is available here.

Does anyone actually use child events exposed to the parent for navigation actions?

To a point (and if my understanding is correct), badoo/RIBs does internally in terms of its design: although it’s smarter than the outlined “theoretical” approach.

Rather than using a global navigator, they would handle child actions through “bubbling” it up to “a parent that can handle it”, rather than assume that the application can provide a singular singleton that is capable of it — thus making it possible to create subscopes, rather than rely on a global scoped component.

This approach however is advanced in regards that it would require a child backstack per each scope node with a hierarchical structure, rather than a global one. You’d be more likely to be able to build this on top of Compose-Router than over Jetpack Navigation’s NavHostFragment.

Conclusion

Hopefully this article has shed some light on the questions of:

  • how layer-based top-level Gradle module structure is an anti-pattern that should be generally avoided, and if modularization is indeed needed, then replaced with feature-based modularization
  • how this would affect navigation within an application where feature modules don’t see each other, WITHOUT having to bypass the established module boundaries with various hacks (reflection, shared FQNs, map multi-binding, or deep-linking for regular navigation), but instead, just rely on established approaches using interfaces and object sharing.

It can be tricky to navigate what is a best practice and what is an anti-pattern, as even with the best intentions, we can follow “best practices” in an “incorrect way”, and end up recreating established anti-patterns — in this case, data/domain/presentation top-level layering.

Alternately, we might try to separate concepts that belong together, without introducing the necessary indirection to retain these established boundaries, and end up demolishing them with various hacky ways (reflection, FQNs).

. . .

Ultimately, a domain module is ALWAYS shady. After all, is there EVER a case when domain is part of the Ubiquitous Language (following DDD terminology)? If a module is meant to be “domain-oriented”, it should represent a domain-level concept that is relevant to the application: domain is not part of the domain.

The modules in the best case scenario expose a self-contained unit, no inter-dependency with other modules (especially not shared ones) — and for maximum reusability and composability, it’s best if these feature modules are created across the seams of domain-level concepts: the bounded contexts themselves.

You can check out the discussion thread on Reddit here for /r/android_devs, and here for /r/androiddev.

--

--

Android dev. Zhuinden, or EpicPandaForce @ SO. Extension function fan #Kotlin, dislikes multiple Activities/Fragment backstack.