Implementing Micro Frontends in Android with Compose and Multi-Module Libraries
Translating Micro Frontends to Android
Cam Jackson's Micro Frontends article on martinfowler.com defines the pattern as independently deliverable frontend applications composed into a greater whole. It's a web article, built around iframes, script tags, and webpack — but the underlying ideas (bounded contexts, autonomous teams, avoiding shared state, using routing as a contract) translate remarkably well to an Android app built with Gradle multi-modules and Compose. This article walks through that translation, module by module, and is honest about where the analogy actually breaks down.
The core mapping
| Micro Frontends (web) | Android equivalent |
|---|---|
| Container application | :app module — navigation shell, theming, auth |
| A micro frontend (one page, one team) | A :feature:* Gradle module |
| Shared component library | :core:designsystem module |
| The URL as a communication contract | Type-safe Navigation routes (NavKey classes) |
| BFF (backend-for-frontend) | A dedicated backend service per feature domain |
The container: your :app module
Fowler's container application renders common page furniture, owns cross-cutting concerns like auth and navigation, and assembles micro frontends without knowing their internals. Your :app module should do exactly this and nothing more: own the NavDisplay/navigation shell, hold the shared auth/session state, apply the app-wide theme, and compose feature modules together — with zero business logic of its own.
// :app module
val entryProvider = entryProvider {
browseFeatureSection() // contributed by :feature:browse
profileFeatureSection() // contributed by :feature:profile
orderFeatureSection() // contributed by :feature:order
}
NavDisplay(
entries = navState.toEntries(entryProvider),
onBack = { navigator.goBack() }
)This is a near-literal translation of Fowler's container pattern — the app module doesn't contain browse or profile logic, it just knows how to ask each feature module for its section and mount it.
Feature modules as micro frontends
Each :feature:* module should be a self-contained bounded context: its own Composable screens, its own ViewModels, its own domain use cases and repository implementations, developed and (ideally) owned by a single team. The critical enforcement mechanism — the Android equivalent of Fowler's "thicker lines around bounded contexts" — is Kotlin's internal visibility modifier, applied aggressively:
// :feature:profile module
@Serializable
data object ProfileRoute : NavKey // public — this is the contract
internal class ProfileViewModel(...) : ViewModel() // hidden from other modules
internal class ProfileRepositoryImpl(...) : ProfileRepository // hidden
fun EntryProviderScope.profileFeatureSection() {
entry { ProfileScreen() } // the only public entry point
}Only the route class and the entry-provider extension function are public. Everything else — ViewModels, repositories, internal state — is invisible outside the module, exactly as Fowler recommends keeping a micro frontend's internals opaque to the container and to other pages.
Where the deployment analogy breaks down — and it's important to be honest about this
Fowler is emphatic that build-time integration (bundling all micro frontends into one deployable via package dependencies) reintroduces a lockstep release process, and recommends against it. Here's the uncomfortable truth for Android: a standard multi-module Android app compiles into a single APK/AAB. Your :feature:* Gradle modules give you compile-time decoupling and team autonomy in the codebase — genuinely valuable — but they still all ship together through one Play Console release, reviewed and rolled out as a unit. That's structurally closer to Fowler's build-time integration than to his preferred run-time model.
The closest Android gets to true independent delivery is Dynamic Feature Modules via Play Feature Delivery — modules that can be downloaded on-demand or conditionally, after the base app is already installed. This gets you closer to Fowler's "deploy this piece without redeploying everything" ideal, but it's still gated through the same single app listing and release process; you don't get per-team, independent CI/CD pipelines shipping straight to users the way web micro frontends can. Be honest with your team about this distinction — multi-module Android buys you codebase and team autonomy, not true independent deployability, unless you invest specifically in dynamic delivery for the modules that need it.
Styling: Compose actually has an advantage here
Fowler spends real time on CSS's global, cascading nature as a genuine problem for micro frontends — one team's h2 selector silently clobbering another's. Compose doesn't have this problem by default: styling is explicit, passed down via MaterialTheme and composition locals, not globally cascading. Your job is simpler than the web equivalent: put your design tokens (colors, typography, shapes) in a :core:designsystem module, have every feature module consume that theme rather than defining its own, and you get visual consistency without needing BEM-style naming conventions or CSS-in-JS isolation tricks.
Shared component libraries: harvest, don't predict
Fowler's advice here transfers almost word-for-word: don't try to build a comprehensive shared component library upfront by guessing what every feature will need. Let feature modules build their own components as they need them, and only promote a component into :core:designsystem once you see the same pattern emerge in two or three features independently. And critically — a shared component should contain only UI logic, never domain logic. A shared RestaurantCard composable that bakes in assumptions about what a "restaurant" is creates exactly the kind of cross-module coupling Fowler warns against.
Cross-feature communication: routes as the contract
Fowler highlights the URL as an unusually good communication mechanism between micro frontends: well-defined, globally accessible, declarative, and it forces indirect communication rather than direct coupling. Android's type-safe navigation routes are a near-perfect equivalent. When the browse feature needs to open a specific restaurant, it doesn't call into the order feature's internals — it navigates to a route:
navigator.navigate(RestaurantRoute(id = restaurant.id))The RestaurantRoute class is the contract, exactly as a URL path is the contract in Fowler's example — and just like his warning about URL contracts, changing that route's shape is a breaking change that needs coordinating across every module that references it. Avoid the temptation to share mutable state (a shared ViewModel holding cross-feature data) as your default communication mechanism — it recreates the "shared Redux store" problem Fowler warns against, where every module ends up implicitly coupled to a shared state shape.
Backend communication: BFF still applies
Fowler's backend-for-frontend pattern — a dedicated backend that serves exactly one frontend's needs — applies cleanly if your team also owns backend services (say, in NestJS). If a feature team owns both the Android module and its corresponding API, they can iterate independently of other teams' backend changes. If your app instead depends on a single monolithic API shared by every feature, that's a real point of coupling between otherwise-independent modules worth acknowledging, not a total blocker.
Testing: mirror Fowler's pyramid
Unit test each feature module's ViewModels and use cases in complete isolation — this is where most of your test coverage should live, exactly as Fowler recommends for individual micro frontends. Then add a small number of instrumented tests at the :app level that verify the "contract" is honored: does navigating to RestaurantRoute(id) actually render the right screen with the right data. Keep these thin — they're checking integration, not re-testing business logic that's already covered at the module level.
The downsides, translated honestly
- Payload size → APK size and method count. Each feature module pulling in its own version of a utility library, instead of sharing one from
:core, bloats your final APK the same way duplicated React bundles bloat a micro-frontend site. Centralize common dependencies (Compose BOM, coroutines, common utilities) in core modules. - Environment differences → module isolation for development. Give feature modules their own minimal host Activity or Compose Preview setup so they can be developed and previewed without spinning up the entire app — the direct equivalent of Fowler's "standalone mode" for each micro frontend.
- Governance complexity → Gradle convention plugins. More modules means more repeated build configuration. Invest in Gradle convention plugins early so every feature module shares consistent lint rules, compiler options, and dependency versions, rather than each team improvising its own
build.gradle.ktsconventions.
The honest conclusion
Most of what makes micro frontends valuable — bounded contexts, team autonomy, avoiding accidental coupling, using a contract (routes) instead of shared state — transfers directly to Android multi-module architecture with Compose, and arguably transfers more cleanly than it does to the web, since Compose's explicit theming avoids CSS's global cascade problem entirely. The one place the analogy genuinely breaks is deployment independence: Android's single-APK release model means you're buying codebase and team decoupling, not the fully independent per-team release pipelines Fowler describes — unless you specifically invest in Dynamic Feature Modules for the pieces that need that property. Know which benefit you're actually getting before you sell the architecture internally as "microfrontends for Android."