Introducing the Composition Root Pattern in a Swift Codebase

April 24, 2023

In early 2022, I explored different approaches to dependency injection in Swift and came across the Composition Root pattern introduced by Mark Seemann in Dependency Injection in .NET. Since then, I have introduced the Composition Root pattern in several Swift codebases and have found it to be very useful.

In this post, we will explore how to introduce a Composition Root in a Swift codebase and use it in an iOS project. But first, let's have a brief overview of dependency injection.

Introduction to Dependency Injection

Dependency Injection is a design pattern in which dependent objects are passed to objects from an external source rather than having objects create their dependencies themselves.

Consider the following MovieService type that loads movies and stores them in a MovieRepository.

struct MovieRepository {
  func add(_ movie: Movie) {
    ...
  }
}

struct MovieService {
  let repository = MovieRepository()
  
  func loadMovies() {
    ...
  }
}

The MovieService has a dependency on MovieRepository which it creates upon initialization. This tight coupling between the two types can pose a problem when unit testing our code. For example, MovieRepository could store the movies on disk, which may not be desirable in unit tests.

We can modify the code to decouple the components like this:

protocol MovieRepository {
  func add(_ movie: Movie)
}

struct DiskMovieRepository: MovieRepository {
  func add(_ movie: Movie) {
    ...
  }
}

struct MovieService {
  let repository: MovieRepository

  func loadMovies() {
    ...
  }
}

We have made three changes to the codebase:

  1. Added the MovieRepository protocol.
  2. Renamed the previous implementation of MovieRepository to DiskMovieRepository, which now conforms to the MovieRepository protocol.
  3. MovieService now depends on the MovieRepository protocol rather than a concrete implementation.

With those changes, we can use our MovieService as shown below:

let repository = DiskMovieRepository()
let service = MovieService(repository: repository)

The repository is now being injected into the service through the initializer. This makes it clear which dependencies the MovieService has and makes it easy for us to swap out the implementation of the repository to accommodate different needs. For example, we may want to use a repository that stores the movies in memory when running our unit tests.

In this post, we are focusing on Initializer Injection, which means injecting dependencies through the initializer. It's worth noting that there are other types of dependency injection, namely Method Injection and Property Injection.

Building the Dependency Graph

When injecting dependencies through initializers, we essentially create a graph of objects where each node represents a dependency. Let us expand on our previous example to demonstrate this.

class WatchlistViewController: UIViewController {
  init(userService: UserService, movieService: MovieService) {
    ...
  }
}

struct UserService {
  let networkClient: NetworkClient
}

struct MovieService {
  let repository: MovieRepository
  let networkClient: NetworkClient
}

We are dealing with an object graph here since WatchlistViewController has two dependencies which in turn have their own dependencies. The graph can be illustrated as shown below.

As we require dependencies to be injected through the initializer, we depend on an external source to create those dependencies but that external source may have dependencies as well and as such, the graph of dependencies grows and the question becomes:

How do we construct this graph and where in our codebase do we do it?

There are plenty of open-source Swift frameworks that address these questions. Some of these frameworks use a variant of Dependency Injection that may result in runtime errors if we forget to provide a dependency at compile-time. In fact, SwiftUI's @EnvironmentObject may exhibit the same behavior.

Consider the following SwiftUI view:

struct ContentView: View {
  @EnvironmentObject var movieService: MovieService
  
  var body: some View {
    Text("\(movieService.count) movies")
  }
}

ContentView has a dependency on MovieService which should be injected using the .environmentObject() view modifier on one of the view's ancestors. However, if we forget to do so, we get the following error on runtime:

Fatal error: No ObservableObject of type MovieService found. A View.environmentObject(_:) for MovieService may be missing as an ancestor of this view

This runtime error and subsequent app crash is a consequence of a poorly designed dependency injection strategy that we should avoid. Instead, we should aim to design our code in a way that prioritizes compile-time errors over runtime errors.

In addition, selecting a dependency injection strategy is an architectural decision, and I prefer not to rely on third-party frameworks for this purpose as it may create strong dependencies on external frameworks in my codebase.

The Composition Root Pattern Enters the Room

Mark Seemann proposes the use of a Composition Root to answer the question of where to compose the object graph in their book Dependency Injection in .NET. Do not be intimidated by the ".NET" in the title of the book, as we are here to adapt the pattern to Swift 😊.

The Composition Root is responsible for creating all dependencies that are injected into components. We can achieve this by creating an enum named CompositionRoot and listing each dependency as a static property. Whenever an object has a dependency, we can reference the corresponding property.

enum CompositionRoot {
  static var watchlistViewController: WatchlistViewController {
    WatchlistViewController(
      userService: userService,
      movieService: movieService
    )
  }

  private static var userService: UserService {
    UserService(networkClient: networkClient)
  }
  
  private static var movieService: MovieService {
    MovieService(
      repository: movieRepository, 
      networkClient: networkClient
    )
  }
  
  private static var movieRepository: MovieRepository {
    DiskMovieRepository()
  }
  
  private static var networkClient: NetworkClient {
    NetworkClient()
  }
}

