Skip to content

Cached State Pattern

Derived state for runtime performance. Reconcilable against Facts. Disposable.

Prerequisites: PRINCIPLES.md (Principles 7, 10), PRIMITIVES.md, ledger-pattern.md, durable-objects.md


Cached state is derived from Facts for runtime performance. It is not a primitive. It is not authoritative. It can always be rebuilt by replaying the ledger.

PropertyDescription
DerivedCalculated from Facts, not stored directly
DisposableCan be deleted and rebuilt (Principle 7)
ReconcilablePeriodically verified against ledger
LocalLives in DO SQLite, never replicated
FastEnables O(1) eligibility checks on hot path

Key Insight: Cached state is an optimization, not truth. The ledger is truth. When they disagree, the ledger wins.


Each cached state type derives from specific Fact types and answers specific questions.

TypeDerived FromQuestion Answered
BudgetStatecharge, deposit, credit_issuedCan this account afford this action?
CapStateinvocationHas this asset hit its rate limit?
PrepaidBalancedeposit, chargeWhat’s the prepaid balance?
AccessStateaccess_granted, access_modified, access_revokedDoes this user have permission?
ContactCanonical*contact_mergedWhich contact_id is canonical?
SettlementStatecharge (incurred/pending/settled)What’s our exposure on pending settlements?

*ContactCanonical is a special case—see section below.

Tracks spending against available budget for RTB eligibility.

interface BudgetState {
deposited: number; // Sum of deposit Facts
spent: number; // Sum of charge Facts
credits: number; // Sum of credit_issued Facts
remaining: number; // deposited + credits - spent
last_fact_id: string; // Last Fact included in calculation
computed_at: number; // Timestamp of last computation
}

Derivation:

function deriveBudgetState(facts: Fact[]): BudgetState {
let deposited = 0, spent = 0, credits = 0;
let last_fact_id = null;
for (const fact of facts) {
switch (fact.type) {
case 'deposit':
deposited += fact.amount;
break;
case 'charge':
spent += fact.amount;
break;
case 'credit_issued':
credits += fact.amount;
break;
}
last_fact_id = fact.id;
}
return {
deposited,
spent,
credits,
remaining: deposited + credits - spent,
last_fact_id,
computed_at: Date.now()
};
}

Tracks invocation counts against rate limits.

interface CapState {
period_start: number; // Current period start timestamp
period_duration: number; // Period length in ms
invocation_count: number; // Count in current period
last_fact_id: string;
computed_at: number;
}

Derivation:

function deriveCapState(facts: Fact[], periodDuration: number): CapState {
const periodStart = getCurrentPeriodStart(periodDuration);
let count = 0;
let last_fact_id = null;
for (const fact of facts) {
if (fact.type === 'invocation' && fact.timestamp >= periodStart) {
count++;
}
last_fact_id = fact.id;
}
return {
period_start: periodStart,
period_duration: periodDuration,
invocation_count: count,
last_fact_id,
computed_at: Date.now()
};
}

Tracks prepaid account balance (subset of BudgetState logic).

interface PrepaidBalance {
balance: number; // deposits - charges
last_fact_id: string;
computed_at: number;
}

Tracks economic exposure—charges that have been incurred but not yet settled. Answers Principle 9’s question: “What’s our exposure on pending settlements?”

interface SettlementState {
pending_charges: Array<{
charge_id: string; // The incurred charge Fact ID
amount: number; // Charge amount
incurred_at: number; // When the charge was incurred
settlement_model: string; // real-time, eventual, batch
expected_settlement: number; // When settlement is expected
}>;
total_pending: number; // Sum of pending charge amounts
last_settled_at: number; // When the last charge settled
last_fact_id: string;
computed_at: number;
}

Derivation:

