Storage¶
Mockzilla uses a layered storage architecture that provides service isolation while sharing a single backend connection.
Design Principles¶
Shared Backend, Isolated Views¶
All services share a single storage backend (memory or Redis), but each service gets an isolated "view" into that storage through key prefixing. This means:
- Single connection - One Redis client or memory store for the entire application
- No cross-service access - Service A cannot read or modify Service B's data
- Automatic namespacing - Keys are prefixed with the service name transparently
Lazy Resource Creation¶
Tables and history stores are created on first access, not upfront. This keeps memory usage low when services don't use all features.
TTL Support¶
Records can have individual expiration times:
- Per-record TTL - Each
Set()call can specify its own TTL - Zero means forever - A TTL of
0means the record never expires - Lazy expiration (memory) - Expired records are deleted on access, not via background cleanup
- Native TTL (Redis) - Uses Redis's built-in key expiration
Architecture¶
┌─────────────────────────────────────────────────────────┐
│ Application │
├─────────────────────────────────────────────────────────┤
│ Service A Service B Service C │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ DB │ │ DB │ │ DB │ │
│ ├─────────┤ ├─────────┤ ├─────────┤ │
│ │ History │ │ History │ │ History │ │
│ │ Table() │ │ Table() │ │ Table() │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
├───────┴───────────────────┴───────────────────┴─────────┤
│ Storage │
│ (Memory or Redis backend) │
│ │
│ Keys: "serviceA:history:index" │
│ "serviceA:history:entry:{uuidv7}" │
│ "serviceB:cache:{sha256(method+url)}" │
└─────────────────────────────────────────────────────────┘
Components¶
Storage¶
The shared backend that manages the actual data. Supports:
- Memory - In-process storage, data lost on restart
- Redis - Distributed storage, persists across restarts
DB¶
A service-scoped wrapper that provides:
- History() - Typed access to request/response history
- Table(name) - Generic key-value storage with TTL support
Table¶
Generic key-value store with per-record TTL:
Get(ctx, key)- Retrieve a value (returns false if expired or missing)Set(ctx, key, value, ttl)- Store with optional expirationDelete(ctx, key)- Remove a keyData(ctx)- Get all non-expired entriesClear(ctx)- Remove all entries
HistoryTable¶
Append-only log of request/response records:
Set(ctx, resource, req, resp)- appends a record and returns itGetByID(ctx, id)- the full record, bodies includedRecent(ctx, limit)- body-less summaries, newest firstClear(ctx)- removes every record
Entry IDs are UUIDv7, so they are unique, time-ordered and generated by the writer. A backend never fetches a sequence number before writing.
The log is capped at the newest 100 entries and every record expires after
history.duration. Backends trim at write time, so listing the log never pays
for records no caller can reach.
The split between Recent and GetByID is deliberate. The history UI lists
summaries and fetches one full record only when a user opens it, so a backend
is free to keep the summary apart from the bodies and serve the list without
reading a single one. How it does that is up to the backend: Redis keeps a
separate list of summaries, and a store that can project fields out of a record
can read the same view off one row.
Choosing a Backend¶
| Feature | Memory | Redis |
|---|---|---|
| Setup | None | Requires Redis server |
| Persistence | No (lost on restart) | Yes |
| Multi-instance | No (each instance isolated) | Yes (shared state) |
| Performance | Fastest | Network overhead |
Use Memory when: - Running a single instance - Data loss on restart is acceptable - Simplicity is preferred
Use Redis when: - Running multiple instances behind a load balancer - Request history should persist across restarts
Schema install¶
Some backends need a schema before they can store anything. Memory and Redis
do not. A storage driver that does implements db.Installer:
Install(ctx)creates or migrates the schema. It runs on every start, so it has to be safe to repeat and safe when several instances start together.VerifyInstall(ctx)changes nothing. It returns an error when the schema is missing or older than the driver needs.
Mockzilla calls Install right after it opens the backend. Set
storage.install: false (or STORAGE_INSTALL=false) when the application is
not allowed to change the schema, for example when an administrator applies it
by hand. Mockzilla then calls VerifyInstall instead.
A failure in either call counts as a failure to open the backend. With
storage.strict: true the process refuses to start. Without it, mockzilla
falls back to memory.
The request cache¶
cache.requests: true stores GET responses in the cache table, keyed by a
SHA-256 of the method and URL. It is independent of history: turning history
off does not turn the cache off, and a cached hit costs a single read.
Responses above 10KB are not cached. A cache hit serves the whole body or none of it, so a larger payload is regenerated on every request rather than served back clipped. The cap sits just past the 95th percentile of generated response sizes, so it covers nearly every endpoint.
Configuration¶
See App Configuration for setup options.
# Memory (default)
storage:
type: memory
# Redis
storage:
type: redis
redis:
address: localhost:6379
password: ""
db: 0