Mobile Architecture11 min read

Container App Strategies: Managing Multiple Brand Apps

Four proven strategies for structuring white-label container apps — from runtime configuration to micro-frontends.

The Container App Decision

When building white-label mobile applications, one of the most consequential architectural decisions is how to structure your container apps. The container is the shell that hosts your feature modules, applies brand theming, and delivers the final experience to users.

Choose wrong, and you'll fight your architecture for years. Choose right, and adding new brands becomes a configuration exercise.

This guide details four container app strategies, with guidance on when to use each.

Strategy 1: Single App, Runtime Configuration

How It Works

One app binary is published to app stores. At runtime, the app loads brand configuration from:

  • Server-side configuration
  • Deep link parameters
  • MDM configuration (for enterprise)
  • User selection

Implementation

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        // Determine brand from various sources
        let brand = BrandResolver.resolve(
            mdmConfig: MDMConfigReader.read(),
            deepLink: launchOptions?[.url] as? URL,
            storedPreference: UserDefaults.standard.string(forKey: "selectedBrand")
        )

        // Load brand configuration
        BrandManager.shared.loadConfiguration(for: brand)

        // Apply theming
        ThemeManager.shared.apply(BrandManager.shared.theme)

        return true
    }
}

Pros

  • Simplest deployment — One app to build, test, and deploy
  • Instant brand switching — No app reinstall needed
  • Shared app store listing — Single set of reviews and ratings
  • Easy A/B testing — Test across brands without separate releases

Cons

  • Larger app size — Includes all brand assets
  • Security concerns — All brand configs in one binary
  • Limited store customization — Same icon, screenshots for all brands
  • Certification complexity — All brands certified together

Best For

  • Internal enterprise apps where IT controls deployment
  • B2B platforms where brands are customers, not end users
  • Proof-of-concept before committing to more complex strategies

Strategy 2: Build-Time Configuration (Flavors/Schemes)

How It Works

Same codebase produces different app binaries through build configuration. Each brand gets its own app with distinct bundle ID, assets, and store listing.

iOS Implementation (Schemes + Targets)

Project/
  App/
    BrandA/
      Info.plist
      Assets.xcassets
      BrandA.entitlements
    BrandB/
    Shared/
      App source code
  Configurations/
    BrandA.xcconfig
    BrandB.xcconfig
// BrandA.xcconfig
PRODUCT_BUNDLE_IDENTIFIER = com.company.branda
PRODUCT_NAME = Brand A App
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-BrandA
BRAND_IDENTIFIER = brandA

Android Implementation (Product Flavors)

android {
    flavorDimensions "brand"

    productFlavors {
        brandA {
            dimension "brand"
            applicationId "com.company.branda"
            resValue "string", "app_name", "Brand A"
            buildConfigField "String", "BRAND_ID", '"brandA"'
        }

        brandB {
            dimension "brand"
            applicationId "com.company.brandb"
            resValue "string", "app_name", "Brand B"
            buildConfigField "String", "BRAND_ID", '"brandB"'
        }
    }
}

Pros

  • Optimized app size — Only includes relevant brand assets
  • Full store customization — Different icons, screenshots, metadata
  • Clear separation — Each brand is a distinct app
  • Independent releases — Can release brands on different schedules

Cons

  • More complex CI/CD — N builds for N brands
  • Longer build times — Multiplied by number of brands
  • Configuration drift risk — Configs can diverge over time
  • Testing overhead — Must test each brand variant

Best For

  • Consumer apps with distinct brand identities
  • Apps with different feature sets per brand
  • Brands with independent release cycles

Strategy 3: Dynamic Framework Loading

How It Works

Container app loads feature frameworks dynamically at runtime based on brand configuration. Features can be updated without full app releases.

Implementation Concept

class FrameworkLoader {
    func loadFeature(_ featureId: String) async throws -> FeatureProtocol {
        // Check if framework is cached
        if let cached = FrameworkCache.shared.get(featureId) {
            return cached
        }

        // Download framework bundle
        let bundle = try await FrameworkDownloader.download(featureId)

        // Load and instantiate
        guard let principalClass = bundle.principalClass as? FeatureProtocol.Type else {
            throw FrameworkError.invalidBundle
        }

        let feature = principalClass.init()
        FrameworkCache.shared.store(feature, for: featureId)

        return feature
    }
}

Platform Considerations

