Large-Scale App Structure
📖 Concept
Structuring apps at scale (10+ engineers, 500K+ LOC) requires deliberate architectural decisions. At Google, Android apps like Gmail, Maps, and Photos have hundreds of modules and dozens of teams.
Key principles for large-scale apps:
- Feature ownership — Each team owns specific feature modules with clear APIs
- Shared libraries — Design system, analytics, networking as shared modules
- Build system — Convention plugins, Gradle build cache, CI/CD pipeline
- API contracts — Feature modules communicate via interfaces, not implementations
- Incremental adoption — New patterns in new code, legacy code migrated gradually
App structure at scale:
:app (thin shell)
:feature:auth (Team A)
:feature:home (Team B)
:feature:search (Team C)
:feature:profile (Team A)
:core:design-system (Platform team)
:core:analytics (Platform team)
:core:network (Platform team)
:core:database (Platform team)
:core:testing (Platform team)
:lib:image-loader (Shared library)
:lib:crash-reporting (Shared library)
Scaling challenges and solutions:
- Build times: Build cache, incremental compilation, module-level parallelism, Baseline Profiles
- Code conflicts: Module isolation, CODEOWNERS file, feature flags for WIP code
- Consistency: Convention plugins, shared lint rules, architectural fitness functions
- Testing: Each module has its own tests, integration tests at app level, screenshot tests for UI
Feature flags: Essential for large teams. Deploy code behind flags, enable gradually, rollback instantly.
💻 Code Example
1// Feature Flag system for large-scale apps2interface FeatureFlagProvider {3 fun isEnabled(flag: FeatureFlag): Boolean4 fun getVariant(flag: FeatureFlag): String?5}67enum class FeatureFlag(val key: String, val defaultValue: Boolean) {8 NEW_HOME_SCREEN("new_home_screen", false),9 DARK_MODE_V2("dark_mode_v2", false),10 OFFLINE_SYNC("offline_sync", true),11}1213// Remote Config backed implementation14class RemoteFeatureFlagProvider @Inject constructor(15 private val remoteConfig: FirebaseRemoteConfig,16 private val localOverrides: DataStore<Preferences>17) : FeatureFlagProvider {1819 override fun isEnabled(flag: FeatureFlag): Boolean {20 // Local overrides for development/testing21 val localOverride = runBlocking {22 localOverrides.data.first()[booleanPreferencesKey(flag.key)]23 }24 if (localOverride != null) return localOverride25 return remoteConfig.getBoolean(flag.key)26 }27}2829// Usage in feature module30@Composable31fun HomeScreen(featureFlags: FeatureFlagProvider = hiltViewModel<HomeVM>().flags) {32 if (featureFlags.isEnabled(FeatureFlag.NEW_HOME_SCREEN)) {33 NewHomeContent()34 } else {35 LegacyHomeContent()36 }37}3839// CODEOWNERS file — enforce module ownership40// .github/CODEOWNERS41// /feature/auth/ @team-a42// /feature/home/ @team-b43// /feature/search/ @team-c44// /core/design-system/ @platform-team45// /core/network/ @platform-team4647// Architectural fitness function — enforce dependency rules in CI48// Custom Gradle task or ArchUnit test49class ArchitectureTest {50 @Test51 fun featureModulesShouldNotDependOnEachOther() {52 // Parse dependency graph and assert53 val featureModules = getFeatureModules()54 for (module in featureModules) {55 val deps = module.dependencies56 val featureDeps = deps.filter { it.startsWith(":feature:") }57 assert(featureDeps.isEmpty()) {58 "${module.name} depends on features: $featureDeps"59 }60 }61 }62}
🏋️ Practice Exercise
Practice:
- Design a module structure for a social media app with 5 feature teams
- Implement a feature flag system with remote config and local overrides
- Set up CODEOWNERS and branch protection rules for module ownership
- Create an architectural fitness function that validates dependency rules
- Design a shared design system module with Compose components
⚠️ Common Mistakes
Not having a platform team for shared modules — each feature team creates their own networking/analytics code
Allowing feature-to-feature dependencies — creates coupling that slows teams down
Skipping feature flags — deploying directly to production without gradual rollout
Not investing in CI/CD — slow builds and flaky tests compound at scale
Monolithic database module — each feature should own its own tables/DAOs when possible
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Large-Scale App Structure. Login to unlock this feature.