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:

  1. Feature ownership — Each team owns specific feature modules with clear APIs
  2. Shared libraries — Design system, analytics, networking as shared modules
  3. Build system — Convention plugins, Gradle build cache, CI/CD pipeline
  4. API contracts — Feature modules communicate via interfaces, not implementations
  5. 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

codeTap to expand ⛶
1// Feature Flag system for large-scale apps
2interface FeatureFlagProvider {
3 fun isEnabled(flag: FeatureFlag): Boolean
4 fun getVariant(flag: FeatureFlag): String?
5}
6
7enum 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}
12
13// Remote Config backed implementation
14class RemoteFeatureFlagProvider @Inject constructor(
15 private val remoteConfig: FirebaseRemoteConfig,
16 private val localOverrides: DataStore<Preferences>
17) : FeatureFlagProvider {
18
19 override fun isEnabled(flag: FeatureFlag): Boolean {
20 // Local overrides for development/testing
21 val localOverride = runBlocking {
22 localOverrides.data.first()[booleanPreferencesKey(flag.key)]
23 }
24 if (localOverride != null) return localOverride
25 return remoteConfig.getBoolean(flag.key)
26 }
27}
28
29// Usage in feature module
30@Composable
31fun HomeScreen(featureFlags: FeatureFlagProvider = hiltViewModel<HomeVM>().flags) {
32 if (featureFlags.isEnabled(FeatureFlag.NEW_HOME_SCREEN)) {
33 NewHomeContent()
34 } else {
35 LegacyHomeContent()
36 }
37}
38
39// CODEOWNERS file — enforce module ownership
40// .github/CODEOWNERS
41// /feature/auth/ @team-a
42// /feature/home/ @team-b
43// /feature/search/ @team-c
44// /core/design-system/ @platform-team
45// /core/network/ @platform-team
46
47// Architectural fitness function — enforce dependency rules in CI
48// Custom Gradle task or ArchUnit test
49class ArchitectureTest {
50 @Test
51 fun featureModulesShouldNotDependOnEachOther() {
52 // Parse dependency graph and assert
53 val featureModules = getFeatureModules()
54 for (module in featureModules) {
55 val deps = module.dependencies
56 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:

  1. Design a module structure for a social media app with 5 feature teams
  2. Implement a feature flag system with remote config and local overrides
  3. Set up CODEOWNERS and branch protection rules for module ownership
  4. Create an architectural fitness function that validates dependency rules
  5. 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.