The CompositionRoot enum expresses an object graph with the entry point being the enum itself.

This Composition Root is quite small and if you would like to see an example of a Composition Root being used in production, then have a look at this Composition Root in an app that we have developed at Shape.

Now that we have our CompositionRoot in place and have specified how our object graph is composed, we can use our CompositionRoot type to create instances of our objects and wire up their dependencies.

Using the Composition Root in an iOS App

According to Seemann, the object graph should be composed as close to the application's entry point as possible. This is a natural consequence of objects depending on an external source to provide dependencies through an initializer.

In an iOS app, we can regard the AppDelegate and our scene delegates as the application's entry point. That is, we may not have a single entry point because each scene delegate is, in itself, an entry point.

Let us assume that we want a scene that shows an instance of WatchlistViewController. Most often, the root view controller of a scene is an instance of a UINavigationController, so we will modify the CompositionRoot to expose an instance of UINavigationController.

enum CompositionRoot {
  static var rootViewController: UIViewController {
    UINavigationController(
      rootViewController: watchlistViewController
    )
  }

  ...
}

With CompositionRoot exposing our root view controller, all that is left to do is referencing it's rootViewController property in our scene delegate.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  func scene(
    _ scene: UIScene, 
    willConnectTo session: UISceneSession, 
    options connectionOptions: UIScene.ConnectionOptions
  ) {
    let windowScene = scene as! UIWindowScene
    window = UIWindow(windowScene: windowScene)
    window?.rootViewController = CompositionRoot.rootViewController
    window?.makeKeyAndVisible()
  }
}

Sharing State Between Objects

Our CompositionRoot creates new instances every time a dependency is referenced. By examining the code below, we notice that the services returned by the userService and movieService properties each have their own instance of NetworkClient. This is because the networkClient is implemented with a getter only that creates a new instance when accessed.

enum CompositionRoot {
  ...
  
  private static var userService: UserService {
    UserService(networkClient: networkClient)
  }
  
  private static var movieService: MovieService {
    MovieService(
      repository: movieRepository, 
      networkClient: networkClient
    )
  }
  
  private static var networkClient: NetworkClient {
    NetworkClient()
  }

  ...
}

This behavior is desirable in most cases as it allows our objects to be ephemeral. We create them when needed and discard them as soon as possible. However, this approach does not work when we need to share state between two dependencies.

Let's imagine a scenario in which we have custom logic for queuing network requests and need our objects to share an instance of NetworkClient. In that case, we will change the networkClient property in our CompositionRoot to create an instance and store it so that whenever the property is referenced, it returns the same instance. Fortunately, this is as simple as making networkClient a constant.

enum CompositionRoot {
  ...
  
  private static let networkClient = NetworkClient()
  
  ...
}

Handling a Growing Composition Root

One of the main concerns I have heard from people when discussing this adaptation of the Composition Root pattern is that the CompositionRoot enum may become too large over time. This is a valid concern, and to address it, we can split it up into separate files, each of which adds an extension to the CompositionRoot. Typically, these extensions will cover the part of the object graph that relates to a specific domain or feature of our app.

For example, we can create an extension on the CompositionRoot that adds the Movies enum, which holds all objects related to the movies domain of our app.

/// 📄 CompositionRoot+Movies.swift
extension CompositionRoot {
  enum Movies {
    func moviesService(networkClient: NetworkClient) {
      MovieService(
        repository: movieRepository, 
        networkClient: networkClient
      )
    }
    
    private static var movieRepository: MovieRepository {
      DiskMovieRepository()
    }
  }
}

Objects should still use the CompositionRoot directly, so we want it to expose MovieService and use the shared instance of NetworkClient.

/// 📄 CompositionRoot.swift
enum CompositionRoot {
  ...
  
  var movieService: MovieService {
    Movies.movieService(networkClient: networkClient)
  }

  private static let networkClient = NetworkClient()
}

Closing Thoughts

Since early 2022, I have introduced the Composition Root pattern in most of my side projects, and we have also introduced it in a few projects at my day job at Shape.

By introducing dependency injection and the Composition Root pattern, components become less tightly coupled and are therefore easier to unit test. For example, to unit test our MovieService, we only need to create a mock implementation of MovieRepository and pass that when initializing our MovieService.

Furthermore, if we move our components into their own Swift packages, maybe using the strategy outlined by Medium, we can make our host apps extremely thin. After introducing the Composition Root pattern in my projects, I have found that the main target usually contains close to nothing but the app delegate, a scene delegate, and a Composition Root.

The fact that the main target contains very little code means that we have a great setup for introducing Dev Apps into the project. Dev Apps, originally presented by Airbnb, are apps that developers use to iterate on a specific feature during development. These apps make it faster to iterate during development as they launch into a specific part of the app and contain less code than the main target, which decreases their compile times. Building Dev Apps becomes as simple as adding a new target to your Xcode project, possibly using XcodeGen, and adding a Composition Root that composes the app.

If this post has made you curious about introducing a Composition Root in a Swift codebase, you can take a look at the CompositionRoot.swift file in the Tartelet app, an app that I worked on during my day job and which we recently open-sourced.