Persistence: Choosing the Right Storage Tool

Survey every persistence option available in Objective-C and Cocoa — UserDefaults, property lists, files, Keychain, SQLite, Core Data, serialization, migration, and caching — and build a decision framework for when each one is the right tool.

Every non-trivial app needs to remember something after it quits: a user's preferences, a login token, a downloaded article, or a whole database of records. Objective-C and Cocoa give you a surprisingly wide toolbox for this, ranging from a single boolean flag saved in a property list to a fully relational SQLite database. The mistake almost every beginner makes is picking the tool they already know — usually UserDefaults — and stretching it to do a job it was never designed for. This lesson is a map of the whole territory. It will not make you an expert in any single storage mechanism; instead it introduces each one at a conceptual level, explains what makes it different from its neighbors, and gives you a decision framework so that when you sit down to persist real data, you reach for the right tool on the first try. UserDefaults and Keychain get their own dedicated lessons immediately after this one, since they are the two you will use constantly. SQLite and Core Data get their own dedicated lessons later in the course, once you have more foundation to build on. Files themselves were already covered in the earlier Data and File System lesson, so here we only place them on the map rather than re-teach them.

Why 'just save it somewhere' is not a strategy

Persistence sounds like a single problem — 'keep this data around after the app closes' — but it is really a family of different problems that happen to share a verb. Saving a user's font-size preference, saving their OAuth access token, saving ten thousand rows of downloaded product data, and saving a half-finished document draft are four completely different engineering problems with different requirements for size, security, structure, and durability. Treating them as the same problem and solving all four with the same mechanism is where most persistence bugs and security incidents come from.

Cocoa's storage APIs exist precisely because Apple's engineers ran into these different requirements over decades of building macOS and iOS apps and built a purpose-fit tool for each shape of problem. Learning persistence in Objective-C is therefore less about memorizing method signatures and more about learning to ask the right diagnostic questions before you write a single line of storage code: How big is this data? Is it a secret? Does it have relationships to other data? Do I need to search or filter it? Will its shape change over time? The answers to those questions point you almost mechanically at the correct API.

This lesson walks through each tool in turn, always from the angle of 'what problem was this actually built to solve', and closes with an explicit decision framework you can return to whenever you are unsure which storage mechanism a new feature calls for.

UserDefaults: small, simple key-value settings

NSUserDefaults is the tool most Objective-C developers reach for first, and for good reason: it is trivially easy to use. You call a setter with a key, and later you call a getter with the same key. Under the hood it is backed by a property list file that Cocoa loads into memory and keeps synchronized to disk, and it is the standard home for small pieces of app configuration — the user's chosen theme, whether they've seen the onboarding screen, their preferred sort order, a volume level. Anything you would describe as a 'setting' or a 'preference' belongs here.

The key word in that description is small. UserDefaults is designed for a modest number of scalar values and small structured values, not for anything resembling a dataset. We will spend an entire lesson on UserDefaults immediately after this one, including the two explicit warnings the platform gives you about what it is not for: secrets, and large databases.

Property lists: the format underneath UserDefaults, and more

A property list, almost always called a plist, is a structured file format — XML in its readable form, or a more compact binary form — capable of representing dictionaries, arrays, strings, numbers, dates, booleans, and raw data, nested to any depth. It is not a separate storage mechanism so much as a data format that several storage mechanisms are built on top of. UserDefaults, for instance, is really just Cocoa managing a plist file for you and giving you a convenient key-value API in front of it.

You are not limited to using plists only through UserDefaults, though. Classes like NSDictionary and NSArray have direct methods for writing themselves to a plist file and reading themselves back (writeToFile:atomically: and arrayWithContentsOfFile:, for example, or the more modern NSPropertyListSerialization API). This is useful when you want plist-style structured storage — nested dictionaries and arrays of simple values — for a chunk of data that does not belong in the app-wide UserDefaults database, such as a small configuration bundle shipped with the app, or a modest save file you want to remain human-readable and easy to inspect.

Property lists share UserDefaults' core limitation: they are meant for modest amounts of structured data, not large datasets, and they offer no querying — to find one value you load the whole structure into memory and walk it yourself.

