cache-architect
Designs the application cache of an already-scoped use case — which reads are served from a copy, the key of each (every input that changes the result, the caller included), Caffeine or Redis, expiry
SKILL.md.claude/skills/cache-architect/SKILL.md
GitHubAvailable specs
!find "${CLAUDE_PROJECT_DIR:-.}/docs/use-cases" -mindepth 1 -maxdepth 1 -type d -name 'UC-*' 2>/dev/null | sort
Empty above → none yet, run /use-case-design first. (find, not an ls glob: under zsh an unmatched glob
aborts the command before any fallback runs.)
Target
$ARGUMENTS
Cache Architect
Designs which reads are answered from a copy, and how the copy stays honest: the key that
keeps one caller's answer away from another, where the copy lives, how long it may live, who
must throw it away when the data changes, and what happens when the cache is gone. The business
already said how stale an answer may be — that is the Freshness row of 00-use-case.md. This
skill turns that number into a cache, or into the finding that no cache is needed.
Entry rule: no tolerance, no cache. This skill reads
docs/use-cases/UC-NNN-<slug>/00-use-case.md and runs only when its Freshness row states a
tolerance — a duration an answer may be old ("a balance up to 30 seconds old"). The row reads
none (always current), or is missing → stop and say so: a cache without a stated tolerance is a
staleness bug waiting for its first write, and "the query looks slow" is a reason to measure, not a
tolerance (@.claude/rules/caching.md § Boundary). 10-domain.md is required too: the cache
decorates an outbound port it declares, and without one there is nothing to wrap.
Exit rule: it doesn't write code. It emits 22-cache.md. The Java classes come from the
executor agent, which reads the partial and the templates/ exemplars.
Rule rule: the rules don't live here. Boundary, keys, values, expiry, invalidation, failure,
metrics and tests are @.claude/rules/caching.md. This skill applies and cites it; it doesn't
reproduce it.
How it's invoked
Two paths: /cache-architect by hand, or chained by /new-feature after persistence and before
tests. That's why it does not carry disable-model-invocation — a skill the model can't see is
a skill the orchestrator can't call (@.claude/decisions/0007-pipeline-skills-invocation.md). The
guard against firing out of order is the entry rule above.
Why this is a skill and not a subagent
Form 1, motivated by axis 1 of the designer's interview: no piece owned application caching, and
the maintainer chose to design it ahead of a run that shows the defect, on the pitfalls the
official documentation records (references/pitfalls.md). The closest rejected form was the same
skill with its norms in references/ — the executor never loads a design skill's references, so
each partial would restate them. A subagent fails the counter-test on all three points: the
interview is the task, the reference fits in references/ and templates/, and the partial is
short.
Pinned to opus: the partial is what the executor implements verbatim, and the pin holding for
the rest of the turn keeps /new-feature on the model that designed it
(@.claude/decisions/0081-skill-model-required-per-class.md). Design record:
@.claude/decisions/0144-cache-architect-skill.md (this repository only).
Boundary with neighboring skills
| Piece | Decides | Never decides |
|---|---|---|
use-case-design |
The Freshness tolerance — a business fact, asked in business words |
Whether a cache exists, its provider, its expiry |
domain-modeling |
The outbound port and the record it returns | That the port is cached |
persistence-architect / http-client-architect |
The adapter that implements the port, the query or the call behind it | Anything about the copy |
jobs-architect |
The replica count (35-jobs.md § 3), any warm-up or refresh job |
Expiry, keys |
messaging-architect |
A broadcast channel, if a deferred two-level cache ever needs one | The cache itself |
| this skill | Which reads are cached, each key, provider, expiry and size per cache, every writer that must evict, failure and load, serialization, metrics, the redis compose row |
The tolerance, the port, the query, the tests |
test-architect |
Which test proves each cached read, at what level | — |
rest-api-architect |
HTTP response caching (Cache-Control, ETag) |
Anything inside the service |
The replica count has one owner per project, and this skill inherits it like
jobs-architect does; asked only when no partial recorded it.
Procedure
Read the specs.
00-use-case.md(theFreshnessrow and the Components table),23-search.mdwhen the cached port is of kindsearch index— its § 6 lag comes out of the tolerance first, and only the remainder bounds this cache's expiry; a remainder of zero or less means no cache, recorded as such —10-domain.md(the outbound ports and the records they return),20-persistence.mdand28-http-client.mdwhen present (the adapter behind each port),35-jobs.mdwhen present. Apply the entry rule. List the reads the tolerance covers, each as a port method.Survey — every answer the project already gave is inherited, never re-asked.
# Provider already in the build grep -nE "spring-boot-starter-cache|caffeine|spring-boot-starter-data-redis" pom.xml # Caches already in the code grep -rnE "@(Cacheable|CacheEvict|CachePut|Caching)\b|@EnableCaching|CacheManager" --include='*.java' src/main 2>/dev/null # Decisions earlier use cases recorded find docs/use-cases -name '22-cache.md' 2>/dev/null grep -rn "Replicas\|Claim strategy" docs/use-cases/*/35-jobs.md docs/use-cases/*/20-persistence.md docs/use-cases/*/22-cache.md 2>/dev/nullAnd the active blueprint's
packages.mapentry ending in.cache— the package every decorator and the cache wiring go to. No such entry → stop and say which line is missing from the blueprint, instead of choosing a package inside this use case.Fact Inherited from Asked only when Provider A dependency in pom.xml, or an earlier22-cache.md§ 2Neither exists — first cache of the project Replica count An earlier 22-cache.md§ 2,35-jobs.md§ 3, else20-persistence.md§ 1Claim strategyNone exists Cache wiring ( @EnableCaching, manager, error handler)Already in the cache package First cache of the project A cache annotation outside the cache package is not reuse. A project that annotated a persistence adapter or a service before this skill existed has caching the rule does not allow (
@.claude/rules/caching.md§ Boundary). Record it in## Impact on approved use casesand decide with the user between moving it behind a decorator in this run and deferring that with an owner — never a second cache on the same read.Interview — only what steps 1 and 2 did not settle.
AskUserQuestion, at most 4 questions per call and never fewer than 2 real options per question; an axis with one sensible answer is decided and recorded, not asked (@CLAUDE.md§ Known pitfalls). Every option states its cost — the server Redis adds, the drift a local cache accepts.Axis Decides Skip when Replicas — how many instances serve this read in production? Provider (§ 2) Step 2 inherited it Who else writes this data — another use case, a job, a consumer, a bulk statement, another service? § 4, one row per writer The Components tables of every approved case already answer it — read them and list, then ask only "anything else?" Does the result depend on who asks — the caller, the tenant, a role, the locale? The key (§ 1) — and, when the port does not take that input as a parameter, a divergence: 10-domain.mdisdomain-modeling's to change, and the read stays uncached until it doesThe port method already takes every such input Without the cache — can the source take the full read rate, or must load be shed? § 5 Nothing in the case suggests the source is the bottleneck — record "holds" and why Absence — is "not found" asked often, and may it be served stale? Whether absence is cached, with its own expiry The read never returns absence Choose the provider. Apply
references/provider-decision-matrix.md§ 2 to the answers: the first row that matches wins; a tie goes to the provider already inpom.xml, then to Caffeine. One provider per project (@.claude/rules/caching.md§ Boundary) — a case whose needs the project's provider cannot meet is a divergence to report and ask about, never a second one added silently. Record the winner, the row that decided it, and one line per rejected option — the Hibernate second-level cache among them when the read is entity-by-id.Design each cached read. One row of § 1 per read, and every column is filled:
Column Content Read The port method — the decorator implements that port Cache name One per kind of value, kebab-case, added to CacheNamesKey The SpEL expression, naming every input that changes the result — the caller included when the result depends on it, always as a port parameter, never read from a context Value The record the port returns, when it is a record of plain components; otherwise the cache-local …CacheEntryrecord the decorator maps toFreshness tolerance Copied from 00-use-case.md— the bound for § 3; for a search projection, the tolerance minus23-search.md§ 6's lagAbsence cached no, or yes with its own expiry Concurrent misses sync, or per caller — neversyncwithunless(the framework refuses it)The form is fixed:
templates/CachedPortAdapter.java.example— a decorator that implements the port, depends on the port, and is wired as its @Primary bean by the delegate's bean name. A read hot enough that a miss is felt, with a tolerance that covers serving the old value for one refresh, takestemplates/RefreshAheadCache.java.exampleinstead.Size expiry and size — § 3. Per cache: the expiry, never longer than the tolerance; the maximum size for a local cache, from the number of distinct keys the case expects; jitter on Redis; refresh ahead only per step 5. Never one setting for all caches.
List every writer — § 4. Every writer of the data each cache holds, from step 3: whether it goes through the decorated port (evicts by construction, after commit), evicts explicitly, or bypasses the cache — and then the expiry is its staleness bound, written as accepted or flagged. A writer in an approved case that must change goes to
## Impact on approved use cases.Failure, load, serialization, metrics — § 5 to § 7. The error handler and the client timeout every project gets (
CountingCacheErrorHandler,application-cache.yml.example); what the source does without the cache, from step 3; for Redis, the typed serializer per cache and the format version; the metrics and the alarm each exists for.Declare dependencies — § 8.
spring-boot-starter-cache, plus Caffeine orspring-boot-starter-data-redis, versions empty — the Boot parent manages them, and a version is never written from memory (@CLAUDE.mdinvariant 8). The executor may writepom.xmlfor exactly these rows.
9b. Decide the design patterns of this layer. Run
@.claude/skills/gof-design-patterns/SKILL.md § Design-time use over the decorators this case
adds — the Decorator over the port is the shape, recorded as such. The answer goes into
## Design patterns — none is not valid when a decorator is written.
Check Redis has a container — Redis only. List the keys inside the
services:block:awk '/^services:[[:space:]]*$/{s=1;next} /^[^[:space:]#]/{s=0} s&&/^ [A-Za-z0-9_.-]+:[[:space:]]*(#.*)?$/{sub(/[[:space:]]*#.*$/,"");print}' docker-compose.ymlWrite the answer as a row of
## Compose services, shaped and decided by@.claude/skills/docker-architect/references/compose-service-decision.md—existswhenredisis listed, otherwise the question that reference owns. Anexternalanswer makes that address the host default ofspring.data.redis.hostin § 9. Caffeine →none. Do not invokedocker-architectand do not editdocker-compose.yml: a design run produces a spec, never a container.Write the partial.
docs/use-cases/UC-NNN-<slug>/22-cache.md, fromtemplates/cache-spec.md.example. Ten numbered blocks plus## Compose servicesand## Design patterns, all mandatory, each written asnonewhen there is nothing — absence is notnone. § 10Deferredfollows the same shape as every other partial's.Report and stop. Path of the file written; the provider and the row that chose it; the replica count and where it came from; each cached read with its key and expiry next to its tolerance; every writer of § 4 whose staleness bound is the expiry; the
## Compose servicesrow. A writer nobody could name, or a result that depends on the caller through a port that does not take the caller, is a finding. Don't invoke anyone else:test-architectruns next and reads § 1 and § 4.
What the partial contains
| Block | Fixes |
|---|---|
| 1 · Cached reads | One row per read, every column of step 5 |
| 2 · Provider | The provider, the matrix row that chose it, the rejected ones in one line each, transaction awareness, and whether it was inherited |
| 3 · Expiry and size | Per cache: expiry next to its tolerance, maximum size, jitter, refresh ahead |
| 4 · Invalidation | Every writer of the cached data, its path, its eviction, its staleness bound |
| 5 · Failure and load | Error handler, client timeout, what the source does without the cache |
| 6 · Serialization | Per cache: serializer and format version (Redis); none for a local cache of immutable records |
| 7 · Observability | Metric names, and the alarm each one exists for |
| 8 · Declared dependencies | Coordinates, version empty when managed |
| 9 · Configuration | Every property the cache reads, its value, and which partial owns the value |
| 10 · Deferred | Same shape as every partial. none when nothing was deferred |
| Compose services | The redis row, or none for a local cache |
| Design patterns | The Decorator over each port, with its classes — step 9b |
Plus ## Impact on approved use cases and ## Implementation order, as in every partial.
The exemplars in templates/ are reference for form, not files to copy:
| Exemplar | Shape of |
|---|---|
templates/CaffeineCacheConfig.java.example |
Local provider; also CacheSettings, CacheNames, CountingCacheErrorHandler, shared by both providers |
templates/RedisCacheConfig.java.example |
Distributed provider: typed serializer per cache, format version, jitter, SCAN, transaction-aware |
templates/CachedPortAdapter.java.example |
The decorator over a persistence port (read + evicting write) and over an HTTP port (read only) |
templates/RefreshAheadCache.java.example |
One hot read, refreshed ahead of expiry |
templates/application-cache.yml.example |
Redis connection with short timeouts and the password placeholder; app.cache.* per cache |
templates/cache-spec.md.example |
The partial |
Contract
Class: design — the territory is skill_classes.design in
@.claude/schemas/extensions.json, and ArchHook.java guard enforces it. Writes inside the use
case folder and nothing else.
Reads docs/use-cases/UC-NNN-<slug>/00-use-case.md (mandatory), 10-domain.md
(mandatory), 20-persistence.md, 28-http-client.md and 35-jobs.md (when present), the
Components tables and 22-cache.md of earlier cases, pom.xml, docker-compose.yml, the active
blueprint's packages.map, @.claude/rules/caching.md, @.claude/rules/architecture-ddd.md,
@.claude/rules/observability.md, @.claude/rules/secrets.md,
@.claude/skills/docker-architect/references/compose-service-decision.md, and this skill's
references/.
Writes docs/use-cases/UC-NNN-<slug>/22-cache.md. Nothing else.
Owns which reads are cached and the key of each, the cache provider of the project, expiry and
size per cache, the list of writers that must evict, failure and load handling of the cache, the
cache serialization and format version, cache metrics, and the redis compose row.
Does not decide the freshness tolerance (00-use-case.md), the port or the record it
returns (10-domain.md), the query or the call behind the port (20-persistence.md,
28-http-client.md), the replica count when a partial already recorded it, or the tests
(40-tests.md). Does not write Java, pom.xml, application.yml or docker-compose.yml — the
executor and docker-architect do, from this partial. Doesn't touch .claude/rules/**.