Skip to content

Architecture: Clean Architecture

How the toolkit applies Clean Architecture to iOS. The companion skill is skills/architecture/ios/clean_architecture.md; the rules are in standards/architecture_standards.md.

Overview

Three layers with dependencies pointing inward. Inner layers know nothing about outer ones; outer layers depend on inner abstractions (Dependency Inversion).

The dependency arrows only point toward Domain. RepositoryImpl (Data) conforms to the RepositoryProtocol (Domain) — the only link from Data into Domain, and it's via abstraction.

Data Flow (read path)

Dependency Rule (enforced)

  • Domain: pure Swift, no framework imports.
  • Data depends on Domain (implements its protocols); never the reverse.
  • Presentation depends on Domain (use cases); never on Data directly.
  • DTOs stay in Data.

Sample Implementation

swift
// Domain
struct Account: Equatable { let id: String; let name: String; let balanceCents: Int }
protocol AccountRepository { func accounts() async throws -> [Account] }
struct FetchAccountsUseCase {
    let repository: AccountRepository
    func execute() async throws -> [Account] {
        try await repository.accounts().sorted { $0.balanceCents > $1.balanceCents }
    }
}

// Data
struct AccountDTO: Decodable { let id: String; let name: String; let balance_cents: Int }
final class RemoteAccountRepository: AccountRepository {
    let client: APIClient
    init(client: APIClient) { self.client = client }
    func accounts() async throws -> [Account] {
        try await client.send(Endpoint<[AccountDTO]>(path: "/accounts"))
            .map { Account(id: $0.id, name: $0.name, balanceCents: $0.balance_cents) }
    }
}

// Presentation (see skills/architecture/ios/mvvm.md for the full ViewModel)

Composition Root

swift
enum AppContainer {
    static func makeAccountsViewModel() -> AccountsViewModel {
        let client = LiveAPIClient(baseURL: .api)
        let repo = RemoteAccountRepository(client: client)
        return AccountsViewModel(fetchAccounts: FetchAccountsUseCase(repository: repo))
    }
}

When to simplify

For a tiny app, collapsing use cases into the repository is acceptable — but keep the layer boundaries and the dependency rule. Add use cases when business logic grows.