Note: detail description of this graph is below in the section: How Modular Architecture Scale

Modular Architecture

Modular Programming is a software design technique that emphasizes separating the functionality of a program into independent, interchangeable modules, such that each contains everything necessary to execute only one aspect of the desired functionality.

We use here CocoaPods as a dependency manager to split the App in isolated modules. CocoaPods is a powerful dependency management system that makes the frameworks integration very easy and convenient. Other dependency managers which we could use are Swift Package Manager or Carthage.

We create a new feature module when the feature is large enough to be called or considered as a product on its OWN. And it can be developed in isolation by cross-functional team.

Every module will have its own Architecture. Each team decides which architecture fits the best for their module to develop(e.g. Clean Architecture +MVVM, Redux..) and Domain. It means that all Domain Entities of the module will be fetched from API inside this module and mapped here. Note: when we want to share some Domain Entities like User we create CommonDomain module.

Every isolated module feature will have its own Dependency Injection Container to have one entry point where we can see all dependencies and injections of the module.

Every module will have an example/demo project to develop it in isolation fastly without compiling the whole app.

Every module's Tests also will be running fast without the host app. They will be with the module source code.

Important to mention that the modules are local, which means that they are located in the same repo(Monorepo) as the main App, inside the folder with the name DevPods. We only make them remote, in a separate repo, when we want to share them with another project. This makes sense until a new second project needs to use this module then it can be very easily moved to its own repo.

As a general rule, we try to minimize the use of 3rd part framework in this Architecture too. If a module needs to depend on 3rd party framework, we try to isolate this use by using wrappers and limit the use of this dependency only to one module. We explain this on an example inside the section bellow: Initial App Scaling -Adding Authentication Module with 3rd party framework

Note: In this article module and framework have equivalent meaning. because the way of creating a new module in Xcode is by creating a new framework. And DIContrainer — is a Dependency Injection Container. Example App of a feature module — means it is Demo App of this feature module and it is used to develop the feature.

Advantages of Modular Architecture

Builds times are faster. Changing one feature module will not affect other modules. And the app will compile faster by recompiling only the changed module. Compiling a big monolith app is much much slower than compiling isolating part(both clean and incremental builds). For example, our clean build time of all app is: 4 minutes vs Payments module example/demo app: 1 minute.

Development time is faster. The improvement is not only in compiling speed but also in accessing the screen that we are developing. For example, during module development from example project, you can easily open this screen as the initial screen in the app launch. You do not need to get through all the screens as it happens usually when we are developing in the main app.

Isolation of Change. When developing in modules there is clear responsibility of the area of code in the project, and when doing merge requests it is easy to see what module is affected.

Tests running in seconds because they still will run without host app, they will be in Pod of the module.

Module Dependencies rules:

Modules can depend on each other(without circular dependencies) and on 3rd party frameworks. (e.g dependency A<->B is not allowed)

A ->B->C means that module A which imports B will have access also to C

When creating a new module and this module depends on other feature that was not yet extracted into a separate module, we delegate this functionality to the main App using delegation or closures. For example, if the Delivery module needs to show chat for a user, we can create delegate func openChat(withUserId:itemId:onView:) inside Delivery module, and implement it inside main App and inject it into Delivery module.

On the other hand, if the Feature already exists in a separate module, we just configure it inside the main App and injected it into the module which we are separating.

Applying Modular Architecture on example project

Here we will separate the monolith App into Service and Feature, totally isolated modules. The monolith App is in this repo.

Before moving Movie Search Feature into a module, first, we need to move Networking Service into a separate module, because from this feature we will need to fetch movie items using Networking Service. It will be configured with the base URL and API key inside App and injected into the Movie Search Feature module(using DIContainer).