Mobile Architecture12 min read

Modular Mobile Architecture: Features as Frameworks

How to structure mobile applications as composable feature modules — enabling parallel development, independent testing, and flexible deployment.

The Monolith Problem

Every mobile application starts simple. A few screens, a handful of features, one team. Then growth happens. More features. More developers. More complexity. Before you know it, you have a monolithic codebase where:

  • Changing one feature breaks another
  • Build times stretch to 15+ minutes
  • New developers take weeks to become productive
  • Testing requires running the entire app
  • Teams step on each other's code constantly

The solution? Treat features as frameworks — independent, versioned, composable modules that can be developed, tested, and deployed in isolation.

What Is a Feature Framework?

A feature framework is a self-contained module that encapsulates:

  • UI Components — Screens, views, and view controllers
  • Business Logic — Use cases, services, and domain models
  • Data Layer — Repositories, network calls, and local storage
  • Tests — Unit, integration, and UI tests

From the container app's perspective, a feature is a black box with well-defined inputs (dependencies) and outputs (public interfaces).

Benefits of Feature Modularization

1. Parallel Development

With clear boundaries, teams can work independently:

  • Team A develops the Payments feature
  • Team B develops the Profile feature
  • Team C develops the Messaging feature

No merge conflicts. No coordination meetings. No waiting.

2. Faster Build Times

Modular architectures enable:

  • Incremental builds — Only rebuild changed modules
  • Parallel compilation — Modules build simultaneously
  • Cached artifacts — Unchanged modules don't rebuild

A 15-minute monolithic build becomes 2-3 minutes of incremental builds.

3. Independent Testing

Each feature can be tested in isolation:

// Feature-level integration test
class AuthenticationFeatureTests: XCTestCase {
    var sut: AuthenticationCoordinator!
    var mockNetwork: MockNetworkService!

    func testLoginFlow() async throws {
        mockNetwork.stub(.login, response: .success(mockUser))

        let result = await sut.login(email: "test@example.com",
                                      password: "password")

        XCTAssertEqual(result, .authenticated(mockUser))
    }
}

No need to launch the entire app. No test pollution from other features.

4. Code Ownership

Clear module boundaries enable clear ownership:

FeatureTeamCodeowners
AuthenticationIdentity@identity-team
PaymentsCommerce@commerce-team
MessagingEngagement@engagement-team

Pull requests automatically route to the right reviewers.

5. Flexible Deployment

Features can be:

  • Included or excluded per brand
  • Released independently (with feature flags)
  • A/B tested without full app releases

Architecture Patterns

The Coordinator Pattern

Features expose a coordinator as their public interface:

public protocol AuthenticationCoordinating {
    func start() -> UIViewController
    var authStatePublisher: AnyPublisher<AuthState, Never> { get }
    func logout() async
}

public class AuthenticationCoordinator: AuthenticationCoordinating {
    private let dependencies: AuthDependencies

    public init(dependencies: AuthDependencies) {
        self.dependencies = dependencies
    }

    public func start() -> UIViewController {
        let viewModel = LoginViewModel(
            authService: dependencies.authService,
            analytics: dependencies.analytics
        )
        return LoginViewController(viewModel: viewModel)
    }
}

Dependency Injection

Features declare their dependencies through protocols:

public struct AuthDependencies {
    public let networkService: NetworkServiceProtocol
    public let secureStorage: SecureStorageProtocol
    public let analytics: AnalyticsProtocol

    public init(
        networkService: NetworkServiceProtocol,
        secureStorage: SecureStorageProtocol,
        analytics: AnalyticsProtocol
    ) {
        self.networkService = networkService
        self.secureStorage = secureStorage
        self.analytics = analytics
    }
}

The container app provides concrete implementations:

let authDependencies = AuthDependencies(
    networkService: AppNetworkService(),
    secureStorage: KeychainStorage(),
    analytics: FirebaseAnalytics()
)

let authCoordinator = AuthenticationCoordinator(dependencies: authDependencies)

Feature Flags Integration

Features should respect feature flag state:

public class PaymentsCoordinator: PaymentsCoordinating {
    private let featureFlags: FeatureFlagsProtocol

    public func start() -> UIViewController {
        if featureFlags.isEnabled(.newCheckoutFlow) {
            return NewCheckoutViewController(...)
        } else {
            return LegacyCheckoutViewController(...)
        }
    }
}

Project Structure

A well-organized modular project:

