# State Snapshot Persistence

> Use when a service holds a hot in-memory aggregate that must survive restart without a write on every mutation: dirty-set plus timed flush, a one-table JSON snapshot with idempotent DDL, bounded rehydrate, degrade-to-memory when the database is down, and deleting merged entries so they do not return as ghosts. Not for data where losing the last flush interval is losing a transaction - that is P4's ORM-managed schema.

- Skill: `konradcinkusz/state-snapshot-persistence` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add konradcinkusz/state-snapshot-persistence`
- Raw SKILL.md: https://api.skillmd.com/api/skills/konradcinkusz/state-snapshot-persistence/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: konradcinkusz (https://skillmd.com/u/konradcinkusz)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/konradcinkusz/state-snapshot-persistence

---


# Write-behind snapshot persistence

**Read [`references/STATE-SNAPSHOT-PERSISTENCE.md`](references/STATE-SNAPSHOT-PERSISTENCE.md) before applying any of this.**
That file is the standard; everything below it is a summary to help you decide
whether this skill applies and to check your work afterwards.

Reference-architecture principles: P4, P8.

## What this standard covers

- Mark dirty, flush on a timer
- The schema is one table, bootstrapped idempotently
- Rehydrate on start, with a bound
- A dead database degrades; it never blocks ingest
- Deletions must reach the store, or they come back
- When not to use this

## Failure modes

| Symptom | Cause |
|---|---|
| A burst of traffic produces a burst of writes and latency follows it | Writing on mutation instead of marking dirty and flushing on a timer (§1) |
| The hot path stalls whenever the database is slow | The flush holds the lock across the database call, so the hot path queues behind it (§1) |
| One entry stops being persisted and nothing reports it | A failed write dropped its key instead of re-queueing; the entry updates again only if it is touched again (§1) |
| Deleted or merged entries reappear after a restart | The removal never reached the table, so rehydrate restored them as ghosts (§5) |
| Startup time grows with the size of the history | Rehydrate is unbounded — it reads the whole table rather than the most recent N (§3) |
| The service ran for weeks with no persistence and nobody knew | The degrade-to-memory path logs at a level nobody reads (§4) |
| A schema change requires a migration this shape was chosen to avoid | Too many fields promoted out of the document into columns (§2) |
| A reviewer reads the idempotent DDL as a skipped migration | The choice between this shape and an ORM-managed schema was never written down (§6) |

## Checklist

- [ ] The hot path only marks keys dirty under a lock — no serialising, no awaiting, no database call
- [ ] The flush loop swaps the dirty set inside the lock and writes outside it
- [ ] A failed write re-queues its key for the next tick rather than retrying in place or dropping it
- [ ] One table, one JSON document per aggregate, columns promoted only for what the read path queries or sorts on
- [ ] Schema bootstrapped with idempotent DDL, and the upsert safe to repeat on the same key
- [ ] Rehydrate on startup is bounded, ordered by a promoted column, and the bound is configuration
- [ ] Bootstrap and rehydrate failures log and continue; an unavailable database never blocks the hot path or fails the boot
- [ ] Every in-memory removal **and merge** deletes the corresponding row
- [ ] The repository records why this shape was chosen over an ORM-managed schema

---

Generated from [`docs/guides/STATE-SNAPSHOT-PERSISTENCE.md`](https://github.com/konradcinkusz/architecture-standards/blob/main/docs/guides/STATE-SNAPSHOT-PERSISTENCE.md) by `scripts/build-marketplace.mjs`. Do not edit this file: change the source document, or its entry in `catalog/marketplace.catalog.json`, and re-run the generator.

