Section 5: Two Shapes of the Same Truth - Data Modeling
Executive Summary: This section splits the data into an editable relational source of truth (flags, immutable versions, mutable state, segments, rollout plans, audit) and a compiled per-service snapshot that SDKs load. We write the ordered rule document for smart-reply-v2, show why the kill bit lives outside the rules, place the per-flag salt, and turn down per-user assignment storage. The reusable lesson: the snapshot is derived and rebuildable, so only the database has to be right.
The interviewer points at Section 4's API: "What's actually stored, and what does the SDK hold in memory?" There are two answers for two consumers. Product teams and the rollout controller edit a few flags at a time and want history, approvals and conflict detection. A hundred thousand SDK instances want one document they can swap in atomically and evaluate in microseconds. One shape would serve both badly, so you draw two.
The Control Plane: Relational, Versioned, Boring on Purpose
You say it plainly: "The control plane is a relational database. Flag versions are immutable, there's an append-only audit log, and edits use optimistic concurrency." The write rate is 5,000 changes a day (about 0.06/s), bursting to ~50/min on deploy days. Every flag fits in ~60 MB (30,000 flags × ~2 KB), and history grows by 10 MB/day, about 3.65 GB a year. It needs transactions, not throughput.
CREATE TABLE flags (
flag_key TEXT PRIMARY KEY, -- 'smart-reply-v2'
type TEXT NOT NULL, -- BOOLEAN | STRING | NUMBER | JSON
owner_team TEXT NOT NULL, description TEXT,
salt TEXT NOT NULL, -- random at creation, feeds bucketing
created_at TIMESTAMPTZ NOT NULL, archived_at TIMESTAMPTZ);
CREATE TABLE flag_versions ( -- append-only: never UPDATE, never DELETE
flag_key TEXT, env TEXT, version BIGINT, rules_json JSONB NOT NULL,
created_by TEXT, created_at TIMESTAMPTZ, comment TEXT,
PRIMARY KEY (flag_key, env, version));
CREATE TABLE flag_state ( -- the one mutable row per flag per env
flag_key TEXT, env TEXT, current_version BIGINT NOT NULL,
killed BOOLEAN NOT NULL DEFAULT false,
kill_reason TEXT, killed_by TEXT, killed_at TIMESTAMPTZ,
PRIMARY KEY (flag_key, env));
Splitting flag_versions from flag_state is the central choice. A version is a fact ("at v42, the rollout went to 25%"), so it never changes: rollback moves the pointer back, and an auditor can rebuild what any SDK evaluated at any moment. flag_state holds the only two things that change: which version is live, and whether the flag is killed. The If-Match header from Section 4 becomes a single compare-and-swap:
UPDATE flag_state SET current_version = 42
WHERE flag_key = 'smart-reply-v2' AND env = 'prod' AND current_version = 41;
-- 0 rows updated → 412 Precondition Failed: someone else edited first
Flag versions count per flag — the v41 ETag becomes v42 — and the same commit takes the next number from one monotonic snapshot sequence per environment, 8812 → 8813. That environment sequence is what the fleet tracks, and why an SDK can resume the stream at sinceVersion=8812.
Interviewer: "Why not a key-value store? The read path is all key lookups."
You: "SDKs never read this store. Its workload is 'write a version, move the pointer, append the audit row' as one atomic step, plus queries like 'which flags use this segment.' A KV store without transactions makes me fake atomicity across three writes. Git as the database gives history but no row-level concurrency and no instant kill without a commit. If teams want PR review, git syncs into this database, as Section 12 shows."
The rest of the schema:
| Table | Key fields | Why it is shaped this way |
|---|---|---|
segments |
segment_id, name, kind RING | TENANT_LIST | REGION | CUSTOM, version, members_ref |
Members sit in a blob, so the largest list (50,000 tenant IDs × 16 B ≈ 800 KB) never bloats a row |
rollout_plans |
plan_id, flag_key, env, steps_json, status ACTIVE | PAUSED | DONE | KILLED, current_step |
The controller advances a step by writing a flag version through the human path |
audit_log |
actor, action, flag_key, env, from/to version, diff, reason, timestamp |
Append-only, about the same size as version history |
usage_rollup |
flag_key, service, variation, minute, evals |
~17 GB/day, ~1.5 TB at 90 days, so it lives in an analytics store, not this DB |
Quick check before moving on:
Sign in to continue reading
The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.
Sign in to read the full lesson