# Architecture Decision Records — cache-valkey ## ADR-001: Flat package (no sub-packages) **Status:** Accepted All three adapters (PermissionCache, RateLimiterStore, Blacklist) wrap the same `Provider` object and cannot function without the connection. Splitting into sub-packages (e.g., `cachevalkey/permcache`) would add import verbosity without providing any isolation benefit — every caller still needs the `Component` from `New()` before constructing any adapter. Consistent with `auth-jwt` (single concern, flat package). --- ## ADR-002: Provider interface wraps common ops; Native() is the escape hatch **Status:** Accepted `Provider` exposes six operations using only standard Go types (`string`, `bool`, `int64`, `time.Duration`). Callers that need operations beyond this set call `Native() vk.Client` to access the full valkey-go client. This means: - Code that only uses Provider methods does not need to import `valkey-go`. - Code that calls `Native()` opts in to the valkey-go dependency explicitly. - The `Component` interface extends `Provider` — pass the result of `New()` wherever a `Provider` is expected. Pattern reference: micro-lib `minio` uses the same `Client` + `Native()` architecture. --- ## ADR-003: Adapters take Provider, not vk.Client **Status:** Accepted Adapter constructors (`NewPermissionCache`, `NewRateLimiterStore`, `NewBlacklist`) accept `Provider` instead of `vk.Client`. Benefits: 1. **Testability**: adapter behavior is testable with a `mockProvider` in-process — no live Valkey server, no valkey-go mock library required. 2. **Abstraction**: adapters depend only on the six Provider methods, not on the full client API. If the underlying driver changes, adapters are unaffected. 3. **Duck-typed wiring**: in application code, `Component` satisfies `Provider`, so the result of `New()` is passed directly to adapter constructors: ```go vk := cachevalkey.New(logger, cfg) permCache := cachevalkey.NewPermissionCache(vk) // → auth/rbac.Cache rateLimiter := cachevalkey.NewRateLimiterStore(vk, time.Second, 100) // → web/mw.RateLimiterStore blacklist := cachevalkey.NewBlacklist(vk) // → auth-jwt.Blacklist ``` No cast, no glue interface. Go's structural typing handles the assignment. --- ## ADR-004: IncrWithTTL uses Lua for atomic fixed-window rate limiting **Status:** Accepted The `IncrWithTTL` method implements the standard fixed-window counter via a Lua script: ```lua local count = redis.call('INCR', KEYS[1]) if count == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end return count ``` Lua scripts execute atomically in Valkey. The INCR + conditional EXPIRE pattern is race-free: the EXPIRE is set only on the first increment, preventing the window from being extended by racing requests. Alternatives considered: - **MULTI/EXEC**: achieves atomicity but is heavier and not pipeline-friendly. - **True sliding window with sorted sets**: more accurate, but significantly higher memory and complexity for minimal real-world benefit in most rate-limiting scenarios. --- ## ADR-005: Health priority LevelDegraded **Status:** Accepted `Priority()` returns `observability.LevelDegraded`, not `LevelCritical`. A Valkey outage degrades — it does not halt — the service: - Rate limiting (`RateLimiterStore`) fails open (web/mw contract). - Permission cache (`PermissionCache`) misses fall through to the base provider (rbac.CachedPermissionProvider degrades to uncached reads). - Blacklist (`Blacklist`) errors propagate to `authjwt.RefreshTokenPair`, which returns the error to the caller (token refresh fails — safe outcome). No case requires Valkey to be available for the service to process requests. Consistent with micro-lib `valkey` (`health.LevelDegraded`).