Files: the raw, general-purpose option

Beneath every higher-level persistence mechanism sits the file system, and Objective-C gives you direct access to it through NSData and NSFileManager. This is the lowest-level, most general-purpose persistence tool available: any bytes at all — a downloaded image, a video, a PDF, an exported document, a large binary blob — can be written to a file and read back later. We covered this in depth in the earlier Data and File System lesson, so we will not repeat that material here, but it is worth placing on the map: whenever your data is naturally a large binary blob rather than small structured values, writing it directly to a file with NSFileManager is usually the right call, rather than trying to cram it into a plist or a database.

The tradeoff is that raw files give you no structure, no querying, and no built-in concurrency safety — you are responsible for choosing a sensible directory (Documents, Caches, or the app's Application Support directory), naming your files sensibly, and handling read/write errors yourself.

Keychain: secure storage for secrets

Keychain Services is a specialized, security-focused storage system that exists for exactly one job: holding secrets safely. Passwords, API tokens, OAuth refresh tokens, encryption keys — anything where a leak would compromise the user's account or data — belongs in Keychain and nowhere else. Unlike UserDefaults' plist file, Keychain items are encrypted at rest by the operating system, and the API supports fine-grained access control over when and how an item can be read.

Keychain gets its own full lesson right after UserDefaults, because the two are best understood as a pair: UserDefaults for ordinary settings, Keychain for anything sensitive. The spec's core rule bears repeating even at this survey level: never store passwords or other long-lived secrets in UserDefaults.

SQLite and Core Data: real databases for structured, queryable data

When your data stops being 'a handful of settings' and starts being 'a dataset' — hundreds or thousands of records, records with relationships to other records, or data you need to search, filter, sort, and query efficiently — you need an actual database, not a plist or a set of loose files. SQLite is a lightweight, embedded, SQL-based relational database engine that ships with the OS and is the workhorse behind an enormous number of iOS and macOS apps. You interact with it either through its C API directly or, more commonly in Objective-C apps, through a thin wrapper library.

Core Data is Apple's own higher-level persistence framework, sitting a layer above raw SQLite (though it can also use other storage backends). Rather than writing SQL, you describe your data as an object graph — entities, attributes, and relationships — and Core Data handles turning that graph into rows in a SQLite database behind the scenes, along with change tracking, undo support, and in-memory object management. Core Data is the right choice when your data has a genuine object-graph shape: entities with relationships to other entities, where you want to work with Objective-C objects rather than SQL rows directly.

Both of these are large enough topics that this course gives each one its own dedicated lesson later on. For now, the important thing is knowing they exist and roughly what problem each solves, so you recognize the moment a feature has outgrown UserDefaults or plain files.

Serialization: turning objects into something storable

Every persistence mechanism eventually faces the same underlying problem: your data lives in memory as live Objective-C objects, but disk only understands bytes. Serialization is the general name for the process of converting an object graph into a storable, transmittable form, and deserialization is the reverse. Different storage mechanisms use different serialization strategies. UserDefaults and plist-based file storage rely on property list serialization, which only understands a fixed set of plist-compatible types (strings, numbers, dates, booleans, arrays, dictionaries, and data). JSON is another extremely common serialization format, widely used for network payloads and increasingly for local storage too, via NSJSONSerialization. Core Data has its own internal serialization mechanism for turning managed objects into database rows and back.

For your own custom Objective-C classes — objects that are not naturally arrays, dictionaries, strings, or numbers — you typically implement the NSCoding protocol (encodeWithCoder: and initWithCoder:) so the object can describe how to serialize and deserialize itself, commonly using NSKeyedArchiver and NSKeyedUnarchiver. Understanding serialization as a concept that cuts across every one of these storage mechanisms — rather than a separate, unrelated topic — will make each individual API easier to learn, because you will recognize the same underlying pattern each time.

Migration: your data outlives your first schema

No matter which storage option you choose, there is a concern that applies to essentially all of them once your app has real users: migration. The shape of your persisted data — the keys you use in UserDefaults, the fields in your plist, the schema of your SQLite tables, the entities and attributes in your Core Data model — is not something you get to design once and freeze forever. As your app evolves, you will add fields, rename them, split one entity into two, or change a value's type, and every user who already has data saved in the old shape needs a path to the new shape without losing what they had.

Migration strategies differ by tool. UserDefaults migrations are usually simple manual code — check for an old key, read it, write it under the new key or transformed shape, delete the old key. Core Data has a formal, fairly sophisticated migration system (lightweight and custom mapping models) built specifically because object-graph schemas tend to evolve in complex ways. SQLite migrations are typically hand-written ALTER TABLE and data-transformation scripts, versioned and run in order the first time the app launches after an update. Whatever mechanism you choose, treat 'how will this evolve' as a design question from day one rather than an afterthought — retrofitting migration logic onto a shipped app with real user data is far harder than planning for it up front.

Caching: persistence for speed, not for safekeeping

Caching deserves to be called out separately because it inverts the usual mental model of persistence. Most of what we have discussed so far is about data that matters — losing it would upset the user. A cache is the opposite: it is data you persist purely for performance, and that can be regenerated or re-downloaded if it disappears. A cached thumbnail image, a cached list of search results, a cached parsed response from a network request — none of these are things the user would notice or care about losing, because your app can simply fetch or recompute them again.

This distinction matters practically because it changes how carefully you need to treat the data. Cached data can live in the Caches directory (which the OS is explicitly permitted to purge under disk pressure, unlike Documents), does not need to survive a device backup, and does not need the same rigor around migration or corruption-recovery that durable user data needs — if a cache file is corrupt or missing, the correct response is simply to regenerate it, not to treat it as data loss. Recognizing 'is this data durable or is it a cache' early saves you from either over-engineering throwaway data or, worse, under-protecting data the user actually cares about.

The decision framework: matching data shape to storage tool

With all the options on the table, here is the decision framework the spec asks for directly, stated as a set of concrete rules of thumb you can apply to any new persistence need.

• Small settings and preferences (a handful of scalar values, flags, or short strings) → UserDefaults. • Secrets — passwords, tokens, API keys, anything sensitive → Keychain, never UserDefaults. • Simple structured data at small-to-medium scale, with no need for complex querying → a property list file, or a lightweight SQLite table if you expect it to grow or need searching. • Complex object graphs with relationships between entities, where you want to work in terms of Objective-C objects rather than SQL → Core Data. • Large files or binary blobs (images, videos, documents, downloads) → the file system directly, via NSFileManager. • Data whose loss would only cost a re-download or a recomputation, not real user data → a cache, stored in the Caches directory rather than Documents.

A quick map of the storage APIs

This is a map, not a tutorial — each of these lines is explained in full in its own dedicated lesson. Notice how each API's shape hints at its purpose: UserDefaults is key-value, SQLite is SQL, Core Data is object-oriented.

// Settings (small key-value data)
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"hasSeenOnboarding"];

