Skip to content
Prepare to Grow

iOS Architecture

Separation of Concerns and Dependency Direction

Create boundaries that keep policy independent from UI frameworks, storage, networking, and other implementation details.

Advanced6 min read2 guided activities

Before you begin

What you will understand

  • Separate policy from framework and infrastructure details.
  • Recognize high-level and low-level responsibilities.
  • Reverse dependencies with small Swift protocols.
  • Place abstractions near the code whose needs they express.
01

Learning stage

Concept Explanation

Separation of concerns gives components focused reasons to change. A view formats and presents information, a use case applies a business decision, and a repository obtains or stores data. Separation is valuable when these concerns change independently; splitting two lines into separate types without such pressure only adds navigation cost.

Dependency direction determines which decisions are protected. High-level policy should not require knowledge of URLSession, UserDefaults, or a particular view controller. Instead, policy states the capability it needs through a narrow protocol, and the application composition root supplies a concrete implementation.

A useful protocol describes a consumer's requirement rather than copying every method from an existing service. Prefer capability-shaped interfaces such as CourseFetching or ProgressSaving. Keep boundaries small, pass domain values across them, and decide deliberately whether failures, cancellation, and actor isolation are part of the contract.

  • Policy: a business or application decision.
  • Detail: a framework, transport, database, or UI implementation.
  • Dependency inversion: high-level code owns an abstraction implemented by lower-level code.
  • Capability protocol: narrow interface describing what a consumer needs.
  • Composition root: constructs concrete implementations and connects them.
02

Learning stage

Syntax & Examples

The use case depends on a capability it owns, while an API implementation handles transport details elsewhere.

CompleteLesson.swiftswift
struct LessonProgress: Equatable {
  let lessonID: UUID
  let completedAt: Date
}

protocol ProgressSaving: Sendable {
  func save(_ progress: LessonProgress) async throws
}

struct CompleteLesson: Sendable {
  private let progressStore: any ProgressSaving
  private let now: @Sendable () -> Date

  init(progressStore: any ProgressSaving,
       now: @escaping @Sendable () -> Date = Date.init) {
    self.progressStore = progressStore
    self.now = now
  }

  func callAsFunction(lessonID: UUID) async throws {
    try await progressStore.save(
      LessonProgress(lessonID: lessonID, completedAt: now())
    )
  }
}

struct APIProgressStore: ProgressSaving {
  let client: APIClient
  func save(_ progress: LessonProgress) async throws {
    try await client.send(ProgressRequest(progress: progress))
  }
}
The use case knows the domain operation but not the request format, transport, or endpoint.
03

Learning stage

Real-World Usage

Connect the architectural concept to maintainable iOS features and real change scenarios.

  • Replace remote storage with a local implementation in tests.
  • Keep URL and decoding changes out of business workflows.
  • Reuse a completion rule from UIKit, SwiftUI, widgets, or background work.
  • Introduce caching without changing every caller.
04

Learning stage

Common Mistakes

Identify abstractions and dependencies that add coupling, ambiguity, or unnecessary ceremony.

  • Creating one enormous service protocol shared by unrelated consumers.
  • Returning URLResponse or database records through a domain boundary.
  • Placing every protocol in a generic global Interfaces folder.
  • Adding a repository that merely renames an API client without adding a boundary.
  • Ignoring concurrency and failure semantics in an asynchronous contract.
05

Learning stage

Practice Exercises

Apply the design to a small feature and evaluate it through observable change scenarios.

Activity 1

Invert a concrete dependency

Refactor a view model that creates URLSession directly. Define the smallest capability it needs and inject a live and in-memory implementation.

Hint: Write the consumer's required method first; do not mirror the entire network client.

Activity 2

Review boundary leakage

Inspect models crossing your layers and identify framework, transport, persistence, or UI details that leak into policy code.

Hint: Ask whether the high-level decision could run in a command-line test target.

06

Learning stage

Knowledge Check

Answer each question before revealing the explanation.

QUESTION 1

In dependency inversion, what should high-level policy depend on?

  1. A.An abstraction expressing the capability it needs
  2. B.A concrete URLSession singleton
  3. C.A view controller subclass
  4. D.A database table name
Reveal answer

An abstraction expressing the capability it needs

The lower-level implementation then conforms to the abstraction, protecting policy from implementation changes.

QUESTION 2

Why prefer a small consumer-shaped protocol?

Reveal answer

It exposes only the required capability, reducing coupling and making replacements easier to create.

A copied service interface forces consumers and tests to know unrelated operations.

07

Learning stage

Interview Preparation

Explain the decision, dependency direction, trade-offs, and evidence—not only the pattern name.

How do you apply dependency inversion in Swift?

Short answer: Let high-level code define a narrow protocol for the capability it needs, then inject a concrete implementation at the composition root.

Explore the detailed answer

I identify the policy I want to protect, design an interface in its vocabulary, and keep transport or storage models behind the adapter. Swift protocols, closures, or generic parameters can express the seam. I include failure and concurrency semantics and avoid abstractions that have only one caller and no meaningful change pressure.

Possible follow-ups

  • • Where should the protocol live?
  • • When is a closure enough instead of a protocol?
08

Learning stage

Key Takeaways

Separation protects independently changing concerns, and dependency inversion lets policy define what it needs without importing implementation details.

  • Separate by reasons to change.
  • Keep policy independent from frameworks where it matters.
  • Let consumers shape narrow capabilities.
  • Translate implementation models at boundaries.
  • Assemble concrete dependencies in one visible place.