# The Composition Root Pattern in SwiftUI Apps

Every article in this series has ended with the same instruction: *wire everything at the Composition Root*. We've used it as the place where `RemoteFeedRepository` meets `CachingFeedRepository`, where `UserNotificationScheduler` gets injected into `ReminderViewModel`, where Decorators stack on top of each other.

But we've never stopped to examine the Composition Root itself.

This article is about that. What is a Composition Root, why does it matter, how to structure it as your app grows, and what goes wrong when you don't have one.

* * *

## What Is the Composition Root?

The Composition Root is **the single place in your application where you assemble the object graph** — connecting all concrete types together.

Mark Seemann, who coined the term, defines it as: *"a (preferably) unique location in an application where modules are composed together."*

In a SwiftUI app, the natural Composition Root is the `@main` struct. It's the entry point. It starts before any view is shown. It has no business logic of its own. It is the perfect place to wire everything together.

```swift
@main
struct FeedApp: App {
    var body: some Scene {
        WindowGroup {
            // Everything is composed here — and only here
            FeedView(
                viewModel: FeedViewModel(
                    loadFeed: LoadFeedUseCase(
                        repository: CachingFeedRepository(
                            decoratee: RemoteFeedRepository(url: feedURL),
                            cache: CodableFeedStore(storeURL: storeURL)
                        )
                    )
                )
            )
        }
    }
}
```

One place. All concrete types. All wiring. Nowhere else.

* * *

## What Goes Wrong Without One

Without a Composition Root, composition happens *everywhere*. ViewModels instantiate their own dependencies. Views create their own ViewModels. Repositories reference shared singletons. The object graph is implicit, scattered, and impossible to test.

```swift
// ❌ No Composition Root — composition scattered across the codebase

final class FeedViewModel: ObservableObject {
    // Creates its own dependency — hidden, untestable
    private let repository = RemoteFeedRepository(url: URL(string: "https://api.example.com/feed")!)
}

struct FeedView: View {
    // Creates its own ViewModel — impossible to inject a stub
    @StateObject private var viewModel = FeedViewModel()
}
```

Every class that instantiates its own dependencies is doing two jobs: its own job, and the job of the Composition Root. That's a SRP violation — and it means you can never test any of it in isolation.

* * *

## Step 1 — Extracting the Composition Root into a Factory

As the app grows, the `@main` struct becomes cluttered. Extract the wiring into a dedicated factory:

```swift
// CompositionRoot/FeedUIComposer.swift
enum FeedUIComposer {

    static func feedViewController(feedURL: URL, storeURL: URL) -> FeedView {
        let store = CodableFeedStore(storeURL: storeURL)
        let remoteRepository = RemoteFeedRepository(url: feedURL)
        let cachingRepository = CachingFeedRepository(
            decoratee: remoteRepository,
            cache: store
        )
        let useCase = LoadFeedUseCase(repository: cachingRepository)
        let viewModel = FeedViewModel(loadFeed: useCase)
        return FeedView(viewModel: viewModel)
    }
}
```

The `@main` struct becomes a clean delegation:

```swift
@main
struct FeedApp: App {
    private let feedURL = URL(string: "https://api.example.com/feed")!
    private let storeURL = FileManager.default
        .urls(for: .cachesDirectory, in: .userDomainMask)[0]
        .appendingPathComponent("feed.store")

    var body: some Scene {
        WindowGroup {
            FeedUIComposer.feedViewController(feedURL: feedURL, storeURL: storeURL)
        }
    }
}
```

The `FeedUIComposer` is the Composition Root for the feed feature. It knows about every concrete type in the feature. Nothing else does.

* * *

## Step 2 — Handling Navigation at the Composition Root

Navigation is a composition concern. A view should not know what screen comes next — it should tell a coordinator *what happened*, and the coordinator decides *where to go*.

Define the coordination interface in the Presentation layer:

```swift
// Presentation/FeedCoordinator.swift
protocol FeedCoordinator {
    func showItemDetail(_ item: FeedItem)
}
```

Implement it at the Composition Root:

```swift
// CompositionRoot/FeedCoordinatorImpl.swift
final class FeedCoordinatorImpl: FeedCoordinator {
    private let navigationPath: Binding<NavigationPath>

    init(navigationPath: Binding<NavigationPath>) {
        self.navigationPath = navigationPath
    }

    func showItemDetail(_ item: FeedItem) {
        navigationPath.wrappedValue.append(item)
    }
}
```

Wire it in the composer:

```swift
enum FeedUIComposer {

    static func feedScene(
        feedURL: URL,
        storeURL: URL,
        navigationPath: Binding<NavigationPath>
    ) -> some View {
        let coordinator = FeedCoordinatorImpl(navigationPath: navigationPath)
        let viewModel = FeedViewModel(
            loadFeed: LoadFeedUseCase(
                repository: CachingFeedRepository(
                    decoratee: RemoteFeedRepository(url: feedURL),
                    cache: CodableFeedStore(storeURL: storeURL)
                )
            ),
            coordinator: coordinator
        )
        return FeedView(viewModel: viewModel)
    }
}
```

