/ game-development / Building A Save System For A Game That Keeps Changing
game-development 8 min read

Building A Save System For A Game That Keeps Changing

How I built a forward-compatible JSON save for Orchard Deck and migrated old saves automatically when the game was renamed from Apple Orchard. Round-trips verified.

Orchard Deck title screen with a saved collection loading

The save system is the part of a game nobody sees and everybody depends on. Players never thank you for a save that works. They only ever notice the one that does not, the one that wiped their collection or refused to load after an update, and that single bad experience ends the relationship. Orchard Deck is a collection game, which makes the stakes worse than usual. The entire emotional point of the game is the collection you build over many sessions. If the save fails, I have not lost a level of progress. I have erased the thing the player came for. So the save system got more careful design than almost any flashy feature, precisely because it is the one piece that cannot fail.

The added difficulty is that Orchard Deck is a build-in-public game that changes constantly. Every week the save file needs to hold a little more than it did last week. A new system arrives, a new field appears, and the save format that worked for last week's build has to keep working for this week's. A save system for a finished game is easy. A save system for a game that keeps changing is the real problem, and it is the one I want to walk through.

Play Orchard Deck in your browser

Plain JSON On Purpose

The save lives at a single file path on the player's machine, and it is plain JSON. Not a binary blob, not an engine-specific resource format, just readable JSON with named fields. That choice was deliberate and it pays off in three ways that matter for a game in constant flux.

The first payoff is that I can read it. When a player reports a bug, I can ask them to open their save and I can look at it and understand it instantly, because it is named fields with obvious values. A binary save would force me to write a separate tool just to inspect the thing I am debugging. The second payoff is that adding a field is trivial. A new system needs to persist something, I add a key, and saves written before that key existed simply do not have it. The third payoff is that JSON degrades gracefully. A missing key is not a corruption. It is an absence, and an absence has an obvious answer, which leads directly to the most important rule in the whole system.

Defaults Are The Whole Trick

The single principle that makes the save forward-compatible is this. Loading never assumes a key is present. Every field read out of the save is read with a default for the case where the key is missing, and the default is always the value a brand new player would have for that field.

This sounds small and it is the entire game. Picture an old save written before the daily quest system existed. That save has no quest data in it at all. When a player loads it into a build that has quests, the load does not crash and it does not refuse the file. It reads the quest data, finds nothing, and falls back to the default, which is the same fresh-quest state a new player gets. The old save loads cleanly into the new game, and the player simply starts using the new system from a clean slate, with everything they already collected fully intact. No migration script needed for ordinary additions. The default-on-missing rule handles every case where the only change is that the new build saves more than the old one did, which is the overwhelming majority of changes I make.

The discipline this requires is constant. Every single read from the save must have a default, with no exceptions, because the one read that assumes a key exists is the one that crashes on the first old save it meets. I treat a defaultless read as a bug the same way I treat a null dereference. The reward for that discipline is that I almost never have to write migration code, because the format absorbs additive change automatically.

The Rename That Needed Real Migration

Defaults handle additive change. They do not handle the rare structural change, and Orchard Deck had exactly one of those. The game was originally called Apple Orchard. At some point I renamed it to Orchard Deck, and a rename is not a harmless cosmetic thing when your save data lives in a directory named after the game.

The problem is that the operating system gives an application its own private data directory keyed to the application's name. Rename the game and the new build looks for its save in a new directory, finds nothing, and concludes you are a brand new player. Every existing player would have launched the renamed build and watched their entire collection vanish, not because the save was deleted, but because the new name was looking in the wrong drawer. The data was still sitting safely in the old Apple Orchard directory. The new Orchard Deck build just did not know to look there.

So the very first thing the game does on launch, before it touches anything else, is check whether a save exists in the new location. If it does, business as usual. If it does not, the game looks at the old Apple Orchard directory. If a save is sitting there, it copies that save into the new location and proceeds as if it had always lived there. The migration runs once, silently, on the first launch of the renamed build, and from the player's side nothing happened at all. They opened the game, their collection was there, and they never knew there was a moment where it could have been lost. The best migration is the one no player ever notices ran.

Verifying The Round-Trip

A save system you have not tested is a save system you have not built, because the failure mode is invisible until it strikes a real player and then it is too late. The thing I verify, over and over, is the round-trip. I take a full game state, save it, load it back, and check that what came out is identical to what went in. If the round-trip is clean, the save and load code agree, and agreement between those two is the whole contract.

I run the round-trip against the messiest states I can build, not the clean ones. A fresh game saves and loads fine and proves nothing. The state I care about is a deep collection with a half-finished campaign, partial quests, a login streak mid-stride, and a tech tree bought out in a weird order. Save that, load it, and confirm every value survived. Then I run the harder case, taking a save written by an older build and loading it into the current one, which is the real-world scenario every time I ship an update. If an old save round-trips into the new build with the collection intact, the forward-compatibility actually holds rather than just sounding good in a blog post.

The reason I lean on this so hard is that save bugs are silent. A rendering bug is loud, you see it on screen the moment it happens. A save bug hides until a player loads a file days later and finds it broken, and by then the trust is gone and they are not coming back. The round-trip test is how I move that failure from the player's machine, where it is fatal, to my machine, where it is just a red line I fix before anyone ever sees it.

Why The Boring Part Got The Care

None of this shows up in a trailer. There is no screenshot of a save system, no moment of delight when a default fills in a missing field. But the collection a player builds is the entire reason Orchard Deck exists, and the save is the only thing standing between that collection and oblivion every time the player closes the tab or I ship an update. Giving the invisible part real care is what lets the visible part be trusted, and trust is the only currency a build-in-public game has.

So the save sits underneath everything, plain JSON with a default behind every read, copying itself forward through a rename and round-tripping clean through every update I ship. Nobody will ever see it work. They will just close the tab, come back days later, and find the whole collection exactly where they left it, which is the only thing this part of the game was ever supposed to do.

Play Orchard Deck in your browser

Built by Kevin

Like this? You'll like what I'm building too.

Two ways to support and get more of this work.

Desktop App

HEARTH

A privacy-first Life OS for your desktop. Journal, tasks, and notes that stay on your machine. Coming soon, direct download from this site.

Read more
Digital Products

MY TOOLKITS

Receipts-first toolkits for shipping after hours, building Claude agents, publishing on Amazon, and more. The exact methods I used, not theory.

Browse on Whop

Need This Built?

Kevin builds products solo, from first version to live. If you want something like this made, work with him.