Skip to content

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 0 means 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 expiration
  • Delete(ctx, key) - Remove a key
  • Data(ctx) - Get all non-expired entries
  • Clear(ctx) - Remove all entries

HistoryTable

Append-only log of request/response records:

  • Set(ctx, resource, req, resp) - appends a record and returns it
  • GetByID(ctx, id) - the full record, bodies included
  • Recent(ctx, limit) - body-less summaries, newest first
  • Clear(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