Data Persistence Layer

A port hierarchy that decouples save contracts from Unity backends so file storage, PlayerPrefs and encryption can all change without touching game code.

What the system is for

Every game needs to save data, and every project ends up coupling that need to a specific backend too early. File paths get scattered across systems, PlayerPrefs keys collide, encryption is bolted on as an afterthought, and swapping one storage approach for another becomes a refactor rather than a configuration change.

The Persistence layer in Serenity defines the save contract at the domain level and keeps Unity out of it. Backends implement ports. Game code talks to ports. The Checkpoint system and Game Settings sit on top of the same foundation without knowing which store is underneath.

The Unity problem

Directly calling File.WriteAllBytes or PlayerPrefs.SetString from game code works until it does not. The first time you want to add encryption, you touch every save site. The first time you want to run in a context where the file system is restricted, you have no seam to inject an alternative. Health checks do not exist, so persistence failures surface as runtime exceptions rather than graceful degradation.

Namespace collisions are another quiet failure mode. Two systems writing the same PlayerPrefs key overwrite each other silently. There is no enforced separation, no existence check contract, and no common delete operation. Each system invents its own conventions and the project accumulates incompatible patterns.

How Serenity approaches it

Serenity defines a port hierarchy under the Serenity.Persistence namespace. IPersistenceStore is the base: it carries a Namespace property and a HealthCheckAsync method that every backend must satisfy. IKeyedStore extends it with ExistsAsync and DeleteAsync. IBlobStore adds async stream I/O through IReadOnlyBlobStore, while IKeyValueStore offers typed byte-level get and set for lightweight backends. IAppendableBlobStore handles log-style append scenarios. IContentTransformer covers the Transform and InverseTransform pair for encryption and compression pipelines.

Two concrete backends ship out of the box. FileStore implements IBlobStore and IAppendableBlobStore using IFileWriterService for atomic writes via the file system. UnityPlayerPrefsKeyValueStore implements IKeyValueStore and stores values as Base64 strings in PlayerPrefs under an optional prefix, preventing key collisions across systems. Both report a namespace string and answer health checks.

For structured, queryable data — records with fields, filters and ordering rather than an opaque blob — the same layer gains the technology-agnostic IStructuredStore contract family: keyed records, filter/order/limit/count queries and conditional upsert. It is the shared backend behind Leaderboard, GameSave/GameProgress and structured logging, and it is deliberately backend-plural: device-local key-value, human-readable JSON files on disk, an embedded SQLite database file, your own HTTP service, or a direct Postgres/MySQL/MongoDB connection for trusted environments. One contract, five interchangeable places to put the data.

How it fits into Serenity

Persistence follows Serenity's layered structure. The Domain layer holds the port interfaces: IPersistenceStore, IKeyedStore, IReadOnlyBlobStore, IBlobStore, IAppendableBlobStore, IKeyValueStore, IContentTransformer and ISerializer. The Application layer publishes use cases PersistenceSaveObject, PersistenceLoadObject, PersistenceSaveBytes and PersistenceLoadBytes together with their input DTOs, all grouped under PersistenceUseCases. The Infrastructure layer contains FileStore in the FilePersistence module and UnityPlayerPrefsKeyValueStore in the PlayerPrefsPersistence module. Installation wires everything through PersistenceInstaller, FilePersistenceInstaller and PlayerPrefsPersistenceInstaller.

The Checkpoint system and Game Settings consume persistence through the same port interfaces, so they are backend-agnostic by default. Replacing FileStore with a cloud backend or swapping the serializer means changing an installer, not editing game systems.

The structured side lives alongside it in the StructuredPersistence module, with its own backend implementations behind IStructuredStore — KeyValue, LocalFile (pretty-printed JSON), Sqlite (an embedded database file, schema created automatically on first use), and HTTP/Postgres/MySQL/MongoDB behind SERENITY_STRUCTURED_* scripting defines. Each database backend needs hand-installed, git-ignored driver DLLs, so Tools ▸ Serenity ▸ Validate ▸ Database Drivers reads a per-backend manifest of the exact NuGet packages and assembly files each one needs, marks which are already installed, and only offers to set the scripting define once the full set is in place — so a define is never flipped on ahead of the DLLs it depends on.

Practical workflow

  1. Choose which backend fits the target data: FileStore for binary saves and replays, UnityPlayerPrefsKeyValueStore for lightweight settings, or an IStructuredStore backend for queryable records such as leaderboards and save slots.
  2. Register the chosen backend through the corresponding installer so the container resolves the correct IPersistenceStore implementation.
  3. Optionally chain an IContentTransformer to encrypt or compress payloads before they reach the store.
  4. Use the application use cases PersistenceSaveObject and PersistenceLoadObject to persist domain objects without writing serialization code by hand.
  5. Call HealthCheckAsync at startup or from a diagnostics system to confirm the backend is reachable before the first write.
  6. For structured data, pick a backend card (device / files / database) on the owning feature's settings asset and, for a database backend, open Database Drivers to install the exact packages it needs before flipping on its scripting define.
  7. Add a new backend by implementing the relevant port interfaces and registering them through a custom installer — no game code changes required.

What you get

  • Base port IPersistenceStore with Namespace property and HealthCheckAsync for all backends
  • IKeyedStore adding ExistsAsync and DeleteAsync to the common contract
  • IBlobStore and IReadOnlyBlobStore for async stream-based binary I/O
  • IKeyValueStore for lightweight typed byte get/set, suited to PlayerPrefs-style backends
  • IAppendableBlobStore for append-only log and replay scenarios
  • IContentTransformer with Transform and InverseTransform for encryption and compression pipelines
  • FileStore implementing IBlobStore with atomic writes through IFileWriterService
  • UnityPlayerPrefsKeyValueStore storing Base64 values in PlayerPrefs under a configurable key prefix
  • IStructuredStore contract family for queryable, filterable, orderable records — shared by Leaderboard, GameSave, GameProgress and structured logging
  • Five interchangeable structured backends: device key-value, human-readable JSON files, embedded SQLite, your own HTTP service, or direct Postgres/MySQL/MongoDB
  • Database Drivers window (Tools ▸ Serenity ▸ Validate ▸ Database Drivers) listing each backend's exact driver DLLs and NuGet packages, and gating its scripting define until they're all installed

When to use this

  • Projects that need to swap the save backend between platforms without changing game systems.
  • Games that require encryption or compression on saved data and want a single transformation point.
  • Codebases where multiple systems save data and need enforced namespace separation to prevent key collisions.
  • Projects building on Serenity's Checkpoint or Game Settings systems, which consume these ports directly.
  • Features that need queryable, ranked or filtered records — leaderboards, save slots, structured logs — backed by anything from a local file to a real database.

Related systems

Use Serenity when you want persistence contracts that outlast any single backend — file today, cloud tomorrow, structured records or a raw blob — with encryption, health checks and namespace safety already in place.

Back to the home page