Core Data, SwiftData, GRDB, SQLite.swift or Realm: how do you choose, and what do you give up with each?

13.11.4 · L5 · Interview

Mental model. Core Data and SwiftData are furnished flats: quick to move in, but the layout is fixed. GRDB is a bare shell with good plumbing: more work, but you decide where every wall goes.

Choose by what you need to control. Core Data is the mature object-graph framework: lazy loading, change tracking, undo, migrations, CloudKit mirroring (NSPersistentCloudKitContainer) and SwiftUI integration (@FetchRequest); the cost is a large concept surface (contexts, faults, merge policies), runtime errors from key-value coding, and awkward concurrency. SwiftData is the modern, macro-based layer on the same storage ideas, with @Model, #Predicate and tight SwiftUI and CloudKit integration and far less boilerplate; it is young (behaviour and bugs vary by OS release, fewer escape hatches, limited predicate expressiveness, migration tooling still maturing), so it suits new apps with a modern deployment target and moderate data. GRDB (and the lighter SQLite.swift) give you SQL you control: records mapped to rows, a typed query builder, migrations, FTS, observation with ValueObservation, a reader pool with WAL, and Sendable-friendly design; the cost is that you design the schema, relationships and sync yourself. Realm offers a fast, live-object database with its own file format and (formerly) sync service; weigh its maintenance status, size, and the cost of leaving a proprietary format.

Decision guide: needs CloudKit sync with minimal code and a modern target → SwiftData or Core Data; complex queries, full-text search, reporting, strict control of transactions and migrations, or a data layer shared with a server-side Swift or another platform → GRDB or plain SQLite; object graph with undo and a UI bound to results on an older target → Core Data. Whatever you pick, hide it behind a repository (Q337) with domain types, so swapping the engine is a contained project, and write migration tests with real old files. Evaluate on a spike with your worst-case data and queries (size, relationships, concurrency, migration from the previous version), on your minimum OS, and weigh the team’s skills: the best database is the one the team can operate and debug in production. Avoid building on a third-party store you cannot replace or patch if the vendor stops maintaining it.

Common traps

  • Choosing by novelty. A young framework on a low deployment target can cost more in workarounds than it saves.
  • Letting storage types leak into the UI and domain code. Switching engines then touches the whole app.

They might follow up with

  • When is plain files enough? A small, rarely changing document model, loaded entirely into memory, with atomic writes (Q339).
  • What about syncing? CloudKit mirroring is easiest with Core Data or SwiftData; with GRDB you build or buy sync (Q340).

Related questions

  • 13.11.3 Implement an LRU cache with O(1) get and set: which data structures, and what does the eviction order look…
  • 6.8.1 What is an app group, and what can go wrong when an app and an extension share a database?
  • 13.4.1 How do SwiftData’s @Model, ModelContainer, ModelContext, #Predicate and FetchDescriptor work together?
  • 13.5.1 What is the repository pattern, and why map storage models to domain models?
  • 13.7.1 How would you design multi-device sync: identity, change tracking, conflict resolution, deletions and…

← Implement an LRU cache with O(1) get and set: which data structures…