← Knowledge hub
Architecture · ORIGINAL STUDY NOTE

iOS design patterns: Factory, Builder, Coordinator & Strategy

What these patterns solve, Swift examples, trade-offs and why Singleton does not replace dependency injection.

Start with the problem

A pattern is a vocabulary for recurring trade-offs, not a requirement for a 'senior' codebase. Use the simplest structure that keeps responsibilities clear. Factory builds objects; Builder assembles complex configuration; Coordinator owns navigation; Strategy swaps behaviors; Singleton exposes one shared instance.

Factory — isolate object construction

Factory creates a feature from dependencies
struct AccountsViewModel {
    let repository: any AccountsRepository
}
protocol AccountsRepository { }
struct LiveRepository: AccountsRepository { }
enum AccountsFactory {
    static func makeLiveViewModel() -> AccountsViewModel {
        AccountsViewModel(repository: LiveRepository())
    }
}

Factory trade-off: a simple factory method is often enough. Avoid a sprawling factory hierarchy; construction logic is still code that needs tests.

Builder — construct a complex configuration step by step

Builder with a safe final product
import Foundation
struct RequestConfiguration {
    let baseURL: URL
    let timeout: TimeInterval
    let cacheEnabled: Bool
}
struct RequestBuilder {
    let baseURL: URL
    var timeout: TimeInterval = 30
    var cacheEnabled = false
    func build() -> RequestConfiguration {
        RequestConfiguration(baseURL: baseURL, timeout: timeout,
                             cacheEnabled: cacheEnabled)
    }
}

Builder trade-off: don't use it for two simple initializer parameters. Use it when optional combinations or incremental construction genuinely improve readability.

Coordinator — move navigation decisions out of feature views

Minimal navigation coordinator model
import SwiftUI
@MainActor @Observable
final class AppCoordinator {
    var path: [Route] = []
    enum Route: Hashable { case account(String), settings }
    func showAccount(id: String) { path.append(.account(id)) }
    func goBack() { if !path.isEmpty { path.removeLast() } }
}
// Bind path to NavigationStack(path:); destinations map Route to views.

Coordinator trade-off: useful when multi-step flows and deep links grow; unnecessary for a single small screen. NavigationStack can already manage simple flows without an extra abstraction.

Strategy — encapsulate alternative behavior

Policy variation without switches everywhere
protocol FeeStrategy {
    func fee(for amount: Decimal) -> Decimal
}
struct FlatFee: FeeStrategy {
    func fee(for amount: Decimal) -> Decimal { 2 }
}
struct PercentageFee: FeeStrategy {
    func fee(for amount: Decimal) -> Decimal { amount * 0.02 }
}
struct Checkout {
    let fees: any FeeStrategy
    func total(_ amount: Decimal) -> Decimal {
        amount + fees.fee(for: amount)
    }
}

Singleton — not a DI substitute

A static shared instance may be suitable for immutable process-wide infrastructure, but globally mutable state obscures dependencies and can make tests depend on execution order. Prefer injecting the required collaborator explicitly.

Pattern selection
PatternQuestion it answersAvoid when
FactoryWho creates the feature?One trivial initializer
BuilderHow are many options composed?No configuration complexity
CoordinatorWho owns navigation decisions?A tiny self-contained view
StrategyHow do we swap an algorithm?Only one fixed behavior
SingletonIs one process-wide instance required?Convenience hides mutable state
SOURCES & DOCUMENTATION

Personal study notes. Check details against your target Swift/iOS version and official documentation.