The `FeedView` and `FeedViewModel` have no idea what `NavigationPath` is. Navigation is a plug-in, defined by a protocol and wired at the Composition Root.

* * *

## Step 3 — Composing Multiple Features

Real apps have multiple features. Each feature gets its own composer. The app-level Composition Root composes the composers:

```swift
// CompositionRoot/AppComposer.swift
struct AppComposer: View {
    @State private var navigationPath = NavigationPath()

    private let feedURL = URL(string: "https://api.example.com/feed")!
    private let storeURL = FileManager.default
        .urls(for: .cachesDirectory, in: .userDomainMask)[0]
        .appendingPathComponent("feed.store")

    var body: some View {
        NavigationStack(path: $navigationPath) {
            FeedUIComposer.feedScene(
                feedURL: feedURL,
                storeURL: storeURL,
                navigationPath: $navigationPath
            )
            .navigationDestination(for: FeedItem.self) { item in
                ItemDetailUIComposer.detailScene(item: item)
            }
        }
    }
}
```

Each feature's internal wiring is invisible to the others. The app-level composer only knows about the *entry point* of each feature — not its internals.

* * *

## The Composition Root Is the Only Place That Can

The Composition Root has a unique privilege: **it is the only place allowed to reference concrete types from multiple layers simultaneously**.

| Allowed where | Domain layer | Data layer | Presentation layer | Composition Root |
| --- | --- | --- | --- | --- |
| Reference `FeedRepository` (protocol) | ✅ | ✅ | ✅ | ✅ |
| Reference `RemoteFeedRepository` (concrete) | ❌ | ✅ | ❌ | ✅ |
| Reference `CachingFeedRepository` (concrete) | ❌ | ✅ | ❌ | ✅ |
| Reference `FeedViewModel` (concrete) | ❌ | ❌ | ✅ | ✅ |
| Instantiate and connect all of the above | ❌ | ❌ | ❌ | ✅ |

If you find yourself instantiating a `RemoteFeedRepository` inside a ViewModel, or creating a `FeedViewModel` inside another ViewModel, something has leaked out of the Composition Root. Bring it back.

* * *

## Testing the Composition Root

The Composition Root itself is hard to unit test — and that's fine. Its job is wiring, not logic. What you can test is that the object graph it produces **behaves correctly end-to-end**:

```swift
final class FeedUIComposerIntegrationTests: XCTestCase {

    func test_feedScene_rendersLoadedItems() async throws {
        let items = [FeedItem(id: UUID(), title: "Article", imageURL: anyURL())]
        let store = InMemoryFeedStore(stubbedItems: items)

        let viewModel = FeedViewModel(
            loadFeed: LoadFeedUseCase(
                repository: CachingFeedRepository(
                    decoratee: AlwaysFailingRepository(),
                    cache: store
                )
            ),
            coordinator: NullCoordinator()
        )

        await viewModel.onAppear()

        XCTAssertEqual(viewModel.items, items)
    }

    private func anyURL() -> URL { URL(string: "https://any-url.com")! }
}
```

Integration tests at the composition boundary verify the wiring produces the expected behaviour — without hitting a real network or real file system.

* * *

## Everything Comes Together

Looking back at the full series, the Composition Root is the thread connecting every pattern:

| Pattern | What it contributes to the Composition Root |
| --- | --- |
| Dependency Injection | All dependencies are injected — the Root is where they're created |
| Clean Architecture | The Root is the only place that references all three layers |
| Decorator Pattern | Decorators are stacked here — nowhere else |
| Single Responsibility | The Root's only job is composition — no business logic |
| TDD Cache Layer | `CodableFeedStore` is instantiated here |
| Dependency Inversion | Concrete types implement protocols; the Root connects them |
| Snapshot Testing | Tests stub the graph the Root would produce |

Every principle and pattern ultimately serves the same goal: **keep each module unaware of how it's assembled**, so the Composition Root can assemble it any way the current context demands — production, tests, previews, or a different product entirely.

* * *

## What You Gain

| Without Composition Root | With Composition Root |
| --- | --- |
| Object graph assembled everywhere | Assembled in exactly one place |
| Changing infrastructure touches many files | One file changes |
| Testing requires unwinding hidden dependencies | Inject any configuration from tests |
| Features coupled through shared singletons | Features communicate through protocols |
| Adding a Decorator requires touching business logic | Stack it at the Root, touch nothing else |

* * *

## Summary

The Composition Root is not a framework feature or a library — it's a discipline. One place, one job: assembling the object graph.

In SwiftUI:

1.  The `@main` **struct** is the natural Composition Root.
    
2.  Extract feature wiring into a **UIComposer** per feature as the app grows.
    
3.  Wire **navigation** through a coordinator protocol — implement it at the Root.
    
4.  Compose **multiple features** by having the app-level Root combine feature-level Roots.
    
5.  **Integration test** the wiring by constructing the graph with controlled collaborators.
    

A codebase with a clear Composition Root is a codebase where every module is independently testable, every infrastructure choice is replaceable, and every new feature is added by extension — not by modification.

That is the promise of every principle in this series, fulfilled in one place.