function deriveSettlementState(facts: Fact[]): SettlementState {
const pending = new Map<string, PendingCharge>();
let last_settled_at = 0;
let last_fact_id = null;
for (const fact of facts) {
if (fact.type !== 'charge') continue;
switch (fact.subtype) {
case 'incurred':
pending.set(fact.id, {
charge_id: fact.id,
amount: fact.amount,
incurred_at: fact.timestamp,
settlement_model: fact.data?.settlement_model || 'eventual',
expected_settlement: fact.data?.expected_settlement || 0
});
break;
case 'settled':
case 'written_off':
// Remove from pending (terminal states)
pending.delete(fact.source_id);
if (fact.subtype === 'settled') {
last_settled_at = fact.timestamp;
}
break;
// pending, disputed, resolved don't change pending set
}
last_fact_id = fact.id;
}
const pending_charges = Array.from(pending.values());
return {
pending_charges,
total_pending: pending_charges.reduce((sum, c) => sum + c.amount, 0),
last_settled_at,
last_fact_id,
computed_at: Date.now()
};
}

Settlement timing is critical for cash flow visibility and economic loop closure (Principle 9). Monitor proactively.

Metrics to emit:

interface SettlementMetrics {
tenant_id: string;
settlement_model: string; // real-time, eventual, batch
settlement_lag_ms: number; // Time from incurred to settled
pending_count: number; // Number of unsettled charges
pending_total: number; // Total $ pending settlement
}
// Emit on every settlement
async function emitSettlementMetrics(charge: Fact, settledAt: number) {
const lag = settledAt - charge.data.incurred_at;
await this.env.ANALYTICS.writeDataPoint({
blobs: [charge.tenant_id, charge.data.settlement_model],
doubles: [lag],
indexes: [1] // settled
});
}
// Emit pending snapshot periodically
async function emitPendingSnapshot(state: SettlementState) {
await this.env.ANALYTICS.writeDataPoint({
blobs: [this.tenantId, 'pending_snapshot'],
doubles: [state.total_pending, state.pending_charges.length],
indexes: [0]
});
}

SLOs by settlement model:

ModelExpected LagAlert ThresholdRationale
real-time< 100ms> 1sRTB requires immediate settlement
eventual< 5min> 15minCall qualification should complete quickly
batch< 24h> 48hDaily batch should not slip

Dashboard queries:

-- Settlement lag percentiles (last 24h)
SELECT
blob2 AS settlement_model,
QUANTILE(double1, 0.50) AS p50_lag_ms,
QUANTILE(double1, 0.95) AS p95_lag_ms,
QUANTILE(double1, 0.99) AS p99_lag_ms
FROM z0_settlement
WHERE timestamp > NOW() - INTERVAL '24 hours'
AND index1 = 1 -- Only settled
GROUP BY blob2;
-- Current pending exposure by tenant
SELECT
blob1 AS tenant_id,
MAX(double1) AS pending_total,
MAX(double2) AS pending_count
FROM z0_settlement
WHERE timestamp > NOW() - INTERVAL '1 hour'
AND blob2 = 'pending_snapshot'
GROUP BY blob1
ORDER BY pending_total DESC;
-- Settlement failure rate (charges that went to write_off)
SELECT
blob1 AS tenant_id,
COUNT(*) FILTER (WHERE index1 = 0) AS write_offs,
COUNT(*) AS total,
COUNT(*) FILTER (WHERE index1 = 0) * 100.0 / COUNT(*) AS failure_rate
FROM z0_settlement
WHERE timestamp > NOW() - INTERVAL '7 days'
GROUP BY blob1;

Alerting:

AlertConditionSeverityAction
Settlement lag spikep95 > 2x expectedWarningCheck external dependencies
High pending exposuretotal_pending > thresholdWarningReview settlement pipeline
Settlement failurewrite_off rate > 1%CriticalInvestigate root cause
Stale pending chargesAny charge > 2x expected_settlementWarningManual intervention may be needed

Tracks current permissions for a user on an entity.

interface AccessState {
user_id: string;
entity_id: string;
permissions: string[]; // Current permission set
granted_at: number; // When access was first granted
last_modified_at: number;
last_fact_id: string;
computed_at: number;
}

Derivation: Replay access_granted, access_modified, access_revoked Facts in order. Final state is current permissions.

