Chippin’ away at the Monolith

An overview of how the iOS team at Chip is moving to a modular codebase!

Jack Tudor
Chip
5 min readNov 14, 2019

--

Breaking big things into small things, one Chip at a time.

This isn’t a “how-to” guide, but really documenting what challenges we have faced and what solutions we came up with, I hope it sparks some ideas you can use in your own project.

To provide a bit of context, I’ve only been at Chip for around four months, but the codebase is around 18–24 months old. We are using RxSwift with the Model-View-ViewModel (MVVM) design pattern.

When I first joined, the app did have Cocoa Touch Frameworks for common/shared modules but not at all for feature modules, which if introduced would help the codebase scale, make it a lot easier to navigate and ultimately pleasant to work with. This is very similar to what Backend Engineers call Micro-services.

The distinction between them is; feature modules know nothing about each other, so must never import each other. Any code that needs to be shared between feature modules should be moved into a common module.

What we set out to achieve.

Where did we start?

Within the first week, Adam and myself (Chip’s iOS team), decided to start moving some of the app’s tabs into separate feature modules. First being Chip X.

This was the Tab with the smallest amount of logic, so it was fairly easy to decouple and we were able to immediately reap rewards, as we not long after had to build some new features within it.

After getting a taste of modularisation, I tried to move another Tab (Account) into its own module.

This turned into a nightmare as I tried to move one object, it was relied upon by multiple others, so rather than wasting anymore time I stopped. We had to take a bit of a step back to work out what was causing us the biggest pain when trying to move anything, when creating the Accounts module, the biggest issue was anything relating to our models / API resources.

Solving our first problem

The problem: No clear separation of concerns within the main project or at least a clear binding between the ViewModels and our domain objects. To solve this we moved all of our models out of the main project and into a common module (ChipAPI).

This was not ideal but was necessary to achieve what we wanted to do. The reasoning behind this was to allow us to freely move all of our UI code out and into feature modules.

After moving Chip X into its own module.

We agreed that this was temporary solution, and that when a new feature module was created we should move the models and API functions so they aren’t shared with the whole codebase. This allowed us to start moving code into feature modules.

It wasn’t our only problem…

If you come from a codebase that has been built in a way where everything is tightly coupled, there are going to be a few things you’ll need to extract and use a bridge between those components until you’ve completed the migration.

For example, we have a UserService which is responsible for fetching any resources relating to the user and populating model objects. This is used throughout the codebase as we need to know details about the users state across the app.

But we didn’t want to share any of that implementation detail with the “Activity” framework, because it doesn’t need to know about the inner-workings of the class. We created a ActivityAdapter that has our UserService injected and conforms to our ActivityAdapterType which declares what is needed from our UserService:

Our ActivityAdapter which is responsible for transforming our models
ActivityFramework doesn’t need to be exposed to UserService

With an adapter we’re able to pass this into our ActivityCoordinator, to enable it access to these shared models whilst still being able to test the adapters by providing a mock type in our unit tests. Our Coordinator is the entry point into our framework and manages the relationship between the controllers in the framework.

What were the biggest benefits of doing this?

Some readers might be thinking if everything works, where is the benefit?

As the team begins to scale, we want to be able to work on features and areas of the codebase in isolation of one another. We don’t want to deal with huge conflicts when there’s many developers committing code.

It allows us to set a solid foundation for which we can build upon over the next few years, whilst the job isn’t complete it’s important to bear in mind that this will really set us up for the future.

Improved build times with a modular approach because we don’t need to rebuild the whole project, but rather that module so it’s much quicker developing features.

We’re able to see tangible evidence of this, we’re releasing at such a frequent rate (around every 1–2 weeks on average). The number of bugs we’re finding in the app is much lower than a few months ago, and our App Store rating has increased by nearly one whole star!

This is how the Chip iOS codebase highlevel structure looks now

What’s next?

This process is always evolving and never stops, it’s likely we’ll want to breakdown these feature modules further as they continue to grow, this is something we’re beginning to do now with some of the older parts of the codebase.

We’re constantly looking to make improvements in our ways of working, whether that’s architecture, testing or code styling.

If you’re interested in learning how to create a Cocoa Touch Framework, Sam Dods has an excellent blog series outlining how you can make a start.

--

--

Jack Tudor
Chip
Writer for

iOS Engineer @Chip, previously @theappbusiness