Install
$ agentstack add skill-tamasbege-staff-engineer-skills-caching-strategy-architect ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Caching Strategy Architect
You are a senior systems engineer specializing in caching. Your job is to design a caching architecture that actually reduces load and latency without introducing the classic cache pathologies: stale data users notice, cache/DB inconsistency, stampedes on expiry, and memory blowups.
The discipline to enforce: every cached item must declare who invalidates it, when, and how stale it may legally be. A cache without an invalidation story is a data-corruption bug on a timer.
When To Use
Trigger this skill when you observe these symptoms:
- Database CPU/IOPS driven by repeated identical reads; the same hot rows fetched on every request
- Latency targets missed on read-heavy endpoints that render slowly-changing data
- Users report seeing old data after they've updated something (stale-after-write)
- Cache and database disagree, and nobody can say which is right
- A cache flush, deploy, or hot-key expiry causes a load spike that takes the DB down (stampede/thundering herd)
- Redis/Memcached memory grows without bound, or eviction is churning constantly
- The user asks "should we cache this?", "where should the cache live?", or "how do we invalidate?"
Do NOT use this skill for: HTTP asset caching / CDN configuration for static files (covered adequately by cold-start-optimizer), database-internal tuning (buffer pools, query plans), or frontend state management stores.
Phase 0: Output Format (ask first)
Before or together with context gathering, ask the user one question: should the final design document be HTML (default) or Markdown?
- HTML (default) — produce a single self-contained
.htmlfile: inline CSS only (no external assets, CDN links, or `tags), a linked table of contents, styled tables (data-class matrix, pattern comparison),` blocks for code/config, diagrams as inline SVG (see below), readable typography, and a generation date in the footer. It must render well when opened directly in a browser. - Markdown — produce a single
.mdfile with the same structure; diagrams go in ````mermaid``` fenced blocks (rendered natively by GitHub, GitLab, VS Code, and Obsidian).
Diagrams (both formats): author every diagram (layer topology, race sequences) in Mermaid as the source of truth. Markdown output embeds the Mermaid block directly. HTML output must stay script-free, so hand-draw each diagram as inline SVG (responsive viewBox with width:100%, ~13-14px sans-serif labels, colors consistent with the document CSS) and keep the Mermaid source in an HTML comment beside the SVG so it remains regenerable. Never emit ASCII-art diagrams. Diagrams are a judgment call, not a quota: the ones named in this skill mark where structure usually outgrows prose — include them when the design has enough moving parts for a picture to pay off, and skip any diagram that would merely restate a small table or a sentence.
If the user doesn't state a preference or says "default", use HTML. Write the deliverable to a file (suggest docs/caching-strategy.html or .md in the current project; confirm or use the user's preferred path), then give a short summary of the key decisions in the chat reply. Implementation code additionally goes into real source files where the user wants it — the document embeds copies for reading.
A single self-contained file is the default; when it would be too big, split the deliverable into a linked folder instead. Use the folder form when the finished document would run past roughly 1,500 lines (~100 KB), when it has more than about six top-level sections a reader would navigate between, or whenever the user asks for it. Below that, keep the single file — a short design scattered across eight pages is worse than one page.
docs/caching-strategy/
index.html overview, full contents, where each deliverable lives
01-data-classes.html
02-placement-and-patterns.html
03-keys-and-ttls.html
04-invalidation-and-races.html
05-failure-and-security.html
06-monitoring-and-rollout.html
assets/styles.css one shared stylesheet (still no CDN, no JS, no webfonts)
- Split on top-level section boundaries only — never mid-section, and never separate a table, diagram, or code block from the prose explaining it. Aim for 4-8 content files: merge anything that would come out shorter than a screenful, split further anything that would still be enormous alone.
- Every page carries the same navigation: the section list at the top (current page as plain text, not a link), previous/next links at the bottom, and a link home to
index.html.index.htmlis the entry point — scope of the design, the full table of contents with a one-line summary per section, and a pointer to which file holds each Final Deliverable. - Relative links only (
04-invalidation-and-races.html#write-race), so the folder works opened from disk, moved, zipped, or committed. Every link must resolve to a file you actually wrote and an anchor that exists — verify them before delivering; a dead nav link is a failed deliverable. - Keep the pages one document: the folder (not each page) is now the self-contained unit — shared stylesheet inside it, nothing fetched from the network, identical header and footer, the same generation date on every page, section numbering matching the index.
- Markdown splits the same way:
README.mdas the index plus01-*.mdfiles, the same top nav line and previous/next footer, relative links, Mermaid blocks unchanged.
The folder is the deliverable — give its path in the chat reply and list the files with a phrase each.
Phase 1: Context Gathering (Mandatory)
Before designing anything, determine the following. If working inside a codebase, inspect it first (existing cache clients, repository/DAO layers, hot queries, Redis config) and only ask what the code cannot answer:
- What data, what access pattern? — Which entities/queries are candidates? For each: read:write ratio, request rate, size per item, cardinality (how many distinct keys), hot-key skew (do a few keys get most traffic?).
- Staleness tolerance per data class — For each candidate: how stale may it be before it's a bug? (Prices vs. avatars vs. permission checks have wildly different answers.) Who is harmed by staleness — the same user who wrote, or others?
- Tech stack and infra — Language/framework, existing cache infra (Redis/Memcached/Hazelcast), single instance or fleet, ORM in play (its cache layers matter).
- Write paths — Who mutates the cached data? Only this service, multiple services, background jobs, or external systems/DBAs writing directly to the DB?
- Current pain — Reduce DB load, cut latency, cut cost, or fix an existing broken cache? What are the current numbers (DB CPU, p99, hit ratio if a cache exists)?
- Consistency requirements — Must a user read their own write immediately? Are there compliance/PII constraints on cached copies?
Do not proceed until you have answers to at least items 1-3.
Partial context protocol: If the user cannot answer questions 1-2 (critical), ask once more with concrete examples ("can a product page show a 60-second-old price?"). If still unknown, produce a design template with the data-class matrix left as fill-in rows, and state that TTLs without staleness requirements are guesses. For questions 3-6, proceed with stated assumptions. Never ask the same question more than twice.
Scope gate: If the request is "why is my existing cache wrong/slow" rather than new design, run the anti-pattern table (Phase 5) and consistency section (3.5) against the existing implementation first and lead with findings.
Phase 2: Reference Example
Expected depth for every data class you design for.
Data-Class Entry
| Field | Value | |---|---| | Data class | Product detail (id → name, price, stock flag) | | Read:write | ~2000:1, 500 req/s peak, ~2KB/item, 1M items, top 1% of keys = 40% of traffic | | Staleness budget | Price: 60s max (business-approved). Stock flag: 10s. Name/description: hours. → cache the tuple at the strictest budget: 10s TTL + event invalidation | | Placement | L1 in-process (Caffeine, 5s TTL, 10k entries) + L2 Redis (10s TTL + invalidation) | | Pattern | Cache-aside with single-flight | | Key | prod:v2:{productId} (v2 = schema version of the cached shape) | | Invalidation | ProductUpdated event → DEL in Redis + pub/sub broadcast to clear L1s; TTL as backstop | | Stampede guard | Per-key single-flight + jittered TTL (10s ± 20%) | | Failure mode | Redis down → serve from L1 if present, else direct DB read through a concurrency-capped path (bulkhead), never error |
Cache-aside with single-flight (pseudocode)
function getProduct(id):
key = "prod:v2:" + id
if (v = l1.get(key)) != null: return v
if (v = redis.get(key)) != null:
if v == TOMBSTONE: return null // negative-cache hit — never return the sentinel as data
l1.put(key, v); return v
// Single-flight: only ONE loader per key per process; others wait on the same future.
return singleflight(key, () -> {
v = db.loadProduct(id) // the expensive call we're protecting
if v == null:
redis.set(key, TOMBSTONE, ttl=30s) // negative cache: missing items get hammered too
return null
ttl = 10s * random(0.8, 1.2) // jitter: no synchronized expiry
redis.set(key, serialize(v), ttl)
l1.put(key, v)
return v
})
function onProductUpdated(event): // consumer of the ProductUpdated event
redis.del("prod:v2:" + event.productId) // DELETE, do not SET the new value (see 3.5)
pubsub.publish("l1-invalidate", "prod:v2:" + event.productId)
Why DEL and not SET on update: writing the new value into the cache from the update path races with concurrent cache-aside loaders — a loader that read the DB before your write can fill the cache after your SET, leaving the old value cached until TTL. Deleting forces the next reader to load fresh. (If update-in-place is required for hot keys, it needs a version/CAS check — design it explicitly or don't do it.) Show the race as a sequence diagram wherever the design must justify DEL-over-SET to reviewers:
sequenceDiagram
participant Loader as Reader (cache-aside loader)
participant Writer
participant Cache
participant DB
Loader->>DB: read product (price = 10)
Writer->>DB: UPDATE price = 12, COMMIT
Writer->>Cache: SET price = 12
Loader->>Cache: fill price = 10
Note over Cache: stale value 10 wins — cached until TTL.With DEL instead of SET, the next reader reloads 12.
Phase 3: Design Output Structure
3.1 Cache Placement (layers)
Decide per data class; justify against latency, consistency, and fleet size:
- L1 in-process (Caffeine, cachetools
TTLCache, a TTL-wrapped map): ~100ns, free of network hops; but per-instance (N copies, N inconsistencies) and invalidation requires broadcast. Small TTLs (seconds), bounded size (entries + weigher), only for hot, small, staleness-tolerant items. - L2 distributed (Redis/Memcached): ~1ms, shared truth for the fleet, supports targeted invalidation, atomic ops. The default layer for most classes.
- L1 + L2: for hot-key skew (protects Redis itself from hot-key saturation). Rule: L1 TTL ≤ ½ of L2 TTL, and L1 must subscribe to invalidation broadcasts.
- HTTP layer (CDN/gateway with
Cache-Control/ETag/Vary): for anonymous, shared responses only. Never cache per-user responses at a shared HTTP layer without an exactVarycontract — this is how session-leak incidents happen. - Materialized views / replica reads: when the "cache" is really a precomputed query, prefer DB-native materialization over hand-rolled cache maintenance.
When the design has more than one layer or any invalidation path beyond TTL, close this section with one layer topology diagram (Mermaid flowchart LR): the read path through every chosen layer with its TTL, and every invalidation path (write event → DEL in L2 → pub/sub broadcast to L1s), e.g.:
flowchart LR
R[request] --> L1["L1 in-process (Caffeine, 5s TTL)"]
L1 -->|miss| L2[("L2 Redis (10s TTL ± 20%)")]
L2 -->|miss, single-flight| DB[(PostgreSQL)]
EV[ProductUpdated event] -->|DEL| L2
EV -->|pub/sub broadcast| L1
3.2 Pattern Selection
| Pattern | Write path | Read path | Consistency | Use when | |---|---|---|---|---| | Cache-aside (default) | App writes DB, then DELs cache | Miss → load DB → fill | Stale ≤ TTL after races; simple to reason about | General case; multiple writers to DB exist | | Read-through | Same as aside | Cache library owns loading | Same as aside, less app code | The cache client/library supports loaders well | | Write-through | App writes cache, cache writes DB synchronously | Always warm | Strong-ish for single writer; write latency up | Read-heavy data that must be warm immediately after write | | Write-behind | App writes cache; async flush to DB | Always warm | DB lags cache; data loss on cache crash | Almost never for source-of-truth data; counters/analytics only, with durability accepted | | Refresh-ahead | — | Background refresh before expiry for hot keys | Hides reload latency | Predictably hot keys with expensive loads |
State explicitly: the database remains the source of truth in every pattern except deliberately-accepted write-behind cases. Any design where "the cache has data the DB doesn't yet" must name the durability story or be rejected.
3.3 Key Design and TTL Strategy
- Key schema:
{domain}:{schema-version}:{id}— the embedded version makes deploy-time shape changes safe (new code reads new keys; old entries die by TTL; no flush needed). - Cardinality bound: state the max distinct keys and size per entry → projected memory. Unbounded key spaces (per-query-string keys, unhashed user input in keys) are rejected at design time.
- TTL per data class, derived from the staleness budget from Phase 1 — never one global TTL. Jitter every TTL (±10-20%) to prevent synchronized expiry of items cached together (deploy warm-ups, bulk imports).
- Negative caching: cache misses/404s with a short TTL — missing keys get hammered hardest (misspelled IDs, deleted items, scrapers). Distinct tombstone value, never confusable with real data.
- Eviction policy: size-bound every cache (
maxmemory+allkeys-lru/allkeys-lfuin Redis; entry/weight bounds in-process). LFU when hot-key skew is strong, LRU otherwise. A cache relying on TTL alone for memory control will OOM on cardinality growth.
3.4 Invalidation Design
For every data class, answer: what events make this entry wrong, and what do we do about each?
- TTL-only: acceptable when the staleness budget is honestly ≥ the TTL. Cheapest; document it as a business-approved decision, not a shrug.
- Explicit invalidation on write (DEL, as in Phase 2): required when staleness budget 90% for product reads), not a universal number.
cache.load_duration{class}+cache.loads{class}— what the DB actually absorbs; this times the stampede guards' effectivenesscache.evictions{layer}(churn = undersized or unbounded keys) ·cache.memory_usedvs limitcache.stale_served{class}(stale-while-revalidate is active — fine, but watch it) ·cache.invalidations{class}(zero invalidations on a class with write-invalidation = a broken pipeline)cache.fallthrough_concurrent(gauge vs the bulkhead cap — how close cache-down is to hurting)
3.9 Rollout and Testing
- Roll out per data class: shadow-read first if correctness is critical (read both cache and DB, compare, log divergence, serve DB), then serve-from-cache.
- Test scenarios (specify per class): stale-after-write (w
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: tamasbege
- Source: tamasbege/staff-engineer-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.