Note: ContactCanonical is NOT truly disposable cached state. It is a write-through index that must be updated synchronously. This is an exception to Principle 7, documented here for transparency.

Tracks which contact_id is canonical after merges.

interface ContactCanonical {
canonical_id: string; // The surviving contact_id
merged_ids: string[]; // All merged contact_ids
last_fact_id: string;
computed_at: number;
}

Derivation: Replay contact_merged Facts. Build union-find structure. Canonical is the root.

ContactCanonical is unique among cached states - it must be updated synchronously and atomically with the contact_merged Fact write. This is because:

  1. Identity queries happen immediately after merge
  2. Stale canonical ID would cause attribution errors
  3. There’s no “eventual” that’s acceptable for identity

Pattern:

async mergeContacts(sourceId: string, targetId: string): Promise<void> {
// Single atomic operation in DO
await this.sql.exec('BEGIN TRANSACTION');
// 1. Write the merge Fact
await this.appendFact({
type: 'contact_merged',
data: { source_id: sourceId, target_id: targetId, canonical_id: targetId }
});
// 2. Update ContactCanonical immediately (same transaction)
await this.setCachedState('ContactCanonical', {
canonical_id: targetId,
merged_ids: [...existing.merged_ids, sourceId]
});
await this.sql.exec('COMMIT');
}

Unlike other cached states, ContactCanonical cannot tolerate any window of inconsistency.


Cached state updates through three mechanisms:

PatternWhenLatencyConsistency
Inline updateAfter Fact appendImmediateStrong
Lazy updateOn read if staleOn-demandEventual
Scheduled reconciliationPeriodic alarmBackgroundVerified

The primary update path. Update cached state immediately after appending a Fact.

async appendFact(fact: Fact): Promise<Fact> {
// 1. Append to ledger
await this.sql.exec(
`INSERT INTO facts (id, type, timestamp, data) VALUES (?, ?, ?, ?)`,
[fact.id, fact.type, fact.timestamp, JSON.stringify(fact)]
);
// 2. Update cached state inline
await this.updateCachedStateInline(fact);
// 3. Schedule replication
await this.scheduleReplication();
return fact;
}
async updateCachedStateInline(fact: Fact): Promise<void> {
switch (fact.type) {
case 'charge':
await this.incrementBudgetSpent(fact.amount, fact.id);
break;
case 'deposit':
await this.incrementBudgetDeposited(fact.amount, fact.id);
break;
case 'credit_issued':
await this.incrementBudgetCredits(fact.amount, fact.id);
break;
case 'invocation':
await this.incrementCapCount(fact.id);
break;
// ... other types
}
}
async incrementBudgetSpent(amount: number, factId: string): Promise<void> {
await this.sql.exec(`
UPDATE cached_state
SET value = json_set(
value,
'$.spent', json_extract(value, '$.spent') + ?,
'$.remaining', json_extract(value, '$.remaining') - ?,
'$.last_fact_id', ?,
'$.computed_at', ?
)
WHERE key = 'BudgetState'
`, [amount, amount, factId, Date.now()]);
}

Why inline? Single-threaded DO guarantees the append and update are atomic. No race between write and read.

For cached state that may be stale, validate before returning.

async getBudgetState(): Promise<BudgetState> {
const cached = await this.getCachedState('BudgetState');
// Check if any Facts exist after last computed
const newFacts = await this.sql.exec(`
SELECT COUNT(*) as count FROM facts
WHERE id > ? AND type IN ('charge', 'deposit', 'credit_issued')
`, [cached.last_fact_id]);
if (newFacts[0].count > 0) {
// Stale - rebuild
return this.rebuildBudgetState();
}
return cached;
}

Use sparingly. Inline updates should handle the common case. Lazy updates catch edge cases.

Periodic verification that cached state matches ledger truth.

async alarm(): Promise<void> {
const alarmType = await this.storage.get('alarm_type');
switch (alarmType) {
case 'reconciliation':
await this.runReconciliation();
break;
case 'replication':
await this.runReplication();
break;
}
}
async scheduleReconciliation(): Promise<void> {
await this.storage.put('alarm_type', 'reconciliation');
await this.storage.setAlarm(Date.now() + 5 * 60 * 1000); // 5 minutes
}

