- State & Storage
- Patterns
Patterns
Common Patterns
Section titled “Common Patterns”Practical recipes for common KV Store use cases.
A write to a bucket that does not exist returns 404. Create each bucket first, as the caching and session recipes do. The feature-flag and counter recipes assume feature-flags and counters already exist.
Caching with TTL
Section titled “Caching with TTL”Use TTL buckets to cache computed or fetched data. Keys auto-expire after the configured duration, so stale data is cleaned up automatically. On a cache miss, recompute the value and store it for future reads.
import { createClient, IronflowError } from "@ironflow/node";
const client = createClient({ serverUrl: "http://localhost:9123" });const kv = client.kv();
// Create a bucket with 5-minute TTLawait kv.createBucket({ name: "cache", ttlSeconds: 300 });const cache = kv.bucket("cache");
async function getCached(key: string, compute: () => Promise<unknown>) { try { const entry = await cache.getJSON(key); return entry.value; } catch (error) { if (!(error instanceof IronflowError)) throw error; // A missing key or an entry that is not JSON is a miss: recompute and overwrite it. if (error.status !== 404 && error.code !== "INVALID_KV_JSON") throw error; const value = await compute(); const encoded = JSON.stringify(value); if (encoded !== undefined) await cache.put(key, encoded); return value; }}
const profile = await getCached("user.123.profile", async () => { return fetchUserProfile("123");});import { ironflow, IronflowError } from "@ironflow/browser";
ironflow.configure({ serverUrl: "http://localhost:9123" });const kv = ironflow.kv();
// Create a bucket with 5-minute TTLawait kv.createBucket({ name: "cache", ttlSeconds: 300 });const cache = kv.bucket("cache");
async function getCached(key: string, compute: () => Promise<unknown>) { try { const entry = await cache.getJSON(key); return entry.value; } catch (error) { if (!(error instanceof IronflowError)) throw error; // A missing key or an entry that is not JSON is a miss: recompute and overwrite it. if (error.status !== 404 && error.code !== "INVALID_KV_JSON") throw error; const value = await compute(); const encoded = JSON.stringify(value); if (encoded !== undefined) await cache.put(key, encoded); return value; }}
const profile = await getCached("user.123.profile", async () => { return fetchUserProfile("123");});client := ironflow.NewClient(ironflow.ClientConfig{ ServerURL: "http://localhost:9123",})kv := client.KV()
// Create a bucket with 5-minute TTLkv.CreateBucket(ctx, ironflow.BucketConfig{ Name: "cache", TTL: 5 * time.Minute,})cache := kv.Bucket("cache")
// Try cache first_, err := cache.Get(ctx, "user.123.profile")if err != nil { // Cache miss — compute and store data := computeProfile(ctx, "123") cache.Put(ctx, "user.123.profile", data)}from ironflow import IronflowClient, IronflowError
client = IronflowClient(server_url="http://localhost:9123")
# Create a bucket with 5-minute TTLclient.kv_buckets(body={"name": "cache", "ttl_seconds": 300})
# Try cache firsttry: entry = client.kv_get_buckets_keys("cache", "user.123.profile")except IronflowError: # Cache miss — compute and store data = compute_profile("123") client.kv_update_buckets_keys("cache", "user.123.profile", body=data)Feature Flags
Section titled “Feature Flags”Store feature flags as JSON values in a dedicated bucket. Read flags at runtime to control application behavior. Updates take effect immediately without redeployment.
import { createClient } from "@ironflow/node";
const client = createClient({ serverUrl: "http://localhost:9123" });const flags = client.kv().bucket("feature-flags");
// Set a flagawait flags.put("dark-mode", { enabled: true, rollout: 0.5 });
// Check a flagconst flag = await flags.get("dark-mode");const flagData = JSON.parse(Buffer.from(flag.value as string, "base64").toString("utf-8"));if (flagData.enabled) { enableDarkMode();}
// List all flagsconst keys = await flags.listKeys();for (const key of keys) { const entry = await flags.get(key); console.log(`${key}: ${Buffer.from(entry.value as string, "base64").toString("utf-8")}`);}import { ironflow } from "@ironflow/browser";
ironflow.configure({ serverUrl: "http://localhost:9123" });const flags = ironflow.kv().bucket("feature-flags");
// Set a flagawait flags.put("dark-mode", { enabled: true, rollout: 0.5 });
// Check a flagconst flag = await flags.get("dark-mode");const flagData = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(flag.value as string), (c) => c.charCodeAt(0))));if (flagData.enabled) { enableDarkMode();}
// Watch for flag changes in real timeflags.watch({ onUpdate: (event) => { console.log(`Flag changed: ${event.key}`); applyFlag(event.key, JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(event.value as string), (c) => c.charCodeAt(0))))); },});client := ironflow.NewClient(ironflow.ClientConfig{ ServerURL: "http://localhost:9123",})flags := client.KV().Bucket("feature-flags")
// Set a flagflags.Put(ctx, "dark-mode", []byte(`{"enabled":true,"rollout":0.5}`))
// Check a flagentry, err := flags.Get(ctx, "dark-mode")if err != nil { log.Fatal(err)}
var flag struct { Enabled bool `json:"enabled"` Rollout float64 `json:"rollout"`}json.Unmarshal(entry.Value, &flag)
if flag.Enabled { enableDarkMode()}import base64import json
from ironflow import IronflowClient
client = IronflowClient(server_url="http://localhost:9123")
# Set a flagclient.kv_update_buckets_keys("feature-flags", "dark-mode", body={"enabled": True, "rollout": 0.5})
# Check a flag — reads come back base64-encodedentry = client.kv_get_buckets_keys("feature-flags", "dark-mode")flag = json.loads(base64.b64decode(entry["value"]))
if flag["enabled"]: enable_dark_mode()Session State
Section titled “Session State”Store per-user session data in a TTL bucket so sessions expire automatically. Use key naming conventions like user.<id> to organize entries.
import { createClient } from "@ironflow/node";
const client = createClient({ serverUrl: "http://localhost:9123" });const kv = client.kv();
// Sessions expire after 1 hourawait kv.createBucket({ name: "sessions", ttlSeconds: 3600 });const sessions = kv.bucket("sessions");
// Store session dataawait sessions.put("user.abc-123", { userId: "abc-123", role: "admin", lastActiveAt: new Date().toISOString(),});
// Retrieve sessiontry { const session = await sessions.get("user.abc-123"); const sessionData = JSON.parse(Buffer.from(session.value as string, "base64").toString("utf-8")); console.log(sessionData.role); // "admin"} catch { // Session expired or doesn't exist redirectToLogin();}
// Refresh session on activity (resets TTL)await sessions.put("user.abc-123", { ...existingSession, lastActiveAt: new Date().toISOString(),});
// Explicit logoutawait sessions.delete("user.abc-123");import { ironflow } from "@ironflow/browser";
ironflow.configure({ serverUrl: "http://localhost:9123" });const kv = ironflow.kv();
// Sessions expire after 1 hourawait kv.createBucket({ name: "sessions", ttlSeconds: 3600 });const sessions = kv.bucket("sessions");
// Store session dataawait sessions.put("user.abc-123", { userId: "abc-123", role: "admin", lastActiveAt: new Date().toISOString(),});
// Retrieve sessiontry { const session = await sessions.get("user.abc-123"); const sessionData = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(session.value as string), (c) => c.charCodeAt(0)))); console.log(sessionData.role); // "admin"} catch { // Session expired or doesn't exist redirectToLogin();}
// Explicit logoutawait sessions.delete("user.abc-123");client := ironflow.NewClient(ironflow.ClientConfig{ ServerURL: "http://localhost:9123",})kv := client.KV()
// Sessions expire after 1 hourkv.CreateBucket(ctx, ironflow.BucketConfig{ Name: "sessions", TTL: time.Hour,})sessions := kv.Bucket("sessions")
// Store session datadata, _ := json.Marshal(map[string]any{ "userId": "abc-123", "role": "admin", "lastActiveAt": time.Now().Format(time.RFC3339),})sessions.Put(ctx, "user.abc-123", data)
// Retrieve sessionentry, err := sessions.Get(ctx, "user.abc-123")if err != nil { // Session expired or doesn't exist redirectToLogin() return}
var session map[string]anyjson.Unmarshal(entry.Value, &session)fmt.Println(session["role"]) // "admin"
// Explicit logoutsessions.Delete(ctx, "user.abc-123")import base64import jsonfrom datetime import datetime, timezone
from ironflow import IronflowClient, IronflowError
client = IronflowClient(server_url="http://localhost:9123")
# Sessions expire after 1 hourclient.kv_buckets(body={"name": "sessions", "ttl_seconds": 3600})
# Store session dataclient.kv_update_buckets_keys("sessions", "user.abc-123", body={ "userId": "abc-123", "role": "admin", "lastActiveAt": datetime.now(timezone.utc).isoformat(),})
# Retrieve sessiontry: entry = client.kv_get_buckets_keys("sessions", "user.abc-123")except IronflowError: # Session expired or doesn't exist redirect_to_login()else: session = json.loads(base64.b64decode(entry["value"])) print(session["role"]) # "admin"
# Explicit logoutclient.kv_delete_buckets_keys("sessions", "user.abc-123")Optimistic Locking
Section titled “Optimistic Locking”Use compare-and-swap (update) for concurrent-safe modifications. Read the current value and its revision, apply your change, then write back with the original revision. If another writer modified the key in between, the update fails and you retry.
import { createClient, IronflowError } from "@ironflow/node";
const client = createClient({ serverUrl: "http://localhost:9123" });const counters = client.kv().bucket("counters");
// Initialize counterawait counters.put("page-views", 0);
async function incrementCounter(key: string): Promise<number> { while (true) { const entry = await counters.getJSON(key); if (typeof entry.value !== "number") throw new Error("Expected a counter number"); const count = entry.value + 1; try { await counters.update(key, count, entry.revision); return count; } catch (error) { if (!(error instanceof IronflowError) || error.status !== 412) throw error; // Revision mismatch — another writer won, retry continue; } }}
const newCount = await incrementCounter("page-views");console.log(`Page views: ${newCount}`);import { ironflow, IronflowError } from "@ironflow/browser";
ironflow.configure({ serverUrl: "http://localhost:9123" });const counters = ironflow.kv().bucket("counters");
// Initialize counterawait counters.put("page-views", 0);
async function incrementCounter(key: string): Promise<number> { while (true) { const entry = await counters.getJSON(key); if (typeof entry.value !== "number") throw new Error("Expected a counter number"); const count = entry.value + 1; try { await counters.update(key, count, entry.revision); return count; } catch (error) { if (!(error instanceof IronflowError) || error.status !== 412) throw error; // Revision mismatch — another writer won, retry continue; } }}
const newCount = await incrementCounter("page-views");console.log(`Page views: ${newCount}`);client := ironflow.NewClient(ironflow.ClientConfig{ ServerURL: "http://localhost:9123",})counters := client.KV().Bucket("counters")
// Initialize countercounters.Put(ctx, "page-views", []byte("0"))
func incrementCounter(ctx context.Context, bucket *ironflow.KVBucket, key string) (int, error) { for { entry, err := bucket.Get(ctx, key) if err != nil { return 0, err }
count, _ := strconv.Atoi(string(entry.Value)) count++
_, err = bucket.Update(ctx, key, []byte(strconv.Itoa(count)), entry.Revision) if err != nil { // Revision mismatch — another writer won, retry continue } return count, nil }}
newCount, _ := incrementCounter(ctx, counters, "page-views")fmt.Printf("Page views: %d\n", newCount)import base64import json
from ironflow import IronflowClient, IronflowError
client = IronflowClient(server_url="http://localhost:9123")
# Initialize counterclient.kv_update_buckets_keys("counters", "page-views", body=0)
def increment_counter(bucket: str, key: str) -> int: while True: entry = client.kv_get_buckets_keys(bucket, key) count = json.loads(base64.b64decode(entry["value"])) + 1 try: client.kv_update_buckets_keys(bucket, key, body=count, if_match=str(entry["revision"])) except IronflowError as err: if err.status_code == 412: continue # Revision mismatch — another writer won, retry raise return count
new_count = increment_counter("counters", "page-views")print(f"Page views: {new_count}")