When people move between a phone and a tablet, or reinstall an app, they expect their progress to follow them. Getting that right sounds simple and is not: two devices saving at the same moment, a flaky connection halfway through an upload, an old device coming back online after a week. The usual result of a naive design is the worst bug an app can have: lost progress.
We built a small shared cloud-save service for our own games and apps around one principle: nothing is ever overwritten, and the only thing that changes is a pointer.
Two kinds of data
- Blobs. Every save is uploaded as a new, immutable object with a unique name. Blobs are never modified or replaced.
- One pointer per user and slot. A tiny record that says "the current save is blob X", plus a version number.
Writing a save
- Upload the new save as a new blob. If the upload fails halfway, nothing points at it, so nothing is broken.
- Update the pointer with compare-and-swap: "set the current save to blob Y, but only if the current version is still N".
- If another device changed the pointer in the meantime, the swap fails. The client fetches the newer save and decides what to do: keep the newer one, or ask the player.
In HTTP terms, the client reads the pointer and gets an ETag, then writes with If-Match: <etag>. The server answers 412 Precondition Failed if someone else won the race.
Why this design is hard to break
- Partial uploads cannot corrupt anything, because a blob only becomes visible when the pointer moves.
- Two devices cannot silently overwrite each other, because the second write loses the compare-and-swap and has to look first.
- History comes for free. Old blobs are still there, so restoring yesterday's save is just pointing at an older blob.
- Old devices cannot roll you back. A device that comes back online with stale data holds an old version number, so its write is rejected.
Deciding which save wins
The server keeps things safe; the client decides what "newer" means. We store an epoch in each save, a counter that only goes up as the player makes progress, and a device only adopts a cloud save when it is strictly newer than its local one. When it is unclear, asking the player beats guessing.
Two more choices made it reliable on phones:
- Save to the cloud at natural moments, like a manual save or when the app goes to the background, not on every autosave. That protects battery and frame rate.
- Start anonymous, link later. A save can be backed up before the user creates an account; signing in later links that history to their identity so it can move to a new device.
Migrating old saves
When we moved an existing game onto this service, the old saves were re-keyed into the new store as accounts with their full version history, and verified byte for byte before the old path was retired. A migration you cannot verify is a migration you cannot trust.
Where else this fits
The same pattern works for documents, settings, drafts and any user data synced between devices. If losing it would hurt, write it once and move a pointer.