Modularization of mobile applications at scale

Alireza Fard
Teknasyon Engineering

--

Here are the best practices of modularization at scale, that we applied in the Getcontact application, by powering feature modularization rather than layer modularization.

The story has begun on the date that the Getcontact application gained more users and the application codebase increased and increased, on the other hand, new developers joined us to deliver new features.

We needed to have a more autonomous and independent team, and in this case, we found a monolithic codebase is not suited. then we decided to modularize our application and here is the story.

In the beginning, we had a complex application with tons of unknown dependencies, which made separating dependencies hard,

Transforming a monolithic application into a modular architecture is a journey toward scalability, it is crucial to establish a set of rules for building a scalable project. This involves creating different types of modules, each serving specific purposes, these rules became our lantern to see and solve the upcoming challenges.

Let’s see one by one what is the meaning of each of them.

Feature module

The Feature module is a small portion of the application, it is responsible for handling tasks such as business logic, UI logic, or data logic, focusing on a specific area.

Now, here’s the trick to prevent any messy circular dependencies: Feature modules should mind their own business and avoid getting tangled up with each other. That means Feature A shouldn’t have a clue about what Feature B is up to. Let them be independent and self-contained.

Feature module circular dependency

Coordinator Module

Sometimes, we find ourselves in a situation where we need to bring together two feature modules on a single page. In this case, the coordinator modules come into play, enabling us to use multiple feature modules within the same graph.

Coordinator sample screen

Now, you might be wondering how this works. Well, here’s the implementation: We create a shiny new module and add both Feature A and Feature B to the dependency manager inside it. Then, in the code layer, we can effortlessly create a page or component that showcases the combined power of these features. It’s like having a super team-up of features working together harmoniously.

Also, coordinators give us the freedom to replace features on the top-level module, based on some logic, like showing a feature to a specific group of users or enabling a feature as an experiment in a specific version or A/B testing, all of these are responsibilities of coordinators.

Coordinator module

In addition in some cases, we need to have shared business logic like checking authentication or user data. for this case, we need to have plugin modules

Plugin module

Let’s imagine a scenario where we have a remote configuration that we want to use throughout the entire application. The initial approach might be to implement this request separately in each feature module where it’s needed. However, this approach isn’t ideal because if there are any future changes to the model or business logic, we would have to carefully modify all the code that has been written.

In a nutshell, this is where plugin modules come into play. They take on the responsibility of handling small snippets of shared business logic. If we have a piece of code that needs to be shared across modules and doesn’t involve any user interface elements, nor provide infrastructure functionalities, then it fits perfectly as a plugin module. This allows for easier management and maintenance of the shared code, making future modifications and updates less cumbersome.

Plugin showcase

Library module

In addition to coordinator and plugin modules, it is essential to have a few shared classes, data structures, or libraries. This helps us avoid duplicating code and promotes code reusability. To achieve this, we introduce a special type of module: library modules.

Library modules take on the crucial role of providing infrastructure components or operating system libraries. For instance, if we need to handle tasks like sending network requests or storing data locally, we create modules such as Network, Cache, and Database. These modules encapsulate the necessary functionalities, ensuring that we have organized and reusable code for these specific purposes.

Launcher module

Last but not least we have the launcher module type,

Imagine we’ve completed the module refactoring and separation, resulting in multiple modules, each performing its specific role like individual Lego pieces. We now have a puzzle with several pieces, and it’s time to bring them together. To accomplish this, we create a new module that acts as the connector, responsible for gathering all the other modules and building a dependency graph for the entire application.

Think of it as the builder in the Lego world, assembling all the individual components into a cohesive structure. This central module acts as the glue, connecting the pieces and ensuring they work harmoniously as a unified whole. It’s like putting the final touches on our puzzle, transforming it into a complete and functional modular project.

Here’s another aspect to consider: to boost development speed and developer experience, we have the flexibility to create multiple launcher modules. Let’s say Team A is focused on developing Features A, B, C, and D, while Team B is working on Features X, Y, and Z.

In this scenario, both teams can create their dedicated launcher module during the development phase. This approach allows them to work independently without worrying about unnecessary dependencies or loading unrelated modules. All they need to do is add their specific features and coordinators to their respective launcher modules. With each run, instead of navigating through the entire application flow, they can simply focus on and interact with their specific features.

Note: here is a sample that gives more insight Getcontact-Bootcamp, however, consider it is not a full implementation of this architecture.

--

--