Reference solution
The findings below are ordered by priority.
Findings
1. Shared cache keys collide across tenants
Location: src/migrating-project-cache.ts:49-51
The shared key discards tenantId and uses only the project ID. Two tenants with the same project ID read and overwrite one cache entry, which can return one customer's project data to another customer.
Include the normalized tenant and project identities in every shared key, under a stable namespace, and keep the same identity semantics in all cache implementations.
2. Invalidation leaves stale fallback entries that can reappear
Location: src/migrating-project-cache.ts:80-82
delete clears only the shared backend. The process-local fallback retains its old value; the next shared miss reads that fallback at lines 57-63 and resurrects the pre-rename project until its long local TTL expires.
Invalidate both layers as one best-effort operation. A shared miss after a mutation must not make an older fallback value authoritative again.
3. Shared-cache errors fail otherwise valid project requests
Location: src/migrating-project-cache.ts:53-81 and src/project-service.ts:11-35
Shared get, setEx, and delete calls are awaited without recovery. An outage prevents reads from reaching the local fallback or repository, turns a successful repository lookup into an error during cache fill, and can make a completed rename appear failed during invalidation.
Treat each cache operation as best effort: recover from shared errors, use the fallback and repository as appropriate, and report cache health separately without changing the repository-backed request result.
4. Millisecond TTLs are passed to an API that expects seconds
Location: src/migrating-project-cache.ts:66-77
The ProjectCache abstraction supplies ttlMilliseconds, but SharedCacheClient.setEx expects seconds. Passing the number unchanged keeps shared entries for roughly 1,000 times the intended lifetime, increasing stale-data exposure after the fallback window.
Convert the duration at the adapter boundary, with an explicit rounding policy, while continuing to pass milliseconds to the process-local implementation.
5. Negative cache entries are treated as misses
Location: src/project-service.ts:11-18
The cache contract distinguishes null (a cached not-found result) from undefined (no entry), but the truthiness check rejects both. Every request for a missing project therefore reloads the repository despite a valid negative entry.
Check specifically for cached !== undefined and return the cached null until its TTL expires.
6. The fake bypasses the migration semantics under review
Location: src/project-service.test.ts:6-44
The fake keys only by project ID, never expires entries, and tests ProjectService without the shared/fallback composition. The happy-path assertion can pass while tenant isolation, TTL conversion, fallback invalidation, negative caching, and outage behavior are all broken.
Add adapter and migration tests with two tenants sharing a project ID, a controllable clock, both cache layers, injected shared failures, and cached not-found values.
Reasonable non-findings
- Lazy expiry in
ProcessLocalProjectCache is acceptable; eager cleanup and a maximum entry count are outside this exercise's contract.
- Serializing a cached
null as JSON preserves a distinct negative entry. The defect is the service's truthiness check, not JSON serialization.
- The shared-first, local-fallback read order is the intended rollout order when the shared backend returns a normal miss.
- Cache payloads are written only by this adapter and guaranteed to contain valid JSON, so malformed payload recovery is out of scope.