Why should the object graph not be built eagerly at launch, and how do lazy factories keep the composition root cheap?
Predict first
What does each root build at launch, and what after opening the search feature twice?
nonisolated(unsafe) var built: [String] = []
final class Feature { let name: String; init(_ n: String) { name = n; built.append(n) } }
final class EagerRoot {
let search = Feature("search"), profile = Feature("profile"), checkout = Feature("checkout")
}
final class LazyRoot {
private let makeSearch = { Feature("search") }
private let makeProfile = { Feature("profile") }
private let makeCheckout = { Feature("checkout") }
lazy var search = makeSearch()
lazy var profile = makeProfile()
lazy var checkout = makeCheckout()
}
_ = EagerRoot()
print("eager root built at launch:", built); built = []
let lazyRoot = LazyRoot()
print("lazy root built at launch:", built)
_ = lazyRoot.search; _ = lazyRoot.search
print("after opening search twice:", built)
- eager: all three / lazy at launch: none / after opening search twice: just search, once
- Both build everything at launch
- The lazy root builds search twice
Mental model. A restaurant that cooks every dish on the menu before opening wastes time and food. A kitchen that preps the basics and cooks to order opens sooner, though the first order of an elaborate dish takes a bit longer.
A composition root that constructs every feature’s dependencies in its initialiser runs all that work before the first frame: databases opened, SDKs configured, view models allocated for screens the user may never visit. Launch time is a product metric and a review gate, and the work is also a source of ordering bugs. In the check an eager root builds ["search", "profile", "checkout"] at launch; a lazy root builds nothing at launch and creates search only on first use, exactly once even if it is asked twice (a lazy var backed by a factory closure). The pattern: the root stores factories (closures or small builder types), not instances; a feature is created when the user navigates to it; heavy infrastructure (database, image cache) hides behind a lazily created holder; and anything that must run at launch (crash reporter, flag fetch) is listed explicitly instead of happening as a side effect of construction.
Trade-offs and traps. Laziness moves cost to first use, so the first tap on a heavy feature can hitch; warm things up in the background after launch if that matters, using a prioritised startup task list rather than initialisers. Lazy singletons can hide cycles: A needs B lazily and B needs A lazily compiles fine and recurses at run time; keep the graph acyclic and break the cycle with a protocol or an event rather than a lazy reference. A lazy var is not thread safe in Swift, so a root accessed from several threads needs an actor, a lock, or main-actor isolation. Prefer explicit factories to a reflection-based service locator: an unresolved dependency should fail at compile time or in the first test, not when a user opens a screen. And measure: Instruments’ App Launch template and signposts around the root’s creation show how much the eager version really costs before you restructure.
A lazy var runs its factory once on first access.
Common traps
- Hiding startup work in initialisers or static properties. It runs in an unpredictable order; list launch tasks explicitly.
- Assuming
lazy varis thread safe. It is not; guard access or confine it to one actor.
They might follow up with
- What belongs in the launch path? Only what the first screen and crash reporting need.
- How do I find launch cost? Instruments (App Launch), MetricKit launch histograms, and signposts around the root.
Related questions
- 15.8.3 How do you scope dependencies by lifetime (app, session, screen), and what leaks when a longer-lived object…
- 15.2.2 How do you do dependency injection in Swift, and when do you need a container or framework?
- 15.3.1 What is a feature module, how do you draw module boundaries, and how does access control enforce them?
← How do you scope dependencies by lifetime (app, session, screen), and…