Factories, Builders, and Composition Roots in Swift
Factories, builders, and composition roots are all about creating objects, so they are easy to blur together.
That blur can make architecture conversations harder than they need to be. Someone says "factory" when they mean "composition root." Someone adds a builder because an initializer has many parameters. Someone puts concrete networking choices inside a view factory, and suddenly a feature owns decisions that should belong to the app.
The names matter only because the responsibilities matter.
#The Short Version
A composition root chooses concrete dependencies for the application.
A factory creates a feature, screen, or object graph from dependencies it has already been given.
A builder incrementally configures something that is awkward to create in one step.
Those responsibilities can overlap in small apps, but they should not be confused in large ones.
#Composition Root
The composition root is the place where the app decides which concrete implementations exist.
In a SwiftUI app, that might be the @main App type and a root object it owns.
@main
struct ShopApp: App {
private let compositionRoot = CompositionRoot()
var body: some Scene {
WindowGroup {
compositionRoot.makeRootView()
}
}
}
The composition root can create shared infrastructure:
struct CompositionRoot {
private let httpClient = URLSessionHTTPClient()
private let analytics = FirebaseAnalyticsTracker()
func makeRootView() -> some View {
ProductsFeatureFactory(
productsRepository: URLSessionProductsRepository(
httpClient: httpClient
),
analytics: analytics
)
.makeProductsView()
}
}
The important thing is that concrete choices are made at the edge. ProductsView does not decide to use URLSession. ProductsViewModel does not decide to use Firebase. The application does.
You can see this idea in more detail in The Composition Root in SwiftUI Apps and The Composition Root in UIKit Apps.
#Factory
A factory creates something.
That sounds too simple, but the simplicity is the point. A feature factory should not become a second app. It should assemble feature objects from dependencies that were already chosen.
struct ProductsFeatureFactory {
let productsRepository: ProductsRepository
let analytics: AnalyticsTracking
func makeProductsView() -> ProductsView {
let viewModel = ProductsViewModel()
let output = AnalyticsProductsOutputDecorator(
decoratee: viewModel,
analytics: analytics
)
let useCase = GetProductsUseCase(
repository: productsRepository,
output: output
)
ProductsView(
viewModel: viewModel,
onAppear: {
await useCase.execute()
}
)
}
}
This factory knows how the products feature is assembled. It does not know whether productsRepository is backed by URLSession, a local database, a cache decorator, or a test double.
That distinction keeps the feature reusable.
#Factory vs Composition Root
Here is the smell: a feature factory creates infrastructure.
struct ProductsFeatureFactory {
func makeProductsView() -> ProductsView {
let repository = URLSessionProductsRepository(
httpClient: URLSessionHTTPClient()
)
let viewModel = ProductsViewModel()
let useCase = GetProductsUseCase(
repository: repository,
output: viewModel
)
return ProductsView(
viewModel: viewModel,
onAppear: {
await useCase.execute()
}
)
}
}
This might be acceptable in a tiny app, but it does not scale well. The factory now owns a concrete networking decision. If another app, preview, or test wants the same feature with a different repository, the factory is in the way.
A better shape is to inject the concrete dependency into the factory:
struct ProductsFeatureFactory {
let repository: ProductsRepository
func makeProductsView() -> ProductsView {
let viewModel = ProductsViewModel()
let useCase = GetProductsUseCase(
repository: repository,
output: viewModel
)
ProductsView(
viewModel: viewModel,
onAppear: {
await useCase.execute()
}
)
}
}
The composition root chooses. The factory assembles.
#Builder
A builder is useful when construction has steps, optional configuration, or a final validation moment.
For example, building a request can be awkward if every option goes into one initializer.
let request = RequestBuilder(path: "/products")
.method(.get)
.header("Accept", value: "application/json")
.query("page", value: "1")
.build()
This is different from a factory. The builder is not choosing app dependencies. It is helping construct a value through a fluent or staged API.
Builders can be useful for:
- complex request values
- test data setup
- attributed strings
- layout configuration
- feature configuration with many optional values
Builders become suspicious when they hide required dependencies or make invalid states easy to build.
let view = ProductsViewBuilder()
.build()
If ProductsView cannot work without a ViewModel or an intent closure, the builder should not let us pretend otherwise. Initializers are often better because they make required data impossible to miss.
#Test Builders
Builders shine in tests when the goal is to remove irrelevant setup.
let product = ProductBuilder()
.name("Keyboard")
.price(99)
.build()
This can be much clearer than repeating every property in every test.
But test builders should still preserve meaning. If a test depends on a product being unavailable, the builder should make that explicit:
let product = ProductBuilder()
.unavailable()
.build()
The builder is there to clarify the test, not to hide the conditions that matter.
#A Useful Mental Model
Ask three questions:
Who chooses the concrete implementation?
That is the composition root.
Who assembles the feature from already-chosen dependencies?
That is the factory.
Who helps construct a complicated value or configuration?
That is the builder.
If one object answers all three questions, it may be doing too much.
#What You Should Remember
Construction code is architecture code. It decides which parts of the system know about each other.
Keep concrete choices at the app edge. Keep feature assembly near the feature. Use builders for values that are genuinely awkward to construct directly.
The goal is not to use the pattern names correctly in a diagram. The goal is to make dependency decisions easy to find, easy to replace, and hard to accidentally spread through the codebase.
Read next
Use Cases in iOS Apps: Helpful Boundary or Extra Layer?
Learn when use cases help iOS architecture, when they add noise, and how to keep them focused in Swift apps.