top of page

CFA Reference: Repositories and Persistence

Formal CFA AI Toolkit for iOS Documentation · Reference 6 of 26

A repository is the only code that knows how data is stored or fetched. The Feature depends on its protocol, written in domain language, and never on SwiftData, URLSession or file paths.

Definitions

  • Repository protocol: A Sendable protocol whose methods use domain values, for example fetchHabits() async throws -> [Habit].
  • SwiftDataManager: An actor that opens its ModelContainer lazily and routes all SwiftData access. In Habit Tracker it conforms to HabitsRepository.
  • @ModelActor repository: The actor that performs non-trivial SwiftData work on its own model context (HabitRepository in Habit Tracker).
  • NetworkingManager: The single route for URLSession requests.
  • JSON file cache: A file-based cache for remote snapshots, such as RepoFileCache in the CFA App template.

Rules

  1. Choose the simplest single persistence route: a JSON cache for remote snapshots, SwiftData only for structured local data (change gate rule 13).
  2. Make a non-trivial SwiftData repository a @ModelActor (rule 10).
  3. Route SwiftData through SwiftDataManager and URLSession through NetworkingManager (rule 15).
  4. Never let database availability gate the UI (rule 18). Open storage lazily and report failures to the Feature.
  5. Never swallow a persistence failure. Throw it so the Feature can publish an honest error.
  6. Keep domain types out of storage folders; persisted model types, such as StoredHabit, are separate from domain values, such as Habit.

Decision rules

  • When the data is a downloaded snapshot that can be fetched again, use a JSON file cache.
  • When the data is created by the user and has structure or relationships, use SwiftData behind SwiftDataManager.
  • When the store might fail to open, let the first request throw and let the Feature show a recoverable error, rather than blocking launch.
  • When a test needs storage, pass an in-memory implementation of the same protocol.

Example

The compiled repository below is a Sendable protocol with an actor implementation. The actor owns its storage, so no locks are needed.

Compiled example (excerpt)

import Foundation
import Observation

// MARK: Repository contract in domain language
protocol HabitsRepository: Sendable {
    func fetchHabits() async throws -> [Habit]
    func insertHabit(_ habit: Habit) async throws
}

// MARK: Actor-isolated implementation
actor LocalHabitsRepository: HabitsRepository {
    private var storedHabits: [Habit] = []

    func fetchHabits() async throws -> [Habit] {
        storedHabits
    }

    func insertHabit(_ habit: Habit) async throws {
        storedHabits.append(habit)
    }
}

// Habit is defined in Reference 5.

Compiled with Swift 6 language mode and warnings treated as errors: type-checked on Swift 6.2 and built on Swift 6.4 (Linux toolchains, Foundation and Observation only).

In Habit Tracker, the SwiftData route opens its container on first use, so a storage problem reaches the requesting Feature instead of stopping launch:

Repository excerpt, shown for reading. It uses SwiftUI or SwiftData, so it was not compiled for this page. Abridged from Habit Tracker's SwiftDataManager.swift.

actor SwiftDataManager: HabitsRepository {
    private let configuration: ModelConfiguration
    private var habitRepository: HabitRepository?

    private func repository() throws -> HabitRepository {
        if let habitRepository { return habitRepository }
        // Directory failures must reach the requesting feature before SwiftData opens.
        let container = try ModelContainer(
            for: StoredHabit.self, StoredHabitEntry.self,
            configurations: configuration
        )
        let repository = HabitRepository(modelContainer: container)
        habitRepository = repository
        return repository
    }
}

Common mistakes

  • Opening the ModelContainer in the App initialiser and crashing or showing a blank screen when it fails.
  • Calling URLSession.shared directly from a Feature instead of through NetworkingManager.
  • Using SwiftData to cache a remote response that a JSON file would hold more simply.
  • Using a class with a lock instead of an actor for shared mutable storage.
  • Catching a save error and doing nothing.

Links

bottom of page