0002 — Compute on read, no ticks
Date: 2026-11-09 · Status: accepted
Context
The check-in loop is the product: 3 minutes on a phone, nothing important requires being online at a specific moment (GAME-DESIGN.md §2, pillar 5, and §6). Most players are idle most of the time and the design must reach millions of them, so a million idle players must cost nothing — which rules out anything that advances every row on a schedule (ARCHITECTURE.md §1.2). The app is stateless and answers read pressure by adding processes (ARCHITECTURE.md §6).
Decision
State that changes with time is computed on read from a timestamp, never advanced by a background job (ARCHITECTURE.md §1.2):
- Tax ledger = one row per colony per cycle, written by the daily tax sweep and read as a sum; nothing accrues between cycles.
- Build queues =
started_at + duration; completion derived, then lazily written when read. - Fleet position interpolated from
departed_at,arrives_at, origin and destination. - Security and tax are arithmetic over a handful of stored integers (
SECURITY-AND-SOVEREIGNTY.md§8).
One table, jobs, with a due_at index, is the entire scheduler. A worker claims rows with SELECT ... FOR UPDATE SKIP LOCKED and handles exactly three things: arrivals (resolve combat, apply loot, write reports), the daily tax sweep over claimed systems only, and lapse and decay (ARCHITECTURE.md §3).
Consequences
- Nothing ever touches every row: no cron sweeps, no per-entity loops, no simulation loop.
- The only background infrastructure is one small worker, horizontally scalable by adding more, with no coordination beyond
SKIP LOCKED(ARCHITECTURE.md§3). - The two sweeps that scale with territory are bounded by design: the claimed set is limited by what the playerbase can afford to hold, "which is the only set that matters" (
ARCHITECTURE.md§3). - Reads can write: queue completion and structure decay are lazily materialised on the first read after they are due.
- Any new time-varying quantity must be arithmetic over stored timestamps and integers, or it does not fit.
Alternatives rejected
- Per-entity tick loops. Cost scales with the number of entities rather than with activity, so idle players are not free — the opposite of
ARCHITECTURE.md§1.2. - Cron sweeps over all rows. Explicitly excluded: "There is no cron that touches every row, ever" (
ARCHITECTURE.md§1.2). - An in-memory simulation server. The app is stateless and scales by adding processes, and the scheduler already scales via
SKIP LOCKED(ARCHITECTURE.md§3, §6). An in-memory authority holds the universe's live state in one process and must rebuild it on every restart.[INFERENCE from ARCHITECTURE.md §6]