iOS Limitations:

  • App Store apps cannot download and execute code
  • Enterprise distribution can use this pattern
  • Swift Packages/frameworks must be bundled at build time for App Store

Android:

  • Dynamic feature modules via Play Feature Delivery
  • On-demand and conditional delivery supported
// Android Dynamic Feature Module
class FeatureInstaller(private val context: Context) {
    private val splitInstallManager = SplitInstallManagerFactory.create(context)

    suspend fun installFeature(moduleName: String): InstallState {
        val request = SplitInstallRequest.newBuilder()
            .addModule(moduleName)
            .build()

        return splitInstallManager.startInstall(request).await()
    }
}

Pros

  • Update features independently — No full app release needed
  • Reduce initial download size — Load features on demand
  • Maximum flexibility — Different feature sets per user/brand
  • A/B testing at feature level — Test individual features

Cons

  • Platform restrictions — iOS App Store limits dynamic loading
  • Complexity — More moving parts to manage
  • Network dependency — Features may fail to load
  • Debugging difficulty — Dynamic loading is harder to trace

Best For

  • Enterprise apps with own distribution
  • Android apps with modular features
  • Apps with optional/premium features

Strategy 4: Micro-Frontend Architecture

How It Works

Features are delivered as independent mini-apps, each with its own technology stack and deployment pipeline. A shell app composes them into a unified experience.

Implementation Patterns

1. WebView-Based Micro-Frontends

class MicroFrontendHost: UIViewController {
    private let webView = WKWebView()

    func loadMicroFrontend(url: URL) {
        webView.load(URLRequest(url: url))
    }

    // Bridge for native ↔ web communication
    func setupBridge() {
        let bridge = WebBridge()
        webView.configuration.userContentController
            .add(bridge, name: "nativeBridge")
    }
}

2. React Native Micro-Frontends

class ReactMicroFrontend {
    func loadComponent(named: String, props: [String: Any]) -> UIView {
        let bridge = RCTBridge(delegate: self, launchOptions: nil)
        let rootView = RCTRootView(
            bridge: bridge!,
            moduleName: named,
            initialProperties: props
        )
        return rootView
    }
}

Communication Between Micro-Frontends

// Event bus for cross-frontend communication
protocol MicroFrontendEventBus {
    func publish(_ event: MicroFrontendEvent)
    func subscribe(to eventType: String, handler: @escaping (MicroFrontendEvent) -> Void)
}

struct MicroFrontendEvent {
    let type: String
    let source: String
    let payload: [String: Any]
}

Pros

  • Team autonomy — Teams own their features end-to-end
  • Technology flexibility — Different stacks for different features
  • Independent deployment — Update features without coordinating
  • Scaling — Add teams without increasing coordination

Cons

  • Consistency challenges — Harder to maintain unified UX
  • Performance overhead — Multiple runtimes/frameworks
  • Complexity — Significant infrastructure investment
  • Communication overhead — Cross-frontend data sharing

Best For

  • Large organizations with many autonomous teams
  • Acquisitions — Integrating apps built on different stacks
  • Gradual migrations — Moving from legacy to modern stack

Decision Matrix

FactorRuntime ConfigBuild-TimeDynamic LoadingMicro-Frontend
Team sizeSmallSmall-MediumMediumLarge
Release frequencyHighMediumHighHigh
Brand differencesLowMediumMediumHigh
Store customizationNoYesYesYes
App sizeLargerOptimalVariableVariable
ComplexityLowMediumHighVery High
Setup timeFastMediumSlowVery Slow

Migration Path

Most teams evolve through strategies:

  1. Start with Runtime Configuration — Fastest to implement
  2. Move to Build-Time when store customization matters
  3. Add Dynamic Loading for specific features (Android) or enterprise (iOS)
  4. Consider Micro-Frontends only when team scale demands it

Conclusion

The right container strategy depends on your specific context:

  • Small team, quick start → Runtime Configuration
  • Consumer brands, store presence → Build-Time Configuration
  • Enterprise, modular features → Dynamic Framework Loading
  • Large org, team autonomy → Micro-Frontend Architecture

Choose the simplest approach that meets your current needs, but architect for migration to more sophisticated patterns as you grow.


Container strategies refined through multiple enterprise white-label implementations serving millions of users.

Abraham Jeyaraj

Written by Abraham Jeyaraj

AI-Powered Solutions Architect with 20+ years of experience in enterprise software development.