Patrones Apache Kafka y Buenas Prácticas

  • Tiempo de lectura:7 minutos de lectura
  • Autor de la entrada:
  • Última modificación de la entrada:21/07/2025

¿Te has preguntado por qué algunos sistemas que usan patrones de Apache Kafka escalan bien mientras otros colapsan? La diferencia no está en la tecnología, sino en cómo la usas. Si estás diseñando arquitecturas basadas en eventos, presta atención a este artículo.

Patrones Apache Kafka

¿Qué son los patrones en Apache Kafka y por qué importan?

Cuando comencé a trabajar con Apache Kafka, lo primero que noté fue la falta de una «guía oficial» sobre cómo diseñar arquitecturas efectivas. Todos los recursos hablaban sobre productores, consumidores y topics, pero pocos en cómo conectar todo eso en sistemas reales. Ahí es donde los patrones entran en juego.

Los patrones de Kafka son formas probadas y repetidas de resolver problemas comunes en arquitecturas distribuidas orientadas a eventos. Implementarlos correctamente puede ser la diferencia entre un sistema estable, escalable y fácil de mantener, o uno lleno de cuellos de botella, redundancias innecesarias y comportamientos impredecibles.

1. Request–Reply en Kafka: Comunicación síncrona sobre un bus asíncrono

Uno de los primeros retos a los que te puedes enfrentar es a que dos microservicios se hablen en tiempo real, pero sin acoplarse directamente. La solución es implementar el patrón Request–Reply sobre Kafka, lo que al principio parece contradictorio porque Kafka es inherentemente asincrónico.

El truco está en:

  • Generar un correlationId único en cada mensaje.
  • Publicar el mensaje en un topic de peticiones.
  • Escuchar en un topic de respuestas que contenga ese correlationId.
  • Agregar una expiración temporal en caso de no recibir respuesta.

Este patrón es ideal cuando necesitas confirmación de procesamiento, pero sin perder los beneficios del desacoplamiento. Eso sí, requiere diseñar una infraestructura robusta que soporte reintentos y errores parciales.

Aun asi, recuerda nunca usar un único topic para todas las respuestas para evitar saturar el sistema.

Productor (Request)

ProducerRecord<String, String> request = new ProducerRecord<>(
    "request-topic", null, UUID.randomUUID().toString(), 
    "¿Cuál es el estado del pedido #123?", 
    HeadersBuilder.with("correlationId", "abc-123").with("replyTo", "reply-topic")
);
producer.send(request);

Consumidor + Replier

consumer.subscribe(Collections.singletonList("request-topic"));
while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
    for (ConsumerRecord<String, String> record : records) {
        String correlationId = record.headers().lastHeader("correlationId").value().toString();
        String replyTo = record.headers().lastHeader("replyTo").value().toString();

        String respuesta = "Pedido #123 está en reparto";

        ProducerRecord<String, String> reply = new ProducerRecord<>(
            replyTo, null, correlationId, respuesta,
            HeadersBuilder.with("correlationId", correlationId)
        );
        producer.send(reply);
    }
}

2. Event Chunk: dividir y recomponer batch eficientemente

Cuando trabajamos con eventos que contienen grandes volúmenes de datos (por ejemplo, cargas batch desde un ERP), el tamaño del mensaje puede superar los límites máximos permitidos por Kafka. Aquí entra el patrón Event Chunk.

Este patrón consiste en:

  1. Dividir el payload original en múltiples fragmentos más pequeños.
  2. Asignar un chunkId y messageId para agruparlos.
  3. Reensamblar los chunks en el consumidor una vez recibidos todos.

Lo he aplicado en integraciones financieras donde se enviaban hasta 5,000 registros contables por evento. Fragmentar estos mensajes permite que los consumidores manejen los datos en paralelo. Este patrón también te obliga a mejorar la lógica de retry, ya que un solo chunk faltante puede retrasar todo el procesamiento.

Dividir un payload en fragmentos

String payload = largeJson;
int chunkSize = 900000; // menos de 1MB

List<String> chunks = new ArrayList<>();
for (int i = 0; i < payload.length(); i += chunkSize) {
    chunks.add(payload.substring(i, Math.min(payload.length(), i + chunkSize)));
}

String eventId = UUID.randomUUID().toString();
for (int i = 0; i < chunks.size(); i++) {
    ProducerRecord<String, String> chunkRecord = new ProducerRecord<>(
        "chunked-events",
        null,
        eventId,
        chunks.get(i),
        HeadersBuilder
            .with("chunkId", i + "")
            .with("totalChunks", chunks.size() + "")
            .with("eventId", eventId)
    );
    producer.send(chunkRecord);
}

4. Claim Check + Event Chunk: Escalabilidad y control