Reconciliation verifies cached state against the ledger and records any divergence.

async runReconciliation(): Promise<void> {
const stateTypes = ['BudgetState', 'CapState', 'PrepaidBalance'];
for (const stateType of stateTypes) {
await this.reconcileState(stateType);
}
// Schedule next reconciliation
await this.scheduleReconciliation();
}
async reconcileState(stateType: string): Promise<void> {
const startTime = Date.now();
// 1. Get cached value
const cached = await this.getCachedState(stateType);
// 2. Calculate from ledger
const calculated = await this.calculateFromLedger(stateType);
// 3. Compare
if (this.statesMatch(cached, calculated)) {
return; // All good
}
// 4. Record mismatch
await this.recordReconciliationFact(stateType, cached, calculated, startTime);
// 5. Update cache to match ledger
await this.setCachedState(stateType, calculated);
}

Compare significant fields, ignoring metadata:

function statesMatch(cached: BudgetState, calculated: BudgetState): boolean {
return (
cached.deposited === calculated.deposited &&
cached.spent === calculated.spent &&
cached.credits === calculated.credits &&
cached.remaining === calculated.remaining
);
}

Always record when cached state diverges. This is essential for observability.

async recordReconciliationFact(
stateType: string,
cached: CachedState,
calculated: CachedState,
startTime: number
): Promise<void> {
const factsScanned = await this.countFactsForState(stateType);
await this.appendFact({
type: 'reconciliation',
subtype: 'mismatch_detected',
timestamp: Date.now(),
entity_id: this.entityId,
tenant_id: this.tenantId,
data: {
cache_type: stateType,
cached_value: cached,
calculated_value: calculated,
delta: this.computeDelta(cached, calculated),
resolution: 'cache_updated',
facts_scanned: factsScanned,
duration_ms: Date.now() - startTime
}
});
}
function computeDelta(cached: BudgetState, calculated: BudgetState): object {
return {
deposited: calculated.deposited - cached.deposited,
spent: calculated.spent - cached.spent,
credits: calculated.credits - cached.credits,
remaining: calculated.remaining - cached.remaining
};
}

From PRIMITIVES.md:

Fact {
type: "reconciliation",
subtype: "mismatch_detected" | "cache_rebuilt",
timestamp: number,
tenant_id: string,
entity_id: string,
data: {
cache_type: string, // BudgetState, CapState, etc.
cached_value: object, // What the cache had
calculated_value: object, // What the ledger says
delta: object, // The difference
resolution: string, // cache_updated, alert_raised, manual_review
facts_scanned: number, // How many Facts were replayed
duration_ms: number // How long reconciliation took
}
}

Critical invariant: Reconciliation Facts must NOT trigger further reconciliation.

Reconciliation works by comparing cached state against Facts. If reconciliation Facts were included in this comparison, you could create an infinite loop:

1. Reconciliation detects mismatch
2. Reconciliation Fact created
3. Cached state update considers new Fact
4. New Fact causes apparent mismatch
5. LOOP: goto 1

Prevention pattern:

async calculateFromLedger(stateType: string): Promise<CachedState> {
// CRITICAL: Exclude reconciliation Facts from derivation
const relevantTypes = getRelevantFactTypes(stateType);
// Reconciliation Facts are NEVER relevant to any cached state
if (relevantTypes.includes('reconciliation')) {
throw new Error('BUG: reconciliation Facts must not affect cached state');
}
const facts = await this.sql.exec(`
SELECT * FROM facts
WHERE type IN (${relevantTypes.map(() => '?').join(',')})
ORDER BY timestamp
`, relevantTypes);
return deriveState(stateType, facts);
}
function getRelevantFactTypes(stateType: string): string[] {
const mapping = {
'BudgetState': ['charge', 'deposit', 'credit_issued'],
'CapState': ['invocation'],
'SettlementState': ['charge'], // Only charge Facts, filtered by subtype in derivation
// reconciliation is NEVER in any mapping
};
return mapping[stateType] || [];
}

