In the late 2000s, web businesses like Netflix and Amazon famously faced the challenges of building software at mammoth scale. Seeking to minimize the friction of hundreds of contributors all making changes to enormous shared codebases, they split their software into services that could be deployed and scaled in isolation on hardware rented in the cloud. Decomposing monolithic applications into a fleet of independent microservices enabled teams to move faster with fewer operational conflicts: Product-oriented teams could own an individual service, with fine-grained control over its development and operations.
Today, mobile development teams face the same challenges of scale that these web giants experienced in the early aughts. Moreover, they must overcome an additional hurdle: the fact that apps ship as single binaries that users download and run on their devices. As a mobile app’s codebase grows, it takes longer and longer to compile into a bigger and bigger binary.
To address these scaling challenges, mobile teams at companies like SoundCloud, Just Eat, and realestate.com.au have been exploring a microservices-like approach to architecting their applications. By isolating modules in dedicated codebases, they’ve found they can sidestep lengthy build times and work instead on dedicated, feature-specific apps that provide a much faster feedback cycle.
Enter the microapps architecture.
What are microapps?
Microservices took segregated areas of a backend and deployed them individually. Similarly, mobile developers can take the different core parts of their application—single features, shared business logic, and low-level capabilities—and move them to standalone module libraries. The resulting modules are independent from one another and from the main app codebase, and teams can work on them autonomously.
Also in this issue
The building blocks of scale
A discussion of the fundamentals of flexible, extensible mobile teams.
What sets this architecture apart from other approaches that emphasize modularity is the use of module-specific applications—microapps—as a tool for fast-paced development and testing. Teams can build one or more internal-facing microapps tailored to their needs, including only the modules necessary for the feature they’re working on. A team working on the checkout component of an e-commerce app, for example, could build a testing microapp enumerating combinations of payment methods, shipping addresses, and cart contents. This would allow them to test the checkout flow much faster than if they were to manually reproduce each combination in the main application.
Having dedicated microapps for specific features is a great advantage for iteration speed: A microapp builds much faster than the user-facing application because there’s considerably less code to compile. Moreover, because microapps are for internal use only, they don’t require a polished UX, can come pre-seeded with relevant test data, and can dodge entire aspects of the customer journey, such as onboarding. This significantly reduces friction when verifying changes, and compounds with the faster build time to create a more rapid and efficient development workflow.
The microapps architecture, then, consists of a modular design complemented by the use of dedicated applications for development and testing (called microapps), which together serve to increase developer velocity. This is more of an abstract pattern than a well-defined framework like MVC (model–view–controller) or MVVM (model–view–viewmodel), since the architecture will vary depending on a particular app’s features. But while there’s no one-size-fits-all approach, all successful implementations share the same pillars.
The foundations of a microapps application
At its core, a microapps application is a network of loosely coupled, highly cohesive modules, with higher-level feature modules relying on lower-level utility ones. These are tied together by a thin coordination layer—the user-facing application—and supported by a backbone of advanced tooling. Each feature module can have one or more dedicated microapps that teams work on to obtain fast feedback when developing and testing changes.
The user-facing app
The codebase of the user-facing app wraps around the isolated modules and acts as a coordinator, bringing them together into a unified user experience. Its implementation should be minimal since all of its functionalities and business logic exist in dedicated modules. It instantiates the feature modules at launch, provides modules the services they require, relays relevant information from one module to the other, and propagates operating system and application life cycle events.
Each feature or cluster of features that falls under the same business vertical exists in a dedicated module. For example, in an e-commerce app, browsing inventory might live in a different module from cart management. Inside the module’s codebase is all the required business logic and custom UI for the feature.
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.
Modules don’t implement lower-level functionality like networking or persistence directly; instead, they define abstractions for the low-level capabilities they require and rely on the application they plug into to provide concrete implementations. Developers iterate on feature modules mostly through unit tests and by building dedicated microapps.
Whether or not a microapps application has a design system, all UI elements and configurations shared across features should live in a dedicated library that feature modules can import. This dramatically reduces UI code duplication and helps offer a consistent visual experience to the user.
Foundation and utility modules
Foundation and utility modules provide shared lower-level functionalities to the feature modules and user-facing app.
Foundation modules centralize the implementation of functions like interfacing with the remote API or loading data from device storage. Aggregating all logic related to a low-level functionality allows for better local reasoning when making changes: When different feature modules use the same lower-level logic, each module benefits from improvements made to others. Returning to our e-commerce app example, a developer on the inventory browsing team might want to improve sales by speeding up network response decoding. Because network decoding is part of a foundation module, the developer’s change will make all requests in the application faster, not just the ones for the browsing feature module.
Utility modules hold logic such as standard library extensions or well-defined, isolated functionalities like custom date formatting. This code tends to change at a far slower pace than foundation or feature components, so storing it in a dedicated library means it won’t need to be recompiled when building a consumer app.
In a microapps application, CI can inspect every changeset, identifying modified modules and their downstream dependencies and only running tests for that smaller subset of the codebase, leaving out those unaffected by the change. Because of the smaller surface area, these builds run faster and developers receive feedback on their changes sooner.
Automation, in the form of scripts or advanced code generation tools like Tuist, makes integrating new modules into the user-facing app a less error-prone task, eliminating the need for developers to edit configuration files with many options, have a complete mental representation of the application’s dependency tree, or understand the arcane details of the build system. The quality of a microapps application’s automation substrate can be the difference between hours spent wrangling releases and a one-step process that anyone can trigger.
Together, CI and automation are indispensable to microapps applications, ensuring testing and deployment moves quickly and modules are seamlessly integrated.
Challenges and trade-offs
Like any architecture pattern, the microapps approach comes with trade-offs. Microservices have heavily influenced the microapps architecture, but there’s a key difference between the two: Microservices are deployed individually, while the modules that form a microapps application compile down into the same binary. This technological constraint limits the freedom individual teams have when choosing how to build their modules.
Take third-party libraries, for example. In the isolated context of their modules, teams are free to choose different implementation strategies. But what if two teams have imported two libraries to solve the same problem? The user-facing app will have extra weight, making it expensive to download and update. Instead, teams will need to coordinate and agree on a list of minimum viable third-party libraries they can adopt across the application.
Modularity alone doesn’t necessarily lead to greater developer velocity.
Effective communication and collaboration between teams is an omnipresent challenge in software development, but microapps’ emphasis on isolated components exacerbates it. Giving teams freedom of operation within their module while ensuring consistency in the end product is a difficult balance to strike, and engineering leadership must play a pivotal role in helping reach that equilibrium. Holding regular internal meetups and rotating developers across modules can help break up knowledge silos, ensure developers are familiar with every part of the codebase, and promote cross-team adoption of best coding practices.
Modularity alone doesn’t necessarily lead to greater developer velocity. Drawing the appropriate boundaries around modules is a critical—but challenging—part of defining the microapps architecture. High-level module boundaries should align with your organizational structure. An e-commerce company, for example, might have dedicated divisions for inventory and payments, and its app modules should be separated according to these business functions. Otherwise, changes driven by business needs will necessitate updates in multiple modules, and the isolated development workflow might begin to break down.
The path to microapps architecture
Adopting a microapps architecture takes time and requires a good deal of learning and experimentation. During your first few module extractions, pay attention to the boundaries between your system’s components, what it takes to isolate and migrate a component, how the codebase should be organized, and how your tooling needs to be improved to support the build, testing, and deployment of the application as it becomes fully modular.
Parts of the application that are already isolated or shared by multiple features can be ideal candidates for your first module extractions. These should require few changes to the code, allowing your team to focus on and learn about the extraction process itself. Examples include foundational components, such as API clients; ubiquitous UI elements, like buttons with custom styling; or clusters of lower-level functionalities with no dependencies upstream, such as standard library extensions.
Once you’ve pulled out three to five modules, translate what you’ve learned into clear standards for creating new modules. These should lay out how a module’s codebase should be organized, how it should integrate into the user-facing app, and its CI setup. Automation should enable anyone to generate the scaffolding upon which new modules are built. This early investment in learning, documentation, and tooling will establish a stable foundation for the remaining migration.
The microapps architecture is still in its infancy, and there’s plenty of room for teams to iterate and innovate on these approaches. I look forward to seeing more developers experiment with its implementation details, push its boundaries, and share their learnings. I hope this discussion of the architecture’s possibilities, characteristics, and challenges can serve as a helpful starting point.
Many thanks to Adam Sharp, Alberto De Bortoli, Eli Budelli, Jared Sorge, Kassem Wridan, Pedro Piñera Buendía, Pete Goldsmith, Prasanna Gopalakrishnan, Stew Gleadow, and everyone else who shared their time and experience for this article.