El Problema: Un microservicio de usuarios que ahogaba nuestra base de datos
La situación era clásica: teníamos un microservicio de ‘Usuarios’ y otro de ‘Órdenes’. Cada vez que el servicio de Órdenes necesitaba validar un usuario, hacía una llamada síncrona al servicio de Usuarios, que a su vez consultaba la base de datos. Con picos de tráfico, la latencia se disparaba y el thread pool del servicio de Usuarios se saturaba. El p95 de la latencia para obtener un usuario simple superaba los 800ms. Inaceptable.
El objetivo estaba claro: reducir drásticamente la carga sobre la base de datos del servicio de Usuarios y bajar la latencia de respuesta por debajo de los 50ms.
La Arquitectura de la Solución: Apache Camel como orquestador y Redis como caché
En lugar de modificar el código de negocio de los servicios existentes, decidí introducir una capa de orquestación y caché usando Apache Camel dentro de un nuevo microservicio intermediario, construido con Quarkus. La estrategia era implementar el patrón Cache-Aside.
El flujo sería así:
- 1. Una petición para obtener datos de un usuario llega a nuestra nueva ruta Camel.
- 2. Camel primero intenta buscar el ID del usuario en la caché de Redis.
- 3. Cache Hit: Si los datos están en Redis, se devuelven inmediatamente. Fin del proceso. ¡Rápido!
- 4. Cache Miss: Si los datos no están en Redis, Camel procede a llamar al endpoint real del microservicio de Usuarios.
- 5. Una vez obtenida la respuesta del servicio de Usuarios, Camel la almacena en Redis (con un TTL, por ejemplo, de 5 minutos) y luego la devuelve al cliente original.
Este enfoque desacopla la lógica de caché de la lógica de negocio y es transparente para el servicio que consume los datos.
Paso 1: Configurando las dependencias en Quarkus
Primero, lo básico. En el pom.xml de mi proyecto Quarkus, aseguré tener las dependencias necesarias para Camel, Redis y el cliente REST.
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-platform-http</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-redis-client</artifactId>
</dependency>
</dependencies>
Paso 2: La Ruta Camel en Java DSL que hace la magia
Aquí está el corazón de la implementación. Creé una clase RouteBuilder de Camel que define todo el flujo que describí antes. Usar la DSL de Java hace que la lógica de integración sea increíblemente legible.
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.redis.RedisConstants;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class UserCacheRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
// Endpoint que expone nuestra nueva API cacheada
from("platform-http:/users/{userId}")
.routeId("user-cache-route")
// 1. Preparamos la clave para Redis usando el header 'userId'
.setHeader(RedisConstants.KEY, header("userId"))
// 2. Intentamos obtener el valor de la caché
.to("redis:{{redis.host}}:{{redis.port}}?command=GET")
// 3. Usamos un 'Choice' para decidir qué hacer (Cache Hit o Miss)
.choice()
// 3.1. CACHE HIT: Si el body no es nulo, lo encontramos en Redis
.when(body().isNotNull())
.log("Cache HIT for user: ${header.userId}")
// Convertimos el JSON de Redis de nuevo a un objeto o lo dejamos como string
.unmarshal().json(Map.class)
.otherwise()
// 3.2. CACHE MISS: El body es nulo, no estaba en Redis
.log("Cache MISS for user: ${header.userId}")
// 4. Llamamos al servicio de usuarios real
.toD("http://user-service-endpoint/api/users/${header.userId}?bridgeEndpoint=true")
.convertBodyTo(String.class) // Aseguramos que el body sea un String para Redis
// 5. Almacenamos el resultado en Redis para la próxima vez
.setHeader(RedisConstants.VALUE, body())
// Opcional: Establecer un Time-To-Live (TTL) de 300 segundos (5 minutos)
.setHeader(RedisConstants.EXPIRE, constant(300))
.to("redis:{{redis.host}}:{{redis.port}}?command=SETEX")
// Devolvemos el resultado al cliente
.unmarshal().json(Map.class)
.end();
}
}
Paso 3: Configuración en application.properties
Finalmente, solo necesitamos apuntar a nuestra instancia de Redis y listo.
# application.properties
# Redis configuration
redis.host=localhost
redis.port=6379
# Camel configuration
quarkus.camel.service.discovery.enabled=false
Lecciones Aprendidas desde la Trinchera
1. Apache Camel brilla para la lógica de integración: Implementar este patrón directamente en el código de negocio habría sido más verboso y habría mezclado responsabilidades. Camel lo mantuvo limpio, declarativo y fácil de entender.
2. El patrón Cache-Aside es un arma poderosa: Para datos que se leen mucho más de lo que se escriben (como perfiles de usuario), este patrón es devastadoramente efectivo. La latencia promedio cayó de ~800ms a ~30ms en un cache hit.
3. Quarkus + Camel = Rendimiento increíble: El tiempo de arranque y el consumo de memoria fueron mínimos, lo que hace que esta solución sea ligera y perfecta para un entorno de microservicios en Kubernetes.
No subestimes el poder de una buena capa de caché orquestada. A veces, la solución más elegante no es reescribir un servicio, sino construir un puente inteligente frente a él.