Required test:

test('reconciliation Facts do not affect any cached state', () => {
const allStateTypes = ['BudgetState', 'CapState', 'SettlementState', 'PrepaidBalance', 'AccessState'];
for (const stateType of allStateTypes) {
const relevantTypes = getRelevantFactTypes(stateType);
expect(relevantTypes).not.toContain('reconciliation');
}
});
SituationResolutionAction
Small delta, explainablecache_updatedUpdate cache, log for monitoring
Large delta, unexpectedalert_raisedUpdate cache, trigger alert
Cannot calculatemanual_reviewKeep cache, alert ops team
async determineResolution(
stateType: string,
delta: object
): Promise<string> {
// Define thresholds per state type
const thresholds = {
BudgetState: { remaining: 100 }, // $100 threshold
CapState: { invocation_count: 10 }
};
const threshold = thresholds[stateType];
if (!threshold) return 'cache_updated';
// Check if delta exceeds threshold
for (const [field, maxDelta] of Object.entries(threshold)) {
if (Math.abs(delta[field] || 0) > maxDelta) {
return 'alert_raised';
}
}
return 'cache_updated';
}

Cached state may diverge from ledger truth between reconciliation cycles. Define acceptable drift thresholds and monitor for violations.

State TypeMetricTargetRationale
BudgetStateremaining_delta< $10 or < 1%Small variance acceptable for RTB; large variance = bad routing
CapStatecount_delta< 5 invocationsMinor over/under counting acceptable
SettlementStatepending_total_delta< $100Settlement tracking can tolerate brief lag
AccessStatepermission_mismatch0Security-critical; no tolerance for stale permissions

Metrics to emit (per reconciliation):

interface ReconciliationMetrics {
entity_id: string;
state_type: string;
drift_detected: boolean;
drift_magnitude: number; // Absolute value of largest delta
drift_percentage: number; // Percentage of cached value
reconciliation_duration_ms: number;
facts_scanned: number;
}
async function emitReconciliationMetrics(metrics: ReconciliationMetrics) {
await this.env.ANALYTICS.writeDataPoint({
blobs: [metrics.entity_id, metrics.state_type],
doubles: [
metrics.drift_magnitude,
metrics.drift_percentage,
metrics.reconciliation_duration_ms,
metrics.facts_scanned
],
indexes: [metrics.drift_detected ? 1 : 0]
});
}

Alerting thresholds:

AlertConditionSeverityAction
High drift rate> 5% of reconciliations detect driftWarningInvestigate inline update bugs
Large drift magnitudeBudgetState delta > $100CriticalImmediate investigation
Reconciliation timeoutDuration > 30sWarningReview Fact volume, optimize queries
AccessState driftAny mismatchCriticalSecurity review required

Dashboard queries:

-- Drift rate by state type (last 24h)
SELECT
blob2 AS state_type,
SUM(index1) AS drifts_detected,
COUNT(*) AS total_reconciliations,
SUM(index1) * 100.0 / COUNT(*) AS drift_rate_percent
FROM z0_reconciliation
WHERE timestamp > NOW() - INTERVAL '24 hours'
GROUP BY blob2;
-- Largest drifts (last 24h)
SELECT
blob1 AS entity_id,
blob2 AS state_type,
MAX(double1) AS max_drift_magnitude
FROM z0_reconciliation
WHERE timestamp > NOW() - INTERVAL '24 hours'
AND index1 = 1 -- Only drifts
GROUP BY blob1, blob2
ORDER BY max_drift_magnitude DESC
LIMIT 20;
SeverityResponse TimeActions
Warning4 hoursReview, create ticket
Critical15 minutesPage on-call, investigate immediately
Security (AccessState)ImmediateRevoke and re-grant permissions, audit access logs

Invariants:

// Drift must be detected and recorded
∀ reconciliation WHERE drift_detected → ∃ Fact(reconciliation, mismatch_detected)
// SLO metrics must be emitted
∀ reconciliation → metrics emitted to Analytics Engine
// Critical alerts must be actionable
∀ alert(critical) → runbook exists AND on-call notified

