Blog Infos


Pic by Tania MT with ❤


The problem

We often face a common scenario during modularization — a single module needs to depend on many other features and compose them together. Imagine application startup initializing different parts of the app — in this situation the startup code has to access many features.

The typical approach is to make our feature dependent on all the relevant modules to be able to reference the code. This might be okay if we depend on very few modules, however it causes several issues once the dependencies grow.

  • Coupling: One feature depends on many others.
  • Complexity: Aggregation of dependencies in one module.
  • Modules graph: Once we have more of these aggregating features, the module graph starts to become much more complex and we can end up with a web of dependencies that’s hard to manage (why it matters).


The problem: the height of the graph grows and modules are tightly coupled.


Use cases for plugins

There are many situations in which you need many modules to contribute towards common logic, where plugins offer a solution. Some examples:

  • App startup: Many modules need some form of initialization and need to hook into the Application.onCreate method or a different point in the lifecycle.
  • Login or Logout: Different parts of the app need to be set up or cleaned up when the user logs in or out.
  • Handling Push: Typically one module is the entry point for receiving push messages, however multiple modules may wish to receive different types of messages.
  • Handling deep links: Similarly as push, one module serves as an entry point whilst many modules need to handle different deep links.
  • Feature Toggles: A module might want to declare different toggles, however the app might have a single toggle endpoint for all modules.
  • HTTP headers: Different modules want to setup common headers for the application.

The general solution involves a collection of plugins which are composed across modules, collected manually or via dependency injection such as Dagger. We will use a login plugin using Dagger as an example, however the same concept can be implemented via other approaches of passing dependencies.

Plugin pattern— login example.

  1. A feature module defines a public plugin interface and expects to receive a collection of instances, implementing the interface.
    Let’s imagine a :login-api module offering an interface which will be used across many modules.
interface LoginPlugin {
  fun onLogin(user: User)
  fun onLogout()

2. The :login module consumes the plugins as a collection.

class LoginLogic @Inject constructor(
  val loginPlugins: @JvmSuppressWildcards Set<LoginPlugin>
) {
  fun onLogin(user: User) {
    loginPlugins.forEach { plugin -> plugin.onLogin(user) }

  fun onLogout() {
    loginPlugins.forEach { plugin -> plugin.onLogout() }

3. Other modules depend on :login-api and implement their own plugins. For example the :user-theme module changes the theme based on user settings.

class UserThemePlugin @Inject constructor() : LoginPlugin {
    override fun onLogin(user: User) {

    override fun onLogout() {



Modularization – flatten your graph and get the real benefits

The story of improving modularization in multiple projects with 15+ contributing teams, hundreds of modules with complex module structure and how making order in the module graph brings the benefits from the modularization to your…
Watch Video

Modularization - flatten your graph and get the real benefits

Josef Raska
Android Engineer

Modularization - flatten your graph and get the real benefits

Josef Raska
Android Engineer

Modularization - flatten your graph and get the real benefits

Josef Raska
Android Engineer


No results found.

4. The last piece is connecting things together, which could be done using Dagger Multibingings. The :user-theme module uses the @IntoSet annotation to contribute to the Set<LoginPlugin>. All the modules with bindings are then composed within the :app module.

fun bindUserThemePlugin(plugin: UserThemePlugin): LoginPlugin

The Set<LoginPlugin> will now contain the UserThemePlugin instance and the LoginLogic will start calling the relevant methods — UserThemePlugin “plugged in” the login.

Why is this powerful?
  1. The :login module is completely decoupled from the rest of the application and doesn’t know anything about theming.
  2. We can add or remove logic based on which modules we include. This becomes useful when you have multiple apps, want to have apps that only include part of the codebase for fast development or for instant apps.
  3. We can provide different plugins for our tests, allowing us to verify logic.
  4. We can achieve completely isolated features without modification out of the module — only plugins.

This concept is definitely not new and we can find many existing examples.


Nothing is perfect and plugin interfaces also have their own downsides, which we need to keep in mind to prevent being impacted by them.

  • Required plugins missing: It can be easy to forget to add a plugin the app depends on or to bind it incorrectly. The app will compile and run just fine but we would lose features, leading to buggy behavior.
    The solution is to use integration testing, verifying the features behave as expected.
  • Error handling: It might be tempting to simply wrap the plugin’s execution in a try-catch, but we cannot know what logic within the plugin abstraction throws errors. Incomplete execution of the plugins can lead to inconsistent state.
    The solution depends on the use case, but the general recommendation is for the concrete plugin to handle the throwable and recover itself. If the plugin cannot handle the error itself then we should just propagate the throwable and not try to handle it at the level of a collection of plugins.
  • Dependencies between plugins: A group of plugins may need to run in a certain order and implicit dependencies appear.
    The solution is to set a priority and then after injecting using the @IntoMap Dagger annotation, we can sort the plugins by priority or using an enum of known implementations as a key for the priority. This would propagate some implementation knowledge to the plugin declaration as a tradeoff, but it can be effective when an exact ordering of plugins is required.
  • Low performing plugins: Execution of the plugins could became slow if there are many plugins or if one plugin is doing something expensive. Since the owner of the plugin interface does not have control and visibility over the contributing plugins’ implementations.
    The solution is to monitor per plugin execution metrics in critical parts like app startup to identify potential bottlenecks.
Enjoy the plugin world

Modularization and flattening the module graph is hard and as such requires introducing certain modularization patterns. The plugin-based approach is one such pattern and applying it can help bring the desired benefits of modularization and a highly modular codebase.

Do you use plugins in your project or other modularization approaches? Let others know in the comments.

Happy modularizing!

Thanks Andrew Lord for proof reading and editorial.

This article was previously published on




The ABC of Modularization for Android in 2021

Modularization is not a recent topic at all. This concept have been around us…
When developing a modularized Android project it is expected to have many modules and…
This article aims to bring some learning while scaling an application, going from zero…
The world of software engineering is becoming more and more fascinating as the teams…

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.