La optimización más importante de este capítulo: cachea los tokens en memoria en lugar de consultar la base de datos en cada petición.
Hagamos las cuentas. Parte de una base realista: una API de servicio a servicio atendiendo sesenta peticiones por minuto. Cada petición valida un token. Sin caché:
- 60 peticiones/min = 60 consultas a base de datos por minuto
- Digamos 3.600 consultas por hora en operación normal
- Multiplícalo por varios entornos de preproducción y te plantas fácilmente en cinco o diez mil por hora
No es catastrófico, pero tampoco es gratis. Cada consulta golpea la base de datos, usa una conexión y añade latencia.
Pero aquí es donde importa: esta es una API de servicio a servicio. A diferencia de las APIs de cara al usuario, que atienden millones de peticiones ocasionales, las de servicio a servicio tienen menos llamantes distintos haciendo peticiones repetidas. Un sistema de despliegue puede consultar tu API miles de veces por hora. Un servicio de facturación ejecuta operaciones por lotes constantemente.
La escala llega rápido. Esas 60 peticiones/min pueden saltar a 600, y luego a más de 1.000, según se integren más socios. Al llegar a 1.000 por minuto, las cuentas cambian drásticamente:
Sin caché a 1.000 peticiones/min:
- 1.000 consultas/min = 60.000 consultas por hora
- Tu pool de conexiones sufre
- Cada consulta añade entre 5 y 10 ms de latencia
- Cada respuesta es un poco más lenta
- En picos de carga, la base de datos se vuelve el cuello de botella
Con caché a 1.000 peticiones/min:
- 1.000 peticiones/min contra la caché = aciertos instantáneos (menos de 1 ms)
- Sólo los tokens nuevos provocan consultas
- En régimen estable con llamantes repetidos: entre 50 y 100 consultas por hora
- 99 % menos de carga en la base de datos
- Las respuestas siguen rápidas incluso con picos de diez veces el tráfico
Ahí es donde el cacheo se vuelve crítico. Pasas de un problema de escalabilidad a un no-problema.
Sin embargo, antes en esta guía insistíamos en evitar la optimización prematura y en no resolver problemas que aún no tienes. El cacheo de tokens es la excepción, y por esto: añadirlo no cuesta nada —unas líneas en un trait, reutilizadas en dos sitios. No añade complejidad. Pero desbloquea diez veces más escalabilidad gratis. Y lo más importante: escalar de 60 a 1.000 peticiones por minuto no requiere cambios de código. Despliegas la misma aplicación. Añades servidores, subes el TTL de caché, ajustas el tamaño del pool. Escalado de infraestructura, no cambios de código. Ese es el sello de una arquitectura correcta: la construiste bien, así que crecer es un problema de despliegue y no de refactorización.
Así funciona:
trait CachesBearerTokens
{
private function rememberBearerToken(string $token): ?Model
{
$cache = $this->resolveBearerCacheRepository();
$cacheKey = $this->buildBearerCacheKey($token);
$cached = $cache->get($cacheKey);
if ($cached instanceof Model && $this->isCachedTokenStillValid($cached, $token)) {
return $cached;
}
// The model class is resolved from config, so a service can supply its
// own without the trait knowing anything about it.
$bearerModel = config('webplo.bearer_model', Bearer::class);
/** @var class-string<Model> $bearerModel */
$bearer = $bearerModel::where('token', $token)->first();
if ($bearer === null) {
return null;
}
$cache->put($cacheKey, $bearer, $this->calculateBearerCacheTTL($bearer));
return $bearer;
}
private function resolveBearerCacheRepository(): Repository
{
$store = config('webplo.bearer_cache_store');
return is_string($store) && $store !== ''
? Cache::store($store)
: Cache::store();
}
}
private function calculateBearerCacheTTL(Model $bearer): int
{
/** @var \DateTimeInterface|string|null $expiresAt */
$expiresAt = $bearer->getAttribute('expires_at');
if (! $expiresAt) {
return 3600; // No expiry: an hour is long enough to help, short enough to forget.
}
$secondsUntilExpiry = now()->diffInSeconds($expiresAt, absolute: false);
$ttlConfig = config('webplo.bearer_cache_ttl', 'token_expiration');
return match (true) {
is_int($ttlConfig) => $ttlConfig,
// Expire the cache entry five minutes BEFORE the token itself.
$ttlConfig === 'token_expiration' => (int) max(1, $secondsUntilExpiry - 300),
default => (int) max(1, min($secondsUntilExpiry, 3600)),
};
}
El margen es la parte que merece robarse. Cachea un token exactamente el tiempo que es válido y habrás construido una carrera: la entrada expira en el mismo instante que el token, y una petición que llegue en esa ventana puede recibir un token válido en caché y muerto en la base de datos. Restar cinco minutos significa que la caché siempre se rinde primero, y que la base de datos siempre es la que decide.
El mismo razonamiento explica la revalidación al leer. Un modelo cacheado sólo se acepta si su token sigue coincidiendo; si no, un token revocado hace un minuto seguiría funcionando hasta que se agote su TTL, que es precisamente el momento en que menos lo quieres.
Y lo bonito es esto: cualquiera que necesite rememberBearerToken() puede usar el trait. Cualquier middleware. El LicenseServiceProvider lo usa. Ambos dan contra la misma clave de caché. Así que:
Request arrives
↓
Middleware calls rememberBearerToken()
→ First request to the token: database query, cache for TTL, return
→ Second request in same hour: cache hit, no database query
↓
LicenseServiceProvider calls rememberBearerToken()
→ Same cache hit from middleware, no additional query
↓
Controller executes with token already validated and configured
Una consulta a base de datos, dos validaciones, escalabilidad infinita.