Una evolución del patrón anterior es combinarlo con el famoso Claim Check, donde solo se envía una referencia al contenido en Kafka, mientras que el contenido real se guarda en un almacenamiento externo (como Amazon S3 o un sistema de blobs).

Por ejemplo, puede ser útil en una solución e-commerce. Donde se almacenan los detalles de cada orden (incluyendo imágenes y adjuntos) fuera de Kafka, y en los eventos solo se incluye un claimCheckId. Esto reduce drásticamente el tamaño de los eventos y evita congestionar los topics.

Asegúrate aquí de que el sistema de almacenamiento externo tenga redundancia y latencia baja, porque cada consumidor necesitará acceder a él.

5. Arquitecturas dirigidas por eventos con Kafka

Adoptar event-driven architecture (EDA) con Kafka como backbone ha sido uno de los mayores cambios de paradigma que viví como arquitecto. Pasa de pensar en peticiones directas a diseñar flujos de eventos encadenados.

Por ejemplo, podemos tener lo siguiente:

  1. Servicio A emite UsuarioRegistrado.
  2. Servicio B escucha ese evento y envía BienvenidaEnviada.
  3. Servicio C escucha ambos y actualiza el CRM.

Diseñar este flujo requiere tener una coreografía de eventos y control de duplicados.

Recuerda que más eventos no siempre es mejor. Intenta modelar tu coreografía con la menor cantidad de eventos posible y de esta forma reducir el ruido y los errores de correlación.

6. Patrones de procesamiento con Kafka Streams: KStream, KTable y stateful processing

Aquí debes entender bien cuándo usar KStream vs KTable. Evitar errores típicos como intentar hacer joins con KStream donde se necesita un estado.

  • Usa KTable para datos que se acumulan y se actualizan (como catálogos).
  • Usa KStream para flujos inmutables de eventos (como transacciones).
  • Usa stateful processing para operaciones de ventana y agregación.

Kafka Streams con KStream y KTable (Join)

StreamsBuilder builder = new StreamsBuilder();

KStream<String, OrderEvent> orders = builder.stream("orders", Consumed.with(Serdes.String(), orderSerde));
KTable<String, Customer> customers = builder.table("customers", Consumed.with(Serdes.String(), customerSerde));

KStream<String, EnrichedOrder> enriched = orders.join(
    customers,
    (order, customer) -> new EnrichedOrder(order, customer)
);

enriched.to("enriched-orders", Produced.with(Serdes.String(), enrichedOrderSerde));

8. Anti‑patrones comunes en Kafka: qué evitar

Kafka, si se usa mal, puede traer más problemas que soluciones. Aquí algunos anti‑patrones que he vivido en carne propia:

  • Topics genéricos y sin control: evítalos. Un topic llamado “eventos” donde todo tipo de payload entra solo genera caos.
  • Retry sin backoff: hacer reintentos infinitos en consumidores sin control puede saturar tus sistemas.
  • Repartir eventos sin clave (key): esto rompe el orden y complica el procesamiento.
  • Sobreingeniería en esquema de eventos: querer hacerlo “super genérico” lleva a eventos incomprensibles.

9. Buenas prácticas para trabajar con Kafka

Además de los patrones, hay una serie de buenas prácticas que han marcado la diferencia en todos mis proyectos con Kafka:

  • Usa Avro + Schema Registry: para validar, versionar y evitar errores de compatibilidad.
  • Diseña eventos idempotentes: que puedan ser procesados varias veces sin efectos secundarios.
  • Distribuye con buenas claves: para que el particionamiento sea equilibrado.
  • Monitoriza con herramientas como Prometheus y Grafana: entender el lag, throughput y errores en tiempo real es vital.

Configuración de idempotencia y acks en el productor Kafka:

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("acks", "all");
props.put("enable.idempotence", "true");
props.put("retries", Integer.MAX_VALUE);
props.put("max.in.flight.requests.per.connection", 5);
props.put("key.serializer", StringSerializer.class.getName());
props.put("value.serializer", StringSerializer.class.getName());

KafkaProducer<String, String> producer = new KafkaProducer<>(props);

No dejes el manejo de errores como «algo que luego resolveremos». Un simple try/catch con logs no basta. Usa DLQs, alertas y dashboards desde el día 1.

Ddiseña con eventos, no con servicios. Kafka no es una base de datos, ni un broker de peticiones. Es el esqueleto de un sistema vivo que se comunica mediante hechos.

Siguientes Pasos y Formación

Si quieres aprender Apache Kafka a fondo y convertirte en experto, no dudes en invertir en tu formación a largo plazo. Para ello, te dejo mi propio curso en español en el que aprenderás desde cero: con partes teóricas y partes prácticas. Es un curso fundamental para quien desee implementar sistemas escalables de procesamiento de datos en tiempo real.

Comienza con Kafka: Curso de Apache Kafka desde Cero

Deja una respuesta