Cached state exists at the intersection of two principles:

PrincipleImplication
Principle 7: Derived State Is DisposableCached state can be wrong. Ledger is truth.
Principle 10: Budget Is EligibilityBudget checks must be fast. RTB can’t wait for ledger replay.
RTB Hot Path (Principle 10) Ledger Truth (Principle 7)
│ │
│ "Trust cached state" │ "Verify against ledger"
│ Fast (O(1) read) │ Slow (O(n) replay)
│ May be stale │ Always correct
│ │
└──────────── Cached State ────────────┘
ContextApproachRationale
RTB eligibility checkTrust cached BudgetStateSpeed trumps perfection. Reconciliation catches drift.
Billing calculationVerify against ledgerAccuracy required. Can afford latency.
Dashboard displayUse cached stateApproximate is acceptable.
Audit/complianceAlways replay ledgerTruth is non-negotiable.
async checkEligibility(amount: number): Promise<boolean> {
// Hot path: trust cached state
const budget = await this.getCachedState('BudgetState');
return budget.remaining >= amount;
}
async calculateBillableAmount(): Promise<number> {
// Cold path: verify against ledger
const charges = await this.getFactsByType('charge');
return charges.reduce((sum, f) => sum + f.amount, 0);
}
QuestionAccept CachedVerify Against Ledger
Can they afford this call?YesNo
How much do we bill this month?NoYes
Have they hit their cap?YesNo
What’s their exact balance?NoYes
Should we route to this buyer?YesNo
Is this user authorized?Yes*For sensitive ops

*AccessState is cached but should be verified for sensitive operations.


Symptom: Cached state has invalid values (negative balance, impossible counts).

Cause: Bug in inline update logic, schema migration error, SQLite corruption.

Resolution:

async handleCorruptedCache(stateType: string): Promise<void> {
// 1. Log the corruption
console.error(`Corrupted ${stateType} detected, rebuilding`);
// 2. Delete corrupted state
await this.sql.exec(`DELETE FROM cached_state WHERE key = ?`, [stateType]);
// 3. Rebuild from ledger
const rebuilt = await this.calculateFromLedger(stateType);
await this.setCachedState(stateType, rebuilt);
// 4. Record reconciliation Fact
await this.appendFact({
type: 'reconciliation',
subtype: 'cache_rebuilt',
data: {
cache_type: stateType,
reason: 'corruption_detected',
facts_scanned: await this.countFactsForState(stateType)
}
});
}

Symptom: Cached state is behind the ledger (missing recent Facts).

Cause: Inline update failed, Facts appended without update, DO restart race.

Resolution: Reconciliation alarm catches this. Cache is updated, divergence recorded.

// Detection during reconciliation
if (cached.last_fact_id !== calculated.last_fact_id) {
// Cache is stale
await this.recordReconciliationFact(stateType, cached, calculated, startTime);
await this.setCachedState(stateType, calculated);
}

Symptom: N/A - this cannot happen.

Why: Single-writer DO guarantees that Fact append and cache update are atomic. No concurrent writes means no divergence during write.

Request A: append Fact → update cache → respond
Request B: (waits) → append Fact → update cache → respond
Never: Request A append + Request B append racing

Cached state is stored in the DO’s SQLite database.

CREATE TABLE cached_state (
key TEXT PRIMARY KEY, -- BudgetState, CapState, etc.
value TEXT NOT NULL, -- JSON blob
computed_at INTEGER NOT NULL, -- Timestamp of last computation
facts_through TEXT -- Last fact_id included
);
-- Example row
INSERT INTO cached_state (key, value, computed_at, facts_through)
VALUES (
'BudgetState',
'{"deposited":10000,"spent":4500,"credits":0,"remaining":5500}',
1699000000000,
'fact_abc123'
);
async getCachedState(key: string): Promise<CachedState | null> {
const row = await this.sql.exec(
`SELECT value, computed_at, facts_through FROM cached_state WHERE key = ?`,
[key]
).first();
if (!row) return null;
return {
...JSON.parse(row.value),
computed_at: row.computed_at,
last_fact_id: row.facts_through
};
}
async setCachedState(key: string, state: CachedState): Promise<void> {
await this.sql.exec(`
INSERT OR REPLACE INTO cached_state (key, value, computed_at, facts_through)
VALUES (?, ?, ?, ?)
`, [
key,
JSON.stringify(state),
state.computed_at || Date.now(),
state.last_fact_id
]);
}