// Property list, saved directly
[dictionary writeToFile:plistPath atomically:YES];

// Raw files
[fileManager createFileAtPath:path contents:data attributes:nil];

// Secrets
SecItemAdd((__bridge CFDictionaryRef)keychainQuery, NULL);

// Structured, queryable data (covered in a later lesson)
sqlite3_open([dbPath UTF8String], &db);

// Object-graph persistence (covered in a later lesson)
NSManagedObjectContext *context = self.persistentContainer.viewContext;

Code Examples

A single integer describing a display preference is a textbook UserDefaults use case: small, scalar, non-sensitive, and read constantly throughout the app's lifetime. It costs almost nothing to store and read, and there is no benefit to using a heavier mechanism like SQLite or Core Data for a value this small.

[[NSUserDefaults standardUserDefaults] setInteger:size forKey:@"preferredTextSize"];

Both snippets 'save the token', but only the second one is safe. UserDefaults' plist file is not encrypted by the OS the way Keychain items are, so anything sensitive stored there is exposed far more than developers often assume. The full Keychain API is covered in the next-but-one lesson; this example previews the contrast so the distinction is concrete before we get there.

Common mistake: Saving an authentication token with setObject:forKey: on NSUserDefaults instead of using Keychain.

The first case is a handful of small values with no need for querying, so a property list array is perfectly adequate and simple. The second case — thousands of records that need to be searched and filtered — has crossed the threshold where a plist becomes the wrong tool: it would have to be loaded entirely into memory and searched by hand in code, with no indexing, which does not scale. That is the exact moment to reach for SQLite or Core Data instead. The comment about SQLite is illustrative here rather than a complete working call; the full SQLite API is taught in its own later lesson.

