When it comes to scaling a mobile app, it’s tempting to focus solely on the technical challenges. But anticipating what a mobile team will require as it expands, and making strategic decisions to support sustainable growth, can be just as critical as adding features, expanding localization, and enabling more dynamic app delivery.
Above all, mobile teams must be intentional about their team structure, evaluate their codebase from an extensibility perspective, and choose an app architecture that empowers engineers to make independent contributions with reduced risk to the rest of the app. Together, these choices can support mobile teams as they tackle the technical hurdles of scale.
Smaller, more specialized teams
The journey from monolithic team (and codebase) to independent teams and components is a familiar one. Many mobile teams working on a single app start out as one unit that relies on a shared data source. But as they grow larger, well-established client application organizations often splinter into smaller, cross-functional teams dedicated to a particular feature set or functionality.
For example, I once worked on a mobile team that was primarily responsible for activating and managing connected devices in the home. The six of us were evenly split between iOS and Android, and we all worked on every part of the app, from authentication and login to device management. As the app gained users and feature requests increased, our team expanded, too. Once we reached about 12 engineers, things began to slow down: It took more effort to keep track of incoming changes, dividing work required more coordination, and meetings—even daily stand-ups—ran long. Eventually, we considered splitting into smaller teams, with certain developers working on specific parts of the app.
I can be an expert on activation because I don’t have to be an expert on everything else.
There are many advantages to this approach. Having more specialized teams can reduce communication overhead and enables engineers to develop deep expertise and increased ownership over their domain. For example, I currently work on activation within the Xfinity app, which means my main development focus is only a subset of the app’s features. This affords me a deep understanding of all app logic within my scope of work and more ownership over the features I build and maintain. I can be an expert on activation because I don’t have to be an expert on everything else.
But this decreased scope can come at a price. Having smaller, more specialized teams requires strong cross-team relationships so engineers know when and how to collaborate on overlapping features. If my team is only responsible for the activation process but we also need to display the device’s activation status on the app’s landing page, we’ll need to know whether the team responsible for that component can support the change, and how their priorities and delivery timeline might differ.
Unlike a web application, a mobile feature can’t be released independently from the app as a whole, so cross-team coordination and regression cycles must be especially thorough. Clearly documenting changes in a given app release, making pull requests available for anyone to review, implementing a weekly platform stand-up (one for iOS, one for Android), and scheduling shared design reviews for all client engineers can help keep communication flowing across teams without bogging developers down in day-to-day details.
Organized, testable code
The single responsibility principle coined by Robert C. Martin (aka Uncle Bob) holds that “each software module should have one and only one reason to change.” That is, a module—which can be a function, a class, or even a larger component like a library or framework—should have a single responsibility, and shouldn’t be touched unless that responsibility is being modified.
Applying the single responsibility principle to your codebase makes the code simpler to read and navigate, which in turn makes it easier for developers to onboard as your team grows. A new contributor will be able to start working within a component without extensive background knowledge or understanding of the rest of the app. Individual teams will also be able to add new features or functionality without impacting other parts of the code. If the team responsible for the chat feature needs to make a change, for instance, they’ll be able to implement and test it in isolation without affecting the code related to, say, user profiles.
Also in this issue
A primer on automated mobile testing
Insights for teams looking to tailor their test suites and build apps that just work.
This modular approach can also improve a codebase’s testability by making each feature or function’s responsibility clear and intentional, allowing it to be isolated. Mobile teams often find it challenging to implement adequate automated test coverage. UI testing is especially troublesome for mobile given the limited availability of mobile UI testing frameworks, an excess of possible app and device state conditions, and the ability to mock the API responses or business logic responsible for driving the UI, among other issues. Limiting dependencies between objects can help facilitate UI and unit testing by making it possible to isolate the object being tested, usually by mocking its dependencies with dependency injection. Dependency injection patterns require all of an object’s dependencies to be accounted for at creation, which forces developers to be explicit about relationships between objects.
Together, modularity and testability are fundamental properties of a scalable codebase. Implementing the single responsibility principle and being intentional about dependency injection allowed an app team I worked on to expand code coverage from 13 percent to 95 percent, automate many of our test cases, and deliver new features faster. Prioritizing these qualities can also help teams reduce merge conflicts, minimize the need for communication about unrelated changes, and lower the risk of costly defects caused by unclear relationships between objects.
If your end goal is a testable app with components defined by responsibility, how do you get there? Being intentional about app architecture is essential. Code should be structured to enable work in isolation and establish clear boundaries between components. There’s no one-size-fits-all solution here: Your goals—testable code separated into units of shared responsibility, along with any app- or team-specific requirements—should inform your choice of architecture.
The most common and well-documented mobile architectures are MVC (model–view–controller), MVP (model–view–presenter), MVVM (model–view–viewmodel), and VIPER (view, interactor, presenter, entity, router). Each has its own advantages and disadvantages. MVC, for example, is often used in documentation, educational resources, and example apps, since it provides a simple, straightforward app implementation: The view contains UI elements displayed to the user, while the controller implements business logic and acts as a mediator between the model and the view. However, using MVC for an app with more complex navigation, data that requires significant manipulation, or multiple backend services can lead to the massive view controller problem: As the view controller is burdened with more responsibility, it becomes increasingly difficult to isolate components and limit dependencies.
In contrast, MVVM and VIPER lend themselves well to larger codebases because they break components down beyond the MVC pattern. MVVM provides the ability to separate UI logic—not just the UI elements—from business logic by introducing the concept of a view model. VIPER goes one step further, with individual components for view, interactor, presenter, entity, and router. As the component names suggest, VIPER designates specific objects to handle navigation and the flow of data between screens or components.
Though these four dominant mobile architecture patterns provide varying levels of flexibility and modularization, they all tend to focus on the UI and business logic components of the code while glossing over responsibilities outside the user interface. But the data driving your app has to come from somewhere. Most of the apps I’ve worked on have been broken down into layers representing the user interface, business logic, storage and caching, and networking. It’s helpful to think of these as layers, rather than components or sections of the app, because a single component or feature may travel through each layer to retrieve the appropriate data to display in the UI, but adding a new feature shouldn’t require code changes to each one.
Splitting your app into separate layers in the architecture design helps teams modify and test each one independently, allowing feature teams to work in tandem with less risk of merge conflicts. A developer should be able to write automated tests for the UI layer by mocking out the business layer; similarly, they should be able to make changes to the networking layer without affecting other parts of the app.
Though many projects I’ve worked on have taken this approach, it’s worth mentioning that each was at its own stage of maturity with respect to architecture. Some apps had extremely well-defined architectures because we’d spent years improving upon them, while some were more of a work in progress. What’s most important about your app architecture, no matter how robust, is that your team has defined the end goal, and works toward it with every pull request.
Even if your starting point is a legacy codebase, you can implement these changes incrementally and see the benefits materialize quickly.
Developing a flexible and scalable team structure, codebase, and architecture is no small task, and doing so while working within the constraints of a monolithic-by-nature mobile app requires extra care and attention. The good news is this isn’t an all-or-nothing effort. Even if your starting point is a legacy codebase, you can implement these changes incrementally and see the benefits—easier onboarding, more independent teams, better automated test coverage, and faster feature delivery—materialize quickly.
The work is never truly done. There will always be processes that can be improved, or edge cases that haven’t been considered. But if engineers are committed to being good code stewards and feel a sense of ownership over championing improvements (both technical and not), continual evolution can become a part of your culture, enabling teams to scale as quickly and efficiently as the apps they build.