So you’ve decided a loosely coupled, highly cohesive, modular architecture with well defined boundaries, is the approach you want to take with your project. Now its time to go over how to deal with separating out the code. Today we’re going to go over an approach with feature modularization, using an example of an e-commerce app.
Feature Modularization
A modular architecture based on feature modularization separates out the codebase into modules loosely based on team structure and feature set. It sets out to define the boundaries of the modules you create based on rough approximations of the boundaries of the teams.
There are a few module types that come out of this sort of modular architecture, including the following that we go over below:
Feature Modules
Feature modules aim to contain the functionality of an entire feature set within your app. That feature module is then owned by a specific team. Feature modules do not know about each other, and navigation between features is handled separately via a public interface, such as a Router
.
For example, if you have an e-commerce app, it might be that one team handles the checkout flow in the app, and another handles the search functionality. In feature modularization, this might look like the checkout team owning the checkout modules and the search team owning the search modules.
It is key to note the plurality of that sentence. Its a case by case basis, but its entirely possible that the checkout process is a big project into itself, so it could be split out into further modules. Something like the structure below would be totally reasonable:
- Checkout Feature Modules
CheckoutDetails
- The module that handles the user entering their details into the checkout.
CheckoutConfirmation
- The module that shows the user a confirmation page, listing the details of their order.
CheckoutPayment
- The module that takes the payment from the user.
All of that code in one module starts to look pretty large in scope, so breaking it down makes sense.
A totally non-scientific rule-of-thumb that I’ve found to be helpful in this approach, is if something is a page in the app (i.e, a home page, login page, settings page), then extracting that page out into a module and putting boundaries around it is a good place to start when it comes to feature modularization. You can work downwards from that point onward. An example where further modularization might be appropriate is if you have a list of reviews on your product breakdown page in this e-commerce app. If the code behind the review component starts to get large in its own right, then pulling it out from its hypothetical ProductDetails
module would make sense.
Feature modules that require to be navigated to or from should signpost that in their module boundaries. Your Home
module might need to navigate to the search page when a user taps the search icon, so your Home
module could advertise this at the boundary with a public Router
that can be attached to the module. It could look like the below:
public protocol Router {
func didTapSearch()
}
Core Modules (aka Service Modules)
Core modules (sometimes referred to as service modules) are modules that cross the boundaries of your team, and provide functionality across numerous features of your app. These core modules are often usually independent of the app you are creating. The e-commerce app we’re building could use them, but equally the game we have in production could use them too. Feature modules can communicate with core modules, and core modules can communicate with each other if required.
An example of a core module in our e-commerce app could be a module that contains your networking code – let’s call it Network
. This could be just the actual functionality of making a network request. Network
can exist by itself, and a feature module like our CheckoutPayment
module can depend on it if needed.

You might then have another core module that represents the checkout functionality of the web API you interact with – lets call this one CheckoutAPI
. That module communicates with the Network
module to make the network requests to the web API, and handles the business logic of returning the correct data models to the app. The CheckoutPayment
module could then depend on the CheckoutAPI
, instead of interacting directly with the networking module. A diagram representing this below:

This could be useful in such cases where more than one feature module needs to know about the CheckoutAPI
, like the below:

Common examples of core modules include:
- Networking
- Metrics
- Authentication
- Crash reporting
- Logging
UI Modules
Next we have UI modules. UI modules contain shared UI elements, perhaps from an internal design system, that can be shared across modules. There may be more than one UI module. Feature modules and navigation modules can depend on UI modules.
It’s common for companies and projects to have branding and UI elements that need to be used across the app, and that when those elements change, those changes need to be reflected app wide for consistency. Thinking about Apple itself, picture the changes made to the UI in iOS 7. Those changes were OS wide. Those types of changes, in the context of an app, become a lot more approachable if your modules all pull from the same UI components. [^1]
Navigation Modules
Lastly, we have Navigation modules. Feature modules shouldn’t depend on one another. This is because the internals of a feature module are grouped together because of their cohesiveness. If one feature was able to access another, it has the potential to be a chain of dependencies that becomes challenging to cut, and teams depend more on each other to get work complete in the feature module they own. We want our module to be as decoupled as possible to get all the benefits from modularization.
As mentioned above, this includes navigation. Navigation should be handled outside your module. When you first start out making feature modules, it’s reasonable to think your main app will be responsible for handling navigation between those modules.

However, as you start to get more and more feature modules in place, the navigation code within your main app starts to get larger and larger. Splitting this out can be helpful, which is where navigation modules come in.
Navigation modules aim to house the navigation flow for an individual navigation flow within the app. Navigation modules don’t know about other navigation modules, but can depend on multiple feature modules.
Using the checkout flow from earlier, we can build a navigation module that handles the transition from screen to screen in the checkout process. This module would contain the initialization code for the feature modules it depends on, and the logic required to navigate to the next screen. Separately, we also have a navigation module that handles the navigation of the ProductDetail
page going to the Search
page

This would mean the main app no longer depends on the feature modules directly, but rather creates the CheckoutNavigation
module for the checkout flow, and the CheckoutNavigation
module handles the navigation between checkout pages. The CheckoutNavigation
module would have a public interface like the rest of our modules, so the main app knows when key events have occurred, such as when the checkout process has completed, and another navigation module needs to be shown.
Conclusion
That’s a broad overview of feature modularization. As the takeaway you have:
- Feature modules
- These are modules divided up by the concerns of the teams, and contain core functionality to get that feature running.
- Feature modules don’t depend on other feature modules.
- Navigation is handled outside of the feature module when required.
- Code modules
- Contains shared functionality required across the app.
- Code modules can depend on other core modules.
- Feature modules and navigation modules can also depend on core modules.
- UI modules
- Contain portions of your UI that’s shared across your application.
- Important for UI that has the same reason to change, i.e apps that rely on heavy branding what consistency when making UI changes across the app.
- Navigation modules
- Navigation are used to handle the flow of navigation between your feature modules.
- Navigation modules can depend on multiple features.
- Useful when breaking out navigation code from your main project.
[^1]: Isn’t this a large amount of coupling to the UI module(s)? Yes. However, that coupling is mitigated by the fact we’re coupling ourselves to something that has the same reason to change. The TextView
in the UI module, when changed, needs to be reflected across the project. A large amount of coupling to module with this in mind is acceptable, rather than perhaps a large amount of coupling to a data object. A Product
model in the ProductDetails
module may care about the review score of a product, but the CheckoutDetailsFeature
won’t care about that as it’s only interested in the price and name of the item, meaning they don’t have the same reason to change and the design of the items are influenced by different properties of the module. Cross pollination of objects like that, that don’t always have the same reason to change, is where the issues of highly coupled codebase’s become apparent. UI modules do not necessarily have the same concerns, when used correctly.