⚡ Mejora el rendimiento en Spring evitando consultas duplicadas con caché por solicitud

Carlos ExpositoCarlos Exposito
2 min read

¿Te ha pasado que haces validaciones de seguridad con @PreAuthorize, pero terminas llamando a la base de datos más de una vez por la misma entidad? 😱
Hoy te muestro cómo evitar eso elegantemente con un caché ligado al ciclo de vida de la petición en Spring Boot. Y lo mejor: ¡con ejemplos distintos!


🔁 El problema común

Mira este escenario con productos:

@PreAuthorize("@productoSecurity.tieneAcceso(#productoId, authentication.name)")
public Producto obtenerProducto(Long productoId) {
    return productoRepository.findById(productoId).orElseThrow();
}

El método tieneAcceso también usa findById, ¡duplicando la consulta! 😩


💡 La solución: ThreadLocal como caché por petición

Creamos una clase genérica para guardar cualquier entidad durante la petición:

public class PeticionCache<T> {
    private final ThreadLocal<Optional<T>> cache = new ThreadLocal<>();

    public void guardar(T valor) {
        cache.set(Optional.ofNullable(valor));
    }

    public Optional<T> obtener() {
        return cache.get();
    }

    public void limpiar() {
        cache.remove();
    }
}

📦 Registro de cachés

Agrupamos las cachés por tipo de entidad en un solo bean inyectable:

@Component
@Getter
public class RegistroCache {
    private final PeticionCache<Producto> producto = new PeticionCache<>();

    public void limpiarTodo() {
        producto.limpiar();
    }
}

🔐 Seguridad que guarda en caché

En lugar de consultar dos veces, cacheamos al validar permisos:

@Component
@AllArgsConstructor
public class ProductoSecurity {
    private final ProductoRepository repo;
    private final RegistroCache registro;

    public boolean tieneAcceso(Long id, String email) {
        return repo.findById(id)
            .filter(p -> p.getDueño().getEmail().equals(email))
            .map(p -> {
                registro.getProducto().guardar(p);
                return true;
            }).orElse(false);
    }
}

🚀 Servicio que reutiliza la caché

Si la validación pasó, ¡la entidad ya está lista!

@PreAuthorize("@productoSecurity.tieneAcceso(#productoId, authentication.name)")
public Producto obtenerProducto(Long productoId) {
    return registro.getProducto().obtener()
            .orElseThrow(() -> new EntityNotFoundException("Producto no encontrado"));
}

🧼 Filtro para limpiar después de cada request

Evita fugas de memoria limpiando el caché al final de cada petición:

@Component
@RequiredArgsConstructor
public class FiltroLimpiezaCache extends OncePerRequestFilter {

    private final RegistroCache registro;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
        throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } finally {
            registro.limpiarTodo();
        }
    }
}

✅ Beneficios

  • ❌ Evitas consultas duplicadas
  • 🧼 Código más limpio
  • 🔄 Reutilizable para distintas entidades
  • ✅ Compatible con seguridad Spring

Si usas @PreAuthorize, este patrón puede ahorrarte muchas líneas innecesarias y hacer tu app más rápida 🚀

0
Subscribe to my newsletter

Read articles from Carlos Exposito directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Carlos Exposito
Carlos Exposito