App/
  Features/
    Authentication/
      Package.swift
      Sources/
        Public/
          AuthenticationCoordinator.swift
          AuthDependencies.swift
          Models/
        Internal/
          LoginViewModel.swift
          LoginViewController.swift
          AuthService.swift
      Tests/
        AuthServiceTests.swift
        LoginViewModelTests.swift
    Payments/
    Profile/
  Core/
    Networking/
    Storage/
    Analytics/
  Container/
    AppDelegate.swift
    DependencyContainer.swift

Swift Package Manager Structure

Modern iOS projects use SPM for modularization:

// Package.swift for Authentication feature
let package = Package(
    name: "Authentication",
    platforms: [.iOS(.v15)],
    products: [
        .library(name: "Authentication", targets: ["Authentication"]),
    ],
    dependencies: [
        .package(path: "../Core"),
    ],
    targets: [
        .target(
            name: "Authentication",
            dependencies: ["Core"]
        ),
        .testTarget(
            name: "AuthenticationTests",
            dependencies: ["Authentication"]
        ),
    ]
)

Communication Between Features

Features should not depend on each other directly. Use these patterns:

1. Coordinator Callbacks

protocol PaymentsCoordinatorDelegate: AnyObject {
    func paymentsDidComplete(result: PaymentResult)
    func paymentsDidRequestLogin()
}

2. Shared Event Bus

enum AppEvent {
    case userLoggedIn(User)
    case userLoggedOut
    case purchaseCompleted(Product)
}

protocol EventBusProtocol {
    func publish(_ event: AppEvent)
    func subscribe<T>(_ eventType: T.Type) -> AnyPublisher<T, Never>
}

3. Deep Link Router

protocol DeepLinkHandler {
    func canHandle(_ url: URL) -> Bool
    func handle(_ url: URL) -> UIViewController?
}

// Each feature registers its handler
class AuthDeepLinkHandler: DeepLinkHandler {
    func canHandle(_ url: URL) -> Bool {
        url.path.hasPrefix("/auth")
    }
}

Common Challenges

Challenge 1: Shared UI Components

Problem: Multiple features need the same buttons, inputs, and styles.

Solution: Create a Design System module that all features depend on:

Core ← DesignSystem ← Features

Challenge 2: Circular Dependencies

Problem: Feature A needs Feature B, and Feature B needs Feature A.

Solution: Extract the shared dependency into Core, or use protocol-based abstraction:

// In Core
protocol UserProvider {
    var currentUser: User? { get }
}

// Feature A implements, Feature B consumes

Challenge 3: Versioning Complexity

Problem: Which version of Feature A works with which version of Feature B?

Solution: Semantic versioning with compatibility matrices, or monorepo with lockstep versioning.

Challenge 4: Developer Experience

Problem: Working on one feature requires understanding the entire project.

Solution:

  • Feature demo apps for isolated development
  • Comprehensive README in each feature
  • Automated dependency graph visualization

Migration Strategy

Migrating from monolith to modular architecture:

Phase 1: Establish Boundaries

  • Identify logical feature groupings
  • Document dependencies between components
  • Create module interface contracts

Phase 2: Extract Core

  • Move shared utilities to Core module
  • Establish networking, storage, analytics as separate modules
  • Define protocols for cross-cutting concerns

Phase 3: Extract Features (One at a Time)

  • Start with the most isolated feature
  • Create new module, move code, update imports
  • Verify with comprehensive testing
  • Repeat for remaining features

Phase 4: Optimize

  • Enable incremental builds
  • Set up cached artifact sharing
  • Implement feature-specific CI pipelines

Metrics for Success

Track these to ensure modularization is working:

  • Build time reduction — Target 50%+ improvement
  • Feature isolation — Zero dependencies between feature modules
  • Test coverage per feature — Each feature independently tested
  • PR cycle time — Should decrease with focused reviews
  • Developer onboarding time — New devs productive faster

Conclusion

Feature modularization is not just an architectural pattern — it's an organizational pattern. Clear module boundaries enable clear team boundaries, clear ownership, and clear accountability.

The investment in modularization pays dividends in:

  • Developer velocity
  • Code quality
  • Team autonomy
  • Release confidence

Start with your most painful feature, prove the pattern, then systematically modularize the rest.


Patterns refined through years of enterprise mobile development across multiple platforms and team sizes.

Abraham Jeyaraj

Written by Abraham Jeyaraj

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