// Saving a small user preference — exactly what UserDefaults is for.
- (void)setPreferredTextSize:(NSInteger)size {
    [[NSUserDefaults standardUserDefaults] setInteger:size forKey:@"preferredTextSize"];
}

- (NSInteger)preferredTextSize {
    return [[NSUserDefaults standardUserDefaults] integerForKey:@"preferredTextSize"];
}
// WRONG: an auth token saved in UserDefaults, in a plain, unencrypted plist file.
[[NSUserDefaults standardUserDefaults] setObject:authToken forKey:@"authToken"];

// RIGHT: the same token saved through Keychain Services, encrypted at rest.
NSDictionary *query = @{
    (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
    (__bridge id)kSecAttrAccount: @"authToken",
    (__bridge id)kSecValueData: [authToken dataUsingEncoding:NSUTF8StringEncoding]
};
SecItemAdd((__bridge CFDictionaryRef)query, NULL);
// A small, fixed list of favorite recipe IDs — a plist array is fine.
NSArray *favoriteRecipeIDs = @[@101, @204, @317];
[favoriteRecipeIDs writeToFile:favoritesPath atomically:YES];

// Thousands of recipes, each with ingredients, steps, and search needs —
// this has outgrown a plist and belongs in SQLite or Core Data instead.
// sqlite3_exec(db, "SELECT * FROM recipes WHERE title LIKE ?", ...);

Persistence decision map

Ask what shape the data is before writing any storage code — the answer almost always points at one specific API.

What are you storing?
                         |
        +----------------+----------------+-----------------+
        |                |                |                 |
   small setting     a secret       structured data    large binary blob
        |                |                |                 |
  UserDefaults       Keychain      small: plist        file system
                                   growing/queryable:  (NSFileManager)
                                   SQLite or Core Data

Important Rules

Diagnose the data before picking the tool: size, sensitivity, structure, and query needs determine the right storage mechanism.

UserDefaults is for small, non-sensitive settings only — never secrets, never large datasets.

Keychain is the only appropriate place for passwords, tokens, and other long-lived secrets.

Property lists and files are good for small-to-medium structured or binary data with no querying needs.

SQLite and Core Data are for real datasets: many records, relationships, or search/filter requirements.

Plan for migration from the start — persisted schemas change as your app evolves, across every storage mechanism.

Distinguish durable data (must never be lost) from cached data (can always be regenerated) and store each accordingly.

A typical app uses several of these at once

A real production app rarely uses just one persistence mechanism — it layers several, each doing the job it is best suited for. A social app, for example, might use UserDefaults for the user's display preferences and whether they have dismissed a tutorial banner; Keychain for the session token that authenticates API requests; Core Data (backed by SQLite) for the local cache of posts, comments, and user profiles that lets the app work offline; the file system directly for downloaded images and videos, referenced by path from the Core Data records; and the Caches directory specifically for thumbnails and thumbnails alone, since those can always be regenerated from the full-size images. No single tool does all of this well, which is exactly why Cocoa gives you this many options instead of one 'save data' API.

Storing a user's password or session token as a plain string in NSUserDefaults.

Store any password, token, or credential in Keychain, which encrypts it at rest.

UserDefaults persists to an unencrypted plist file. Anything sensitive stored there is far more exposed than developers commonly assume, and this is explicitly called out later in this course as a rule with no exceptions.

Using UserDefaults to store a growing array of thousands of downloaded records.

Move to SQLite or Core Data once your data has real structure, volume, or query needs.

UserDefaults loads its entire plist into memory on every access. Using it as a makeshift database causes real, measurable performance problems as the dataset grows.

Writing large binary blobs, like downloaded images, directly into a property list as NSData.

Save large binary data as its own file on disk with NSFileManager, and store only the file path or a small reference elsewhere.

Plists and UserDefaults are meant for modest structured values. Embedding large binary blobs bloats the file, slows every load and save, and defeats the purpose of a lightweight settings store.

Treating cached, regenerable data (like a thumbnail cache) with the same durability guarantees as irreplaceable user data.

Store caches in the Caches directory and let the OS purge them under disk pressure; reserve Documents-level care for data that truly cannot be regenerated.

Conflating 'must persist' with 'nice to keep around' leads either to wasted engineering effort protecting disposable data, or to genuinely important data being treated as disposable.

Which storage option is specifically designed to keep data encrypted at rest, unlike UserDefaults? A. Property lists B. Keychain C. NSFileManager D. NSUserDefaults suites

B. Keychain — Keychain Services encrypts stored items at rest at the OS level. UserDefaults' underlying plist file does not get this protection.

A property list is best described as which of the following? A. A relational database engine B. A structured data format (XML or binary) that UserDefaults and direct file storage are built on top of C. A synonym for Keychain D. A caching-only mechanism that the OS can purge at will

B. A structured data format (XML or binary) that UserDefaults and direct file storage are built on top of — Property lists are a data format capable of representing dictionaries, arrays, strings, numbers, and more — UserDefaults uses this format internally, and you can also use it directly for your own structured data files.

You need to store 20,000 records with relationships between them and want to work with Objective-C objects rather than writing SQL by hand. What is the best fit? A. UserDefaults B. Keychain C. Core Data D. A single plist array

C. Core Data — Core Data is designed exactly for object-graph persistence with relationships, letting you work in terms of managed objects while it handles the underlying SQLite storage.

Which of the following is the defining characteristic that separates a 'cache' from ordinary durable persisted data? A. A cache is always stored in Keychain B. A cache can be lost and regenerated without real harm to the user, unlike durable data C. A cache never uses a file on disk D. A cache must be encrypted, while durable data does not need to be

B. A cache can be lost and regenerated without real harm to the user, unlike durable data — The defining trait of cached data is that it exists purely for performance and can always be recreated or re-downloaded — that is what allows it to live somewhere the OS is free to purge, like the Caches directory.

Why does migration matter across nearly every persistence mechanism discussed in this lesson, not just one? A. Because only SQLite databases ever change shape over time B. Because any persisted data's schema or format can need to evolve as the app evolves, regardless of which storage tool holds it C. Because migration only applies to Keychain items D. Because UserDefaults keys can never be renamed, unlike every other storage option

B. Because any persisted data's schema or format can need to evolve as the app evolves, regardless of which storage tool holds it — Whatever storage mechanism you choose, the shape of your data is likely to change as your app grows, and each user's existing saved data needs a path forward without being lost — this is a cross-cutting concern, not something unique to one API.

A large downloaded video file should generally be persisted using which mechanism? A. Embedded as NSData inside a property list B. Stored directly via the file system (NSFileManager), with only a reference kept elsewhere C. Stored as a Keychain item D. Stored as an integer in UserDefaults

B. Stored directly via the file system (NSFileManager), with only a reference kept elsewhere — Large binary blobs belong on the file system directly. Embedding them in a plist or UserDefaults bloats the structured-data file and hurts performance for every read and write of that store.

For each of the following pieces of data, name the single most appropriate storage mechanism from this lesson and give one sentence of justification: (1) whether the user prefers dark mode, (2) a downloaded PDF report the user asked to save, (3) an OAuth refresh token, (4) a locally cached catalog of 50,000 products with search, and (5) a resized thumbnail generated from a full-size photo the user already has stored.

(1) UserDefaults — a tiny, non-sensitive scalar setting. (2) The file system directly (NSFileManager), in Documents — a large binary blob the user explicitly wants kept. (3) Keychain — a long-lived secret. (4) SQLite or Core Data — large, structured, and needs searching. (5) A cache file in the Caches directory — regenerable from the original photo, so it does not need durable-data treatment.

Ask the four diagnostic questions for each item: how big is it, is it a secret, does it need structure/querying, and can it be regenerated if lost?