Problem
Every authenticated request executes getByUserIdAndClient() (DB read) even when the same token was validated milliseconds ago. The existing tokenEqualityCache caches BCrypt comparison results but not the token entity itself.
For high-traffic apps with polling (notifications, feed), this adds 1 unnecessary DB round-trip per request that could be avoided with a short-lived cache.
Per-request DB path (from source)
OgiriTokenAuthenticationFilter.authenticateRequest()
→ tokenService.validToken(token, user, client)
→ getByUserIdAndClient(userId, client) // DB: SELECT * FROM user_tokens WHERE user_id = ? AND client = ?
→ tokensMatch(tokenHash, token) // cached BCrypt ✓ (tokenEqualityCache)
→ tokenService.isBatchRequest(...)
→ getByUserIdAndClient(userId, client) // DB hit on batchTimestampCache miss
The BCrypt result is cached, but the entity fetch is not. For the same user/client within seconds, the DB read is redundant.
Proposal
Add an optional SPI interface following the existing pattern (OgiriAuditHook, OgiriRateLimitHook):
package com.quantipixels.ogiri.security.spi
/**
* Optional SPI for caching token lookups.
* When provided, OgiriTokenService checks this cache before hitting the repository.
* Implementations handle their own invalidation strategy.
*/
interface OgiriTokenLookupCache<T : OgiriToken> {
fun get(userId: Long, client: String): T?
fun put(userId: Long, client: String, token: T)
fun evict(userId: Long, client: String)
fun evictAll(userId: Long)
}
Wiring
Inject via ObjectProvider<OgiriTokenLookupCache<T>> in OgiriTokenService constructor (same pattern as auditHookProvider). When no bean is present, fall through to repository directly — zero behavior change for existing consumers.
Invalidation points
These already exist in OgiriTokenService and would call evict():
createOrUpdateToken() → evict(userId, client)
deleteToken(userId, client) → evict(userId, client)
deleteToken(userId, clients) → evict each
deleteAllForUser(userId) → evictAll(userId)
cleanupExpiredTokens() / cleanupExpiredTokensBatched() → bulk eviction or clear
Consumer example (Caffeine)
@Component
class CaffeineTokenLookupCache : OgiriTokenLookupCache<UserToken> {
private val cache: Cache<String, UserToken> = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(5))
.build()
override fun get(userId: Long, client: String) = cache.getIfPresent("$userId:$client")
override fun put(userId: Long, client: String, token: UserToken) = cache.put("$userId:$client", token)
override fun evict(userId: Long, client: String) = cache.invalidate("$userId:$client")
override fun evictAll(userId: Long) { /* invalidate matching prefix or use separate index */ }
}
Consumers wanting distributed caching (Redis) for multi-instance deployments would implement the same interface with a Redis-backed store.
Benefits
- Zero breaking changes — optional via
ObjectProvider, existing apps unchanged
- Follows existing SPI conventions — same pattern as
OgiriAuditHook, OgiriRateLimitHook
- Consumer controls cache strategy — Caffeine, Redis, or custom
- Small surface area — 4 methods, 1 interface, ~20 lines of wiring in
OgiriTokenService
- Reduces DB load proportional to request rate — high-polling apps benefit most
Problem
Every authenticated request executes
getByUserIdAndClient()(DB read) even when the same token was validated milliseconds ago. The existingtokenEqualityCachecaches BCrypt comparison results but not the token entity itself.For high-traffic apps with polling (notifications, feed), this adds 1 unnecessary DB round-trip per request that could be avoided with a short-lived cache.
Per-request DB path (from source)
The BCrypt result is cached, but the entity fetch is not. For the same user/client within seconds, the DB read is redundant.
Proposal
Add an optional SPI interface following the existing pattern (
OgiriAuditHook,OgiriRateLimitHook):Wiring
Inject via
ObjectProvider<OgiriTokenLookupCache<T>>inOgiriTokenServiceconstructor (same pattern asauditHookProvider). When no bean is present, fall through to repository directly — zero behavior change for existing consumers.Invalidation points
These already exist in
OgiriTokenServiceand would callevict():createOrUpdateToken()→evict(userId, client)deleteToken(userId, client)→evict(userId, client)deleteToken(userId, clients)→evicteachdeleteAllForUser(userId)→evictAll(userId)cleanupExpiredTokens()/cleanupExpiredTokensBatched()→ bulk eviction or clearConsumer example (Caffeine)
Consumers wanting distributed caching (Redis) for multi-instance deployments would implement the same interface with a Redis-backed store.
Benefits
ObjectProvider, existing apps unchangedOgiriAuditHook,OgiriRateLimitHookOgiriTokenService