← Knowledge hub
Architecture · ORIGINAL STUDY NOTE

SOLID principles in Swift — practical review

Five design principles, each with an interview-ready explanation and a concrete Swift-oriented example.

S — Single Responsibility

A type or module should have one reason to change. Keep networking, persistence and presentation responsibilities in separate components, instead of combining everything in one ViewModel.

S — Keep fetching separate from presentation
protocol OrderReading {
    func loadOrders() async throws -> [Order]
}

struct LoadOrdersUseCase {
    let repository: any OrderReading

    func execute() async throws -> [Order] {
        try await repository.loadOrders()
    }
}

O — Open/Closed

Extend behavior through protocols and new implementations without repeatedly changing stable code. PaymentMethod implementations should not require a growing switch for each provider.

O — Introduce a new implementation instead of a growing switch
protocol PaymentMethod {
    func charge(_ amount: Decimal) async throws
}

struct CardMethod: PaymentMethod {
    func charge(_ amount: Decimal) async throws {
        // Charge using the card provider.
    }
}

struct TransferMethod: PaymentMethod {
    func charge(_ amount: Decimal) async throws {
        // Charge using bank transfer.
    }
}

L — Liskov Substitution

A substitute implementation must honor the contract's observable behavior. A mock repository that silently converts an error into an empty list may break a caller's expectations.

LSP: mocks must preserve the error contract
struct Order { let id: String }
protocol OrderReading {
    func load() async throws -> [Order]
}
enum OrderError: Error { case unavailable }
struct FailingOrders: OrderReading {
    func load() async throws -> [Order] {
        throw OrderError.unavailable
    }
}
// Returning [] instead would hide an error expected by callers.

I — Interface Segregation

Favor small interfaces tailored to consumers. Splitting an OrderReading protocol from OrderWriting avoids forcing callers to depend on unused functions.

ISP: small, focused consumer interfaces
struct Order { let id: String }
protocol OrderReading {
    func loadOrders() async throws -> [Order]
}
protocol OrderWriting {
    func save(_ order: Order) async throws
}
struct OrderList {
    let reader: any OrderReading
}

D — Dependency Inversion

High-level application logic should depend on abstractions rather than concrete infrastructure. A use case accepts an OrderReading protocol and resolves the implementation at the composition root.

DIP: high-level code depends on an abstraction
struct Order { let id: String }
protocol OrderReading {
    func loadOrders() async throws -> [Order]
}
struct LoadOrdersUseCase {
    let repository: any OrderReading
    func execute() async throws -> [Order] {
        try await repository.loadOrders()
    }
}
SOLID at a glance
PrincipleKey questionPractical hint
SRPHow many reasons to change?Separate API, persistence and UI.
OCPCan behavior be added without editing stable logic?Add protocol-conforming strategies.
LSPCan an implementation replace another safely?Preserve success and error semantics.
ISPDoes the caller depend on unused methods?Use focused protocols.
DIPDoes domain logic know concrete infrastructure?Inject abstractions at boundaries.

Trade-off: protocols are useful for stable boundaries and testing, but a protocol for every concrete implementation adds indirection without necessarily improving maintainability.

SOURCES & DOCUMENTATION

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