# Día 22: Mensajería Asíncrona con Apache Kafka y Spring Boot 3

> Topics, Partitions, Consumer Groups, Producers, KafkaTemplate y Serialización Avro/JSON

- Plan: [JavaMastery](https://aprende-java.webs-economicas.es/), día 22 de 30
- Fase 6: Mensajería Asíncrona con Kafka, RabbitMQ, EDA y CQRS
- Dedicación: 4.5h
- Profundidad: visión de conjunto
- Revisado: 2026-10-29
- Versión interactiva: https://aprende-java.webs-economicas.es/dias/kafka-spring-boot/

## Al terminar este día sabrás

- Explicar Kafka como un log distribuido y qué significa un offset.
- Diseñar claves y particiones para conservar el orden donde importa.
- Escalar consumidores con consumer groups y entender los rebalanceos.
- Producir y consumir con Spring Kafka y gestionar errores con reintentos y DLT.


## Conceptos

### 1. Kafka como Log Distribuido Inmutable: El Concepto Fundamental

Kafka NO es una cola de mensajes tradicional. Es un **log distribuido, inmutable y ordenado** (commit log). Cada mensaje publicado se persiste en disco de forma secuencial, asignándole un **offset** numérico que nunca cambia. Los consumidores leen mensajes indicando desde qué offset quieren empezar, y pueden releer eventos pasados las veces que necesiten.

**Cómo funciona por dentro.**

Kafka no es una cola de la que los mensajes desaparecen al leerlos. Es un log: una secuencia ordenada de registros a la que solo se añade al final. Cada registro tiene una posición numérica, el offset. Los consumidores no borran nada; cada uno recuerda hasta qué offset ha leído y avanza a su ritmo.

Los registros se conservan según la política de retención del topic (por tiempo, por ejemplo 7 días, o por tamaño), independientemente de si alguien los ha leído. Eso permite cosas imposibles en una cola clásica: que varios sistemas lean los mismos eventos de forma independiente, añadir un consumidor nuevo que reprocese el histórico o volver atrás tras corregir un bug moviendo el offset.

Los topics compactados (`cleanup.policy=compact`) conservan solo el último registro de cada clave, convirtiendo el topic en una tabla de estado actual que se puede reconstruir en cualquier momento.

*Ver el log desde la línea de comandos*

```bash
# Crear un topic con 6 particiones y retención de 7 días
kafka-topics.sh --bootstrap-server localhost:9092 --create \
  --topic tareas.eventos --partitions 6 --replication-factor 3 \
  --config retention.ms=604800000

# Leer desde el principio mostrando partición, offset y clave
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic tareas.eventos \
  --from-beginning --property print.partition=true \
  --property print.offset=true --property print.key=true

# Dónde está cada consumer group y cuánto retraso (lag) acumula
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group notificaciones

# Reprocesar: mover el grupo al inicio (con los consumidores parados)
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group notificaciones \
  --topic tareas.eventos --reset-offsets --to-earliest --execute
```

- **En una línea:** `kafkaTemplate.send("ordenes-creadas", ordenId.toString(), new OrdenCreadaEvent(ordenId, items));`
- **Por qué importa:** A diferencia de RabbitMQ (donde los mensajes se eliminan tras ser consumidos), en Kafka los mensajes se retienen durante el período de retención configurado (horas, días o indefinidamente), permitiendo reconstruir el estado de un sistema desde cero.
- **Error típico:** Tratar Kafka como una cola punto a punto como ActiveMQ, esperando que los mensajes desaparezcan una vez leídos.
- **Java 17 frente a 21:** Independiente de la versión de Java.
- **Se conecta con:** Event Sourcing, Replicación de Datos, Audit Trail

### 2. Topics y Partitions: Paralelismo y Orden Garantizado

Un **Topic** es la categoría lógica de un flujo de eventos (e.g. `ordenes-creadas`). Cada Topic se divide en **Partitions** (fragmentos independientes). Los mensajes dentro de una partición mantienen orden estricto (FIFO), pero entre particiones distintas no hay garantía de orden. La clave del mensaje (`key`) determina a qué partición se envía (misma key = siempre la misma partición = orden garantizado para esa entidad).

**Cómo funciona por dentro.**

Un topic se divide en particiones, y cada partición es un log independiente que puede vivir en un broker distinto. Las particiones son la unidad de paralelismo (más particiones permiten más consumidores en paralelo) y de orden: Kafka solo garantiza el orden dentro de una partición, nunca entre particiones.

El productor decide la partición a partir de la clave del mensaje: aplica un hash (murmur2) a la clave y calcula el resto entre el número de particiones. Mismos datos de clave, misma partición, mismo orden. Por eso la clave debe ser la entidad cuyo orden importa: el id del pedido, de la cuenta o de la tarea. Así «creada», «asignada» y «completada» de la tarea 42 llegan en orden.

Cuidado al aumentar particiones de un topic existente: el resto del hash cambia y una misma clave puede pasar a otra partición, rompiendo el orden durante la transición. Conviene dimensionar con margen desde el principio. Cada partición se replica en varios brokers (factor de replicación, normalmente 3) para tolerar caídas.

*Clave de partición = entidad cuyo orden importa*

```java
public record TareaEvento(long tareaId, String tipo, Instant ocurridoEn) {}

@Service
class PublicadorEventos {
    private final KafkaTemplate<String, TareaEvento> kafka;
    PublicadorEventos(KafkaTemplate<String, TareaEvento> kafka) { this.kafka = kafka; }

    void publicar(TareaEvento evento) {
        String clave = String.valueOf(evento.tareaId());     // todos los eventos de la tarea, misma partición
        kafka.send("tareas.eventos", clave, evento)
             .whenComplete((resultado, error) -> {
                 if (error != null) {
                     log.error("No se pudo publicar {}", evento, error);
                 } else {
                     var md = resultado.getRecordMetadata();
                     log.debug("Publicado en partición {} offset {}", md.partition(), md.offset());
                 }
             });
    }
}
```

- **En una línea:** `kafkaTemplate.send("ordenes", orderId.toString(), evento); // key = orderId garantiza orden por pedido`
- **Por qué importa:** Si envías todos los eventos de un pedido concreto con la misma key (el `orderId`), Kafka garantiza que se procesarán en el orden exacto en que fueron producidos.
- **Error típico:** Enviar mensajes sin clave (key=null), provocando distribución round-robin entre particiones y perdiendo el orden lógico entre eventos del mismo dominio.
- **Java 17 frente a 21:** Independiente de Java.
- **Se conecta con:** Sharding, Hash de Clave, Escalabilidad Horizontal

### 3. Consumer Groups: Escalabilidad Horizontal de Consumidores

Un **Consumer Group** es un conjunto de consumidores que se reparten las particiones de un Topic. Si un Topic tiene 6 particiones y tu grupo tiene 3 consumidores, cada consumidor lee 2 particiones. Si añades un cuarto consumidor, Kafka rebalancea automáticamente (2+2+1+1). Un sexto consumidor leerá 1 partición cada uno. Un séptimo consumidor quedará inactivo (idle) porque no hay más particiones que asignar.

**Cómo funciona por dentro.**

Un consumer group es un conjunto de consumidores que se reparten el trabajo de un topic. Kafka asigna cada partición a exactamente un consumidor del grupo, así que los mensajes de una partición se procesan en orden y sin duplicar trabajo dentro del grupo. Si el topic tiene 6 particiones y el grupo 3 instancias, cada una recibe 2.

Cuando entra o sale un consumidor (despliegue, caída, escalado), el grupo hace un rebalanceo y redistribuye las particiones. Durante un rebalanceo el consumo se detiene brevemente, y los mensajes procesados pero no confirmados se volverán a entregar, de ahí la importancia de la idempotencia.

Distintos grupos son independientes: cada grupo recibe todos los mensajes del topic y lleva sus propios offsets. Así un mismo evento lo procesan el servicio de notificaciones y el de analítica, cada uno a su ritmo. Grupos distintos dan publicación-suscripción; consumidores del mismo grupo, reparto de carga.

*Dos grupos sobre el mismo topic*

```java
@Component
class NotificacionesListener {
    @KafkaListener(topics = "tareas.eventos", groupId = "notificaciones", concurrency = "3")
    void alRecibir(TareaEvento evento) {
        // 3 hilos consumidores en esta instancia; con 6 particiones, 2 por hilo
        notificador.avisar(evento);
    }
}

@Component
class AnaliticaListener {
    @KafkaListener(topics = "tareas.eventos", groupId = "analitica")
    void alRecibir(ConsumerRecord<String, TareaEvento> registro) {
        // otro grupo: recibe TODOS los eventos otra vez, con sus propios offsets
        metricas.registrar(registro.value(), registro.partition(), registro.offset());
    }
}
```

- **En una línea:** `@KafkaListener(topics = "ordenes-creadas", groupId = "servicio-facturacion")`
- **Por qué importa:** Permite escalar horizontalmente los consumidores añadiendo réplicas (pods en Kubernetes) hasta igualar el número de particiones del Topic.
- **Error típico:** Crear un Topic con 1 sola partición y esperar que 10 consumidores del mismo grupo procesen en paralelo; solo 1 estará activo y los otros 9 estarán inactivos.
- **Java 17 frente a 21:** Independiente de Java.
- **Se conecta con:** Kubernetes Pods, Rebalanceo, Throughput

### 4. Configuración de KafkaTemplate y @KafkaListener en Spring Boot 3

Spring for Apache Kafka proporciona `KafkaTemplate<K,V>` como productor de alto nivel y `@KafkaListener` como consumidor declarativo. La configuración se centraliza en `application.yml` con propiedades como `spring.kafka.bootstrap-servers`, `spring.kafka.consumer.group-id`, `spring.kafka.consumer.auto-offset-reset` (earliest/latest).

**Cómo funciona por dentro.**

Spring Boot autoconfigura `KafkaTemplate` para producir y la infraestructura de `@KafkaListener` para consumir a partir de las propiedades `spring.kafka.*`. Lo esencial es elegir serializadores: para empezar, JSON con `JsonSerializer` y `JsonDeserializer` (indicando los paquetes de confianza); en sistemas grandes, Avro o Protobuf con un registro de esquemas, que valida la compatibilidad entre versiones.

Para no perder mensajes en el productor: `acks=all` (el broker confirma cuando todas las réplicas sincronizadas tienen el mensaje) y `enable.idempotence=true` (evita duplicados por reintentos de red; es el valor por defecto desde Kafka 3.0).

En el consumidor, `auto-offset-reset` decide dónde empieza un grupo nuevo: `earliest` lee desde el principio, `latest` solo los mensajes que lleguen a partir de ahora. Spring confirma los offsets después de que tu método termine sin excepción.

*Configuración de productor y consumidor*

```yaml
spring:
  kafka:
    bootstrap-servers: kafka-1:9092,kafka-2:9092,kafka-3:9092
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
      acks: all
      properties:
        enable.idempotence: true
        linger.ms: 10               # agrupa mensajes en lotes: más throughput, algo más de latencia
    consumer:
      group-id: notificaciones
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
      properties:
        spring.deserializer.value.delegate.class: org.springframework.kafka.support.serializer.JsonDeserializer
        spring.json.trusted.packages: com.acme.taskflow.eventos
    listener:
      ack-mode: record              # confirma el offset tras procesar cada registro
```

- **En una línea:** `spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=servicio-inventario`
- **Por qué importa:** Elimina cientos de líneas de configuración manual del cliente nativo de Kafka.
- **Error típico:** No configurar `auto-offset-reset: earliest` y perder todos los eventos publicados antes de que el consumidor arrancase por primera vez.
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** Spring Boot Auto-Configuration, Serialización JSON, Dead Letter Topic (DLT)

### 5. Gestión de Errores y Dead Letter Topics (DLT)

Cuando un consumidor falla al procesar un mensaje (excepción, servicio caído), Spring Kafka permite configurar reintentos automáticos (`RetryPolicy`) y, tras agotar los reintentos, enviar el mensaje a un **Dead Letter Topic** (DLT) donde queda almacenado para inspección y reprocesamiento manual o automatizado.

**Cómo funciona por dentro.**

Si un listener lanza una excepción, Spring Kafka usa su `DefaultErrorHandler`: reintenta el mismo registro según un backoff (por defecto, 9 reintentos sin espera) y, si sigue fallando, ejecuta un recuperador. Mientras reintenta, la partición está bloqueada: los mensajes siguientes esperan, lo que preserva el orden.

El recuperador habitual es `DeadLetterPublishingRecoverer`, que envía el registro fallido a un topic de mensajes muertos (por convención, `nombre-topic.DLT`) con cabeceras que explican el error, y permite seguir con el siguiente. Así un mensaje envenenado no paraliza el consumo y queda guardado para analizarlo o reprocesarlo.

Distingue errores transitorios (base de datos caída, timeout: tiene sentido reintentar) de permanentes (JSON inválido, validación: reintentar no sirve). Marca los permanentes como no reintentables. Si no necesitas orden estricto, `@RetryableTopic` hace reintentos no bloqueantes mediante topics de reintento con esperas crecientes.

*Reintentos con backoff y Dead Letter Topic*

```java
@Configuration
class KafkaErroresConfig {

    @Bean
    DefaultErrorHandler errorHandler(KafkaTemplate<Object, Object> template) {
        var recuperador = new DeadLetterPublishingRecoverer(template,
            (registro, ex) -> new TopicPartition(registro.topic() + ".DLT", registro.partition()));

        var backoff = new ExponentialBackOffWithMaxRetries(4);   // 4 reintentos
        backoff.setInitialInterval(500);                         // 0,5 s, 1 s, 2 s, 4 s
        backoff.setMultiplier(2.0);

        var handler = new DefaultErrorHandler(recuperador, backoff);
        handler.addNotRetryableExceptions(                       // directos a la DLT
            DeserializationException.class,
            ValidationException.class);
        return handler;
    }
}

// Alternativa sin bloquear la partición (se pierde el orden estricto):
@RetryableTopic(attempts = "4", backoff = @Backoff(delay = 1000, multiplier = 2))
@KafkaListener(topics = "tareas.eventos", groupId = "notificaciones")
void alRecibir(TareaEvento evento) { notificador.avisar(evento); }

@DltHandler
void muerto(TareaEvento evento, @Header(KafkaHeaders.EXCEPTION_MESSAGE) String error) {
    log.warn("Evento {} enviado a DLT: {}", evento, error);
}
```

- **En una línea:** `factory.setCommonErrorHandler(new DefaultErrorHandler(new DeadLetterPublishingRecoverer(kafkaTemplate), new FixedBackOff(1000L, 3L)));`
- **Por qué importa:** Previene que un solo mensaje envenenado bloquee indefinidamente la partición, deteniendo el consumo de los miles de mensajes posteriores.
- **Error típico:** Ignorar errores con un `try-catch` vacío, perdiendo mensajes silenciosamente sin registro ni alerta.
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** Resiliencia, Monitorización, Idempotencia


## Sesión de 90 minutos

### Bloque 1: Fundamentos del Log Distribuido (0 - 25 min)

- **Qué estudiar:** Conceptos de Topic, Partition, Offset, Retention. Diferencia con colas de mensajes.
- **Pregunta a responder:** ¿Por qué Kafka persiste los mensajes en disco pero sigue siendo extremadamente rápido?
- **Entregable:** Diagrama de un Topic con 3 particiones y 2 Consumer Groups leyendo independientemente.

### Bloque 2: Productor y Consumidor con Spring Kafka (25 - 50 min)

- **Qué estudiar:** Configuración de `KafkaTemplate`, `@KafkaListener`, serialización JSON con Jackson.
- **Pregunta a responder:** ¿Cómo garantizar que todos los eventos de un mismo pedido se procesen en orden?
- **Entregable:** Productor y consumidor funcionales publicando y leyendo eventos JSON de un Topic local.

### Bloque 3: Errores, Reintentos y Dead Letter Topics (50 - 80 min)

- **Qué estudiar:** Configuración de `DefaultErrorHandler`, `DeadLetterPublishingRecoverer` y `FixedBackOff`.
- **Pregunta a responder:** ¿Cómo evitar que un mensaje envenenado bloquee toda una partición?
- **Entregable:** Consumidor con política de reintento y DLT configurados y probados.

### Cierre y Consolidación (80 - 90 min)

- **Qué estudiar:** Repaso de las 5 reglas de oro de Kafka y comparativa inicial con RabbitMQ.
- **Pregunta a responder:** ¿En qué caso elegirías RabbitMQ sobre Kafka?
- **Entregable:** Notas consolidadas en la aplicación.


## Explícalo desde cero

### Por qué Kafka escala mucho mejor que una cola de mensajes tradicional para alto throughput

- **Qué entender:** Las colas tradicionales (ActiveMQ, RabbitMQ en modo cola) eliminan el mensaje del almacenamiento una vez confirmado por el consumidor. Kafka escribe secuencialmente en disco y nunca elimina mensajes (hasta la expiración del período de retención). Esto permite que los consumidores sean independientes: cada Consumer Group mantiene su propio puntero de offset. Un nuevo microservicio puede unirse al ecosistema y leer todos los eventos históricos desde el principio sin afectar a los demás.
- **Qué suele explicarse mal:** Pensar que Kafka es "un RabbitMQ más rápido". Son paradigmas fundamentalmente distintos: cola de trabajo vs log distribuido.
- **Ejemplo sencillo:** RabbitMQ es como un repartidor de correo postal: entrega la carta y se va, nadie puede volver a leerla. Kafka es como un periódico digital: el artículo permanece publicado y cualquiera puede ir a leerlo cuando quiera, empezando desde el primer número o desde hoy.
- **Señal de alerta:** Si intentas configurar Kafka para que elimine el mensaje inmediatamente después de que un consumidor lo lea.


## Preguntas

### Nivel 1 (Básico): ¿Qué ocurre si envías mensajes a un Topic de Kafka con clave null?

Evalúa si entiendes el particionamiento. Sin clave, Kafka utiliza un particionador round-robin (o sticky) que distribuye los mensajes entre todas las particiones del Topic sin orden lógico. Dos eventos del mismo pedido pueden terminar en particiones distintas y procesarse fuera de secuencia.

**Respuesta razonada:** Sin clave, Kafka no usa el hash para elegir partición. Desde Kafka 2.4 el productor aplica un particionador «pegajoso» (sticky): llena un lote para una partición y, al enviarlo, cambia a otra. Desde 3.3 lo hace de forma uniforme teniendo en cuenta la carga de cada broker. Se consigue buen reparto y buen rendimiento.

La consecuencia es que mensajes relacionados pueden acabar en particiones distintas y procesarse en cualquier orden, incluso a la vez en consumidores diferentes. Si «tarea completada» se procesa antes que «tarea creada», tu consumidor ve un estado imposible. Usa clave `null` solo cuando los mensajes son realmente independientes entre sí, como logs o métricas.

### Nivel 2 (Intermedio): ¿Por qué no se puede tener más consumidores activos en un Consumer Group que particiones en el Topic?

Kafka asigna cada partición a exactamente un consumidor dentro del grupo. Si hay más consumidores que particiones, los sobrantes quedan inactivos. No es posible que dos consumidores lean la misma partición dentro del mismo grupo.

**Respuesta razonada:** Porque cada partición se asigna como máximo a un consumidor dentro de un mismo grupo. Es lo que permite garantizar el orden por partición sin coordinación entre consumidores: si dos consumidores leyeran la misma partición, procesarían mensajes consecutivos en paralelo y en cualquier orden.

Si un topic tiene 6 particiones y despliegas 8 instancias en el mismo grupo, 2 quedan sin particiones y ociosas. No es del todo inútil: actúan como reserva y toman particiones en el rebalanceo si cae otra instancia. Pero para escalar el consumo hay que tener más particiones, por eso se suelen crear los topics con margen (12, 24...) aunque al principio haya pocas instancias.

### Nivel 3 (Entrevista Senior): Si un microservicio consumidor de Kafka hace un `commit` del offset y luego falla al guardar el resultado en la base de datos, ¿qué problema de consistencia ocurre?

El offset avanza pero el efecto de negocio no se persiste, provocando pérdida silenciosa de mensajes ("at-most-once delivery"). La solución es usar procesamiento idempotente: guardar el offset junto con el resultado en una misma transacción de base de datos, o diseñar las operaciones para ser idempotentes (que repetir el mismo evento no cause efectos duplicados).

**Respuesta razonada:** Si el offset se confirma antes de terminar el procesamiento y el guardado en base de datos falla, Kafka ya considera el mensaje como leído. Al reiniciar, el consumidor continúa desde el siguiente offset y ese mensaje se pierde para siempre. Es la semántica «como mucho una vez» (at-most-once), casi nunca deseable en datos de negocio.

Lo correcto es procesar primero y confirmar después (at-least-once), que es lo que hace Spring Kafka por defecto: confirma cuando tu listener termina sin excepción. El precio es que, si la aplicación cae entre el guardado y el commit, el mensaje se volverá a entregar. Por eso los consumidores deben ser idempotentes: guardar el id del mensaje procesado en la misma transacción que los datos y descartar repetidos, o usar operaciones naturalmente idempotentes como un upsert.


## Reto del tutor

**Reto:** Un equipo diseña un sistema donde el servicio de pedidos publica un evento `PedidoCreado` en Kafka. El servicio de facturación y el servicio de inventario deben consumir ese mismo evento de forma independiente. Un desarrollador junior configura los dos servicios con el MISMO `group-id: "microservicios"`. ¿Qué problema ocurrirá y cómo lo corregirías?

- **Lo que está bien:** Identifica que con el mismo group-id, Kafka entregará cada mensaje a solo UNO de los dos servicios (balanceo), no a ambos.
- **Respuesta superficial:** Decir que "hay que crear dos topics".
- **Lo que falta de fondo:** No comprender que los Consumer Groups son la unidad de suscripción en Kafka: todos los miembros de un mismo grupo se REPARTEN los mensajes. Para que ambos servicios reciban TODOS los mensajes, cada servicio debe tener su propio group-id único.
- **Respuesta senior:** El problema de raíz es conceptual: dentro de un Consumer Group, las particiones se REPARTEN entre los consumidores (solo uno recibe cada mensaje). Si facturación e inventario comparten el group-id "microservicios", cada evento llegará a uno u otro servicio aleatoriamente, pero jamás a ambos. La corrección es asignar group-ids independientes: `group-id: "servicio-facturacion"` y `group-id: "servicio-inventario"`. Así, cada Consumer Group mantiene su propio puntero de offset y ambos reciben el 100% de los eventos del Topic de forma independiente.


## Katas

### Kata 22.1: Productor y Consumidor Kafka con Spring Boot 3

Crea un `@RestController` que publique un evento `PedidoCreadoEvent(record)` en un Topic y un `@KafkaListener` que lo consuma, lo deserialice con Jackson, y lo imprima en log. Verifica con la consola de Kafka que el offset avanza.


## Reglas de oro

- Siempre incluye una clave significativa (key) en los mensajes para garantizar orden dentro de una partición.
- Cada microservicio consumidor debe tener su propio `group-id` único si quiere recibir todos los eventos.
- Configura siempre un Dead Letter Topic (DLT) para no perder mensajes que fallen tras los reintentos.
- El número de particiones de un Topic es el límite máximo de paralelismo de consumidores en un grupo.
- Diseña tus consumidores para ser idempotentes: que procesar el mismo evento dos veces no cause efectos duplicados.


## Proyecto guiado «Pedidos»: Eventos de pedido con Kafka

Avisar a otros servicios de lo que pasa con los pedidos sin llamarlos directamente.

**Tareas:**

- Publica `PedidoConfirmado` en el topic `pedidos.confirmados` con el id del pedido como clave.
- Crea un servicio `notificaciones` que lo consume en su propio consumer group.
- Configura reintentos y un Dead Letter Topic para los mensajes que fallan.

**Está listo cuando:**

- Si el servicio de notificaciones está caído, los eventos se procesan al volver.
- Un mensaje que falla siempre acaba en el DLT y no bloquea a los demás.


## Fuentes oficiales

- [Documentación de Apache Kafka](https://kafka.apache.org/documentation/)
- [Spring for Apache Kafka: referencia](https://docs.spring.io/spring-kafka/reference/)

