El Dolor: Una API que respondía por carta

Me enfrenté a un problema clásico: una API REST crítica para el negocio tardaba una eternidad en responder. Hablamos de picos de 2 segundos. El endpoint recibía una carga de datos, la validaba contra la base de datos, llamaba a dos servicios externos y finalmente guardaba el resultado. Todo en una sola transacción síncrona. Un desastre esperando a ocurrir en cuanto el tráfico aumentara.

El cliente sentía la lentitud y el sistema era frágil. Si uno de los servicios externos fallaba, toda la operación se caía. No era escalable, no era resiliente. Sabía que tenía que desacoplar el proceso de ingesta de la lógica de procesamiento pesado.

La Arquitectura del Cambio: Del Monolito Síncrono al Pipeline Reactivo

La estrategia fue simple en concepto: transformar el proceso síncrono en un pipeline de datos asíncrono. La API solo sería responsable de una cosa: recibir los datos, hacer una validación mínima y ponerlos en una cola de mensajes. Inmediatamente después, devolvería un 202 Accepted. Rápido y eficiente.

Detrás de escena, un microservicio independiente, construido con Quarkus y orquestado por Apache Camel, consumiría los mensajes de esa cola y ejecutaría todo el procesamiento pesado sin bloquear al cliente.

Paso 1: El Esqueleto del Microservicio con Quarkus

Quarkus fue mi elección por su arranque casi instantáneo y su bajo consumo de memoria, ideal para microservicios. Empezamos con las dependencias clave en el pom.xml:

<!-- pom.xml -->
<dependencies>
    <!-- El corazón de Quarkus -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-resteasy-reactive-jackson</artifactId>
    </dependency>
    
    <!-- La magia de Camel en Quarkus -->
    <dependency>
        <groupId>org.apache.camel.quarkus</groupId>
        <artifactId>camel-quarkus-core</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.camel.quarkus</groupId>
        <artifactId>camel-quarkus-direct</artifactId>
    </dependency>
    
    <!-- Conector para nuestra cola de mensajes, en este caso Kafka -->
    <dependency>
        <groupId>org.apache.camel.quarkus</groupId>
        <artifactId>camel-quarkus-kafka</artifactId>
    </dependency>
</dependencies>

Paso 2: El Endpoint de Ingesta (La Puerta Rápida)

El nuevo endpoint es minimalista. Su única misión es recibir la petición y enviarla a una ruta de Camel. Usé el componente Direct de Camel para conectar el endpoint REST con el pipeline de backend de forma síncrona dentro del mismo JVM, pero el trabajo pesado se delega a la cola.

// DataIngestionResource.java
import org.apache.camel.ProducerTemplate;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;

@Path("/ingest")
public class DataIngestionResource {

    @Inject
    ProducerTemplate producerTemplate;

    @POST
    public Response ingestData(MyData payload) {
        // Validación básica y rápida
        if (payload == null || !payload.isValid()) {
            return Response.status(Response.Status.BAD_REQUEST).build();
        }
        
        // Dispara la ruta de Camel y no espera el resultado final
        producerTemplate.sendBody("direct:startPipeline", payload);
        
        // Devolvemos 202 Accepted inmediatamente
        return Response.accepted().entity("{\"status\": \"processing\"}").build();
    }
}

Paso 3: Orquestando el Pipeline con Apache Camel

Aquí es donde Camel brilla. Definí las rutas de integración usando Java DSL. Una ruta recibe los datos del endpoint y los pone en un topic de Kafka. Otra ruta, completamente independiente, consume de ese topic y ejecuta la lógica de negocio.

// PipelineRoutes.java
import org.apache.camel.builder.RouteBuilder;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class PipelineRoutes extends RouteBuilder {

    @Override
    public void configure() throws Exception {
        // Ruta 1: Desde el endpoint REST hacia Kafka
        from("direct:startPipeline")
            .routeId("ingest-to-kafka-route")
            .log("Recibido nuevo payload. Enviando a Kafka...")
            .marshal().json()
            .to("kafka:raw-data-topic?brokers={{kafka.bootstrap.servers}}");

        // Ruta 2: El worker asíncrono que consume de Kafka
        from("kafka:raw-data-topic?brokers={{kafka.bootstrap.servers}}")
            .routeId("kafka-processor-route")
            .log("Nuevo mensaje consumido de Kafka. Procesando...")
            .unmarshal().json(MyData.class)
            .bean("dataProcessorBean", "processStepOne") // Llama a un bean de CDI
            .bean("externalServiceCaller", "callApiA") // Llama al servicio externo
            .log("Procesamiento completado. Guardando resultado.")
            .to("jpa:ProcessedResult"); // O cualquier otro destino final
    }
}

La belleza de esto es que cada paso (.bean(...), .to(...)) es una unidad de trabajo que Camel maneja. Puedo agregar fácilmente manejo de errores, reintentos y políticas de dead-letter-queue directamente en la ruta de forma declarativa.

Lecciones Aprendidas y Resultados Reales

La implementación fue un éxito rotundo. Los resultados hablan por sí solos:

  • Latencia de la API de ingesta: Pasó de un promedio de 2000ms a ~150ms.
  • Resiliencia: Si un servicio externo cae, no afecta la ingesta. Camel reintentará el procesamiento del mensaje fallido de forma aislada.
  • Escalabilidad: Ahora puedo escalar el número de consumidores del topic de Kafka de forma independiente a la API de ingesta para manejar picos de carga.

Mi conclusión: No proceses en síncrono nada que no necesite una respuesta inmediata. Adoptar un modelo asíncrono con herramientas como Quarkus y Apache Camel no solo mejora drásticamente el rendimiento, sino que construye sistemas más robustos y escalables. El esfuerzo inicial de re-arquitectura se paga con creces en la operación diaria.