Feature Modularization in iOS

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.

Am image showing feature module, CheckoutPayment, depending on the core module, Network.

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:

Am image showing feature module, CheckoutPayment, depending on the core module CheckoutAPI, which depends on the core module, Network.

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

Am image showing two feature modules, CheckoutPayment and CheckoutDetails, both depending on the core module CheckoutAPI, which depends on the core module, Network.

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]

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.

Am image showing your main app depending on a lot of feature modules. It looks a bit messy.

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

Am image showing your main app depending on navigation modules instead. Its a lot clearer.

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.

  • The Double Edged Sword: Apple Music and Dolby Atmos

    A month or so back I bought “Dark Side of The Moon” on Blu-Ray to finally listen to the Atmos remix and – not to mince words her – it was revelatory. Maybe the most…

  • ImageSequencer – Build a video from a collection of images in iOS/macOS/tvOS

    I’ve been working on Lapsey – an app to make beautiful timelapse’s with – and whilst I won’t be open-sourcing the entire project, I am trying to open-source various components in the app that feel…

  • Get all available cameras in iOS using AVCaptureDevice.DiscoverySession

    I’m working on an iOS project that required finding all of the users available cameras on their device. This used to be simple enough prior to iOS 10, as you could just call: This would…

  • Examples of Use Cases in Swift

    In the last post we went over what use cases are in software development and how they can be used in iOS development. We also went over their origin as a requirements gathering technique for…

  • Use Cases in iOS Development

    Use cases have historically been a somewhat confusing topic to venture into for me personally, and I’m now of the believe that is because they typically have a couple of definitions, depending on who you’re…

  • UML Diagrams with PlantUML and SwiftPlantUML

    PlantUML is an open-source tool used to produce an assortment of diagrams using text. With other diagramming tools, the paradigm is typically a GUI and some dragging and dropping of various objects to build up…

  • Camera for iOS – A Swift Package

    Currently preparing a large post going over Clean Architecture in iOS and how that relates to modularization, but whilst that is in the making, I figured I’d post about my newly released Camera framework, and…

  • Feature Modularization in iOS

    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…

  • Module Boundaries in iOS

    We’ve talked about what modularization is, and what its advantages are when used in a decoupled, cohesive way. It feels only reasonable that we should dig into the meat of what a modular architecture could…

  • Advantages to modularization in iOS

    We’ve already talked about what modularization is, and why a team might want to architect their codebase in such a way. But what are the real life advantages to having multiple units of code to…