Wrong:

// DON'T: Store calculated totals as Facts
await this.appendFact({
type: 'budget_snapshot',
data: { remaining: budget.remaining } // This is derived!
});

Right:

// DO: Store the event, derive the state
await this.appendFact({
type: 'charge',
amount: 50
});
await this.updateCachedState('BudgetState', { spent: +50 });

Why: Facts are what happened. Derived state is calculation. Mixing them violates Principle 7 and makes reconciliation impossible.

Wrong:

// DON'T: Assume cache is truth for billing
async generateInvoice(): Promise<Invoice> {
const budget = await this.getCachedState('BudgetState');
return { total: budget.spent }; // Cache might be wrong!
}

Right:

// DO: Calculate from ledger for authoritative operations
async generateInvoice(): Promise<Invoice> {
const charges = await this.getFactsByType('charge');
const total = charges.reduce((sum, f) => sum + f.amount, 0);
return { total };
}

Why: Billing errors are expensive. Cache is for speed, ledger is for truth.

Wrong:

// DON'T: Silently fix cache without record
async reconcile(): Promise<void> {
const calculated = await this.calculateFromLedger('BudgetState');
await this.setCachedState('BudgetState', calculated);
// No audit trail!
}

Right:

// DO: Record every reconciliation
async reconcile(): Promise<void> {
const cached = await this.getCachedState('BudgetState');
const calculated = await this.calculateFromLedger('BudgetState');
if (!this.statesMatch(cached, calculated)) {
await this.appendFact({
type: 'reconciliation',
subtype: 'mismatch_detected',
data: { cached, calculated, delta: this.computeDelta(cached, calculated) }
});
}
await this.setCachedState('BudgetState', calculated);
}

Why: Reconciliation Facts are essential for debugging and observability. Silent fixes hide bugs.

4. Skipping Inline Updates for Performance

Section titled “4. Skipping Inline Updates for Performance”

Wrong:

// DON'T: Defer all updates to reconciliation
async appendFact(fact: Fact): Promise<void> {
await this.sql.exec(`INSERT INTO facts ...`);
// Skip inline update, let reconciliation handle it
}

Right:

// DO: Always update inline, reconciliation is backup
async appendFact(fact: Fact): Promise<void> {
await this.sql.exec(`INSERT INTO facts ...`);
await this.updateCachedStateInline(fact); // Always
}

Why: Reconciliation runs every 5 minutes. That’s 5 minutes of stale cache causing wrong routing decisions.

Wrong:

// DON'T: Cache state that spans entities
interface TenantBudgetCache {
total_across_all_accounts: number; // Requires cross-DO coordination
}

Right:

// DO: Cache only single-entity state
interface AccountBudgetState {
remaining: number; // This account only
}
// For cross-entity: query D1 (eventually consistent is acceptable for aggregates)

Why: DOs are per-entity. Cross-entity state requires distributed coordination, which defeats the single-writer benefit.


ConceptImplementation
What cached state isDerived from Facts, disposable, reconcilable
Where it livesDO SQLite cached_state table
How it updatesInline after Fact append
How it verifiesPeriodic reconciliation alarm
When to trust itRTB hot path, dashboards
When to verifyBilling, audit, compliance
On mismatchUpdate cache, record reconciliation Fact

Cached state bridges Principle 7 (Derived State Is Disposable) and Principle 10 (Budget Is Eligibility). It enables fast eligibility checks while maintaining the ledger as the source of truth. Reconciliation ensures drift is detected, recorded, and corrected.