FinCodeCow · Глава XII

Анатомия «легковесных» фреймворков

Когда брокер событий становится вашим кластером: Kafka Streams, Samza и симбиоз с Kubernetes

Если «тяжеловесные» фреймворки (такие как Apache Spark или Flink) можно сравнить с мощной, но требовательной к ресурсам отдельной электростанцией, то «легковесные» фреймворки — это высокоэффективные генераторы, встроенные прямо в ваши микросервисы. Они не требуют выделения независимого кластера для обработки. Вместо этого они образуют неразрывный симбиоз с брокером событий (Apache Kafka) и системой управления контейнерами (CMS, например, Kubernetes).

В этой главе мы разберём, как работает эта парадигма, почему она стала «золотым стандартом» для stateful-микросервисов, и как с её помощью решать сложнейшие задачи обогащения данных и агрегации в реальном времени.

Это двенадцатая глава цикла. Она опирается на главу V (state stores, rebalance, cooperative sticky), главу VII (RocksDB, LSM, changelog, TTL), главу X (BPC как минимализм) и главу XI (тяжёлые фреймворки как противоположный полюс). Здесь мы замыкаем спектр: от простейшего poll-process-produce через embedded library до распределённых титанов Big Data.

• • •

IАрхитектурный сдвиг: брокер как фундамент

Главное отличие «легковесной» модели заключается в перераспределении ответственности. Фреймворк отказывается от управления кластером, полностью делегируя эти задачи инфраструктуре:

Брокер событий (Kafka)

Берёт на себя роль шины обмена данными (shuffle) и надёжного хранилища состояния (changelog). Внутренние топики используются для перераспределения данных между инстансами, а логи изменений гарантируют сохранность состояния при сбоях.

CMS (Kubernetes)

Отвечает за оркестрацию, масштабирование и отказоустойчивость. Добавление или удаление подов автоматически триггерит перераспределение партиций и восстановление состояния из логов.

Благодаря этому приложение развёртывается как обычный микросервис. Параллелизм контролируется стандартным механизмом групп потребителей (Consumer Groups), а данные одного ключа гарантированно обрабатываются на одном физическом узле, обеспечивая локальность данных.

Связь с главой XI

В «тяжёлом» фреймворке Master Node управляет задачами, а Worker'ы — вычислениями. В «лёгком» эту роль берут на себя Kafka (shuffle, changelog) и Kubernetes (оркестрация). Фреймворк остаётся только библиотекой — без кластера, без Master, без Zookeeper.

• • •

IIМагия внутреннего состояния и changelog

По умолчанию «легковесные» фреймворки используют внутреннее состояние (например, локальные key-value хранилища вроде RocksDB на SSD-дисках). Это обеспечивает микросекундные задержки при чтении и записи, недостижимые для внешних сетевых БД.

Но что происходит, когда инстанс падает или мы добавляем новый узел для масштабирования? Здесь на сцену выходит журнал изменений (changelog).

  1. Каждое обновление локального состояния асинхронно публикуется в скрытый внутренний топик брокера (changelog).
  2. При сбое или масштабировании новый инстанс подписывается на этот топик и «проигрывает» события, восстанавливая своё состояние с нуля.
  3. Compaction гарантирует, что объём восстановления пропорционален размеру состояния, а не числу обработанных событий.
  4. Checkpoint file позволяет начать восстановление с последней позиции, а не с нуля.

Вам не нужно настраивать сложные механизмы контрольных точек (checkpoints) или внешние хранилища — брокер событий уже сделал это за вас.

Связь с главой VII

В тяжёлых фреймворках checkpoint сохраняется в S3/HDFS. В Kafka Streams аналогом является changelog topic. Это два разных подхода к одной задаче: сохранение state для восстановления после сбоя. Разница — в уровне абстракции и стоимости.

• • •

IIIГетерогенное железо как преимущество

Поскольку каждый микросервис сам управляет своим локальным диском и состоянием, вы можете гибко подбирать железо. Один сервис, требующий огромной пропускной способности IOPS, можно запустить на инстансах с дорогими NVMe SSD, а другой, с большим, но редко читаемым состоянием — на дешёвых HDD.

Сервис Требования к state Железо
Fraud detection Высокий IOPS, низкая latency NVMe SSD
Рекомендации Большое состояние, редкие записи SATA SSD
Историческое окно Огромное состояние, редкое чтение HDD
Дедупликация Небольшое состояние, частые проверки NVMe SSD

В «тяжеловесных» кластерах такое разделение ресурсов крайне затруднено: TaskManager'ы гомогенны, и вы не можете дать одному job'у NVMe, а другому — HDD в одном кластере.

• • •

IVБесшовное масштабирование и hot standby

Масштабирование «легковесного» микросервиса — это по сути тот же процесс, что и восстановление после сбоя. Когда вы добавляете новый инстанс, CMS перераспределяет партиции. Новый узел должен загрузить состояние из changelog, прежде чем начать обрабатывать новые события. Это может занять время и вызвать временную задержку (lag).

Решение: горячие реплики (hot standby replicas)

Чтобы минимизировать простой при масштабировании или сбоях, Kafka Streams поддерживает концепцию горячих реплик.

Фреймворк запускает фоновые задачи, которые просто читают changelog-топики и держат локальное состояние в «тёплом» виде на резервных узлах. Когда основной узел падает или требуется добавить мощности, контроллер групп потребителей мгновенно переключает трафик на узел, у которого состояние уже полностью готово. Это обеспечивает почти нулевое время простоя (zero-downtime) при перераспределении нагрузки.

Стратегия Время восстановления Стоимость
Changelog replay Минуты–часы Базовая
Standby replicas Секунды (5–30 с) Двойной диск
Backup + restore Минуты Доп. хранилище
Практическое правило

Для критичных stateful-сервисов всегда включайте num.standby.replicas=1. Это снижает recovery time с минут до секунд за счёт двойного расхода диска. Для некритичных сервисов (аналитика, агрегация) можно обойтись changelog replay.

• • •

VKafka Streams vs Samza: две модели embedded

Многие ставят Kafka Streams и Samza в один ряд как «легковесные библиотеки». Но у них принципиально разные модели, и это влияет на выбор.

Критерий Kafka Streams Apache Samza (Embedded)
Backend Только Kafka Pluggable (Kafka, Kinesis, другие)
State backend RocksDB (Kafka-native) Pluggable (RocksDB, кастомные)
API High-level DSL + Processor API Low-level API, ближе к BPC
Exactly-once exactly_once_v2 (Kafka transactions) Через Kafka transactions
K8s native Да Да (embedded mode)
Зрелость Промышленный стандарт Меньше сообщество
Best for Kafka-to-Kafka pipelines Multi-broker среды
Практическое правило

Если вы живёте в мире Kafka — Kafka Streams. Если у вас несколько брокеров и нужна гибкость — Samza. Для 95% команд Kafka Streams — правильный выбор.

• • •

VIПаттерн обогащения: Stream-Table Join в действии

Одна из «киллер-фич» легковесных фреймворков — возможность выполнять реляционные соединения (JOIN) между бесконечными потоками событий и материализованными таблицами прямо в памяти, без обращения к внешним БД.

Сценарий: анализ рекламных конверсий

Представьте, что у вас есть поток кликов и просмотров рекламы (события). Вам нужно соединить их со справочником рекламных объявлений (сущности), чтобы понять, какой клиент владеет рекламой, и посчитать итоговую конверсию.

// Материализуем справочник рекламы в локальную таблицу (KTable)
KTable<AdId, Advertisement> adCatalog = builder.table("ads-topic");

// Берём поток пользовательских сессий (KStream)
KStream<UserId, UserAction> userSessions = builder.stream("sessions-topic");

// Группируем, агрегируем и соединяем со справочником
userSessions
    .groupByKey()
    .aggregate(...)  // Считаем конверсии
    .join(adCatalog, (conversion, ad) -> {
        // Обогащаем результат метаданными рекламы
        if (ad == null) return null;  // Обработка tombstone
        return new EnrichedReport(conversion, ad.getClientName());
    })
    .to("enriched-reports-topic");

Как это работает под капотом?

  1. Соподразделение (co-partitioning). Фреймворк автоматически гарантирует, что поток агрегатов и таблица справочника разбиты на партиции по одному и тому же ключу (AdId) и имеют одинаковое количество разделов.
  2. Локальность. Событие из потока и соответствующая строка из таблицы всегда попадают на один и тот же инстанс. JOIN происходит в локальной оперативной памяти (RocksDB) за микросекунды.
  3. Обработка удалений (tombstones). Если реклама удаляется из справочника, в топик приходит событие с null. Фреймворк корректно обрабатывает это, позволяя разработчику решить, что делать с «осиротевшими» агрегатами.

По своей сути, эта топология эквивалентна сложному SQL-запросу с FULL OUTER JOIN и GROUP BY, но выполняется она непрерывно, в реальном времени и с высокой производительностью.

• • •

VIICo-partitioning и GlobalKTable

Для stream-table join оба источника должны быть соподразделены: одинаковое число партиций и один и тот же partitioner. Kafka Streams не проверяет это автоматически — если партиции не совпадают, JOIN выдаст неполные результаты тихо и без ошибок.

Альтернатива — GlobalKTable: таблица, полностью реплицируемая на каждый инстанс. Она не требует co-partitioning и позволяет JOIN по произвольному полю через KeyValueMapper.

Тип JOIN Co-partitioning По ключу По произвольному полю
KStream-KStream Да Да Нет
KStream-KTable Да Да Нет
KStream-GlobalKTable Нет Нет Да
Когда использовать GlobalKTable

Для компактных справочников (страны, валюты, категории, тарифы), которые помещаются в память каждого инстанса. Для больших таблиц — избыточно, потому что репликация на каждый инстанс расходует память × N.

• • •

VIIIWindowing: окна в embedded-мире

Stream-table join — только один паттерн. Оконные операции — то, что делает Kafka Streams полноценным фреймворком:

// Tumbling window: заказы за каждый час
stream.groupByKey()
    .windowedBy(TimeWindows.ofSizeWithNoGrace(Duration.ofHours(1)))
    .count();

// Hopping window: rolling count за 15 минут, обновление каждую минуту
stream.groupByKey()
    .windowedBy(TimeWindows.ofSizeAndGrace(
        Duration.ofMinutes(15), Duration.ofMinutes(1))
        .advanceBy(Duration.ofMinutes(1)))
    .count();

// Session window: сессия пользователя с gap 30 минут
stream.groupByKey()
    .windowedBy(SessionWindows.ofInactivityGapWithNoGrace(
        Duration.ofMinutes(30)))
    .count();

Все четыре типа окон из главы V доступны из коробки: tumbling, hopping, sliding, session. Плюс — event time и grace period из главы VI.

• • •

IXExactly-once v2: атомарность из коробки

Kafka Streams предоставляет exactly_once_v2: атомарная транзакция, которая объединяет запись в state store, публикацию в output topic и коммит offset. Либо всё фиксируется, либо всё откатывается.

processing.guarantee=exactly_once_v2
Метрика At-least-once Exactly-once v2
Дубликаты в output Возможны Нет (в пределах Kafka)
Commit interval 30s 100ms (в 300 раз чаще)
Throughput Базовый -10–30%

Это ключевое отличие от BPC (глава X), где нужна ручная идемпотентность. В Kafka Streams достаточно включить одну настройку.

• • •

XInteractive Queries: state store как замена БД

State store — не только внутренний кэш для обработчика. Его можно читать снаружи через REST API — это заменяет отдельную БД для query-сценариев.

@GetMapping("/customers/{id}/total")
public Long getTotal(@PathVariable String id) {
    ReadOnlyKeyValueStore<String, Long> store = streams.store(
        StoreQueryParameters.fromNameAndType(
            "customer-totals",
            QueryableStoreTypes.keyValueStore()));
    return store.get(id);
}

Это даёт:

  • CQRS без внешней БД. Read model живёт прямо в state store.
  • Миллисекундный отклик. Нет сети между приложением и хранилищем.
  • Автоматическую актуализацию. Состояние обновляется по мере поступления событий.
Ограничение

State store локален для каждого инстанса. Если запрос приходит на инстанс, у которого нет нужного ключа, придётся маршрутизировать запрос к правильному инстансу через metadata service или использовать global state store.

• • •

XIОграничения Kafka Streams

Kafka Streams — не серебряная пуля. Честный разговор об ограничениях:

  1. JVM-lock. Только Java/Scala. Для Go/Python/Node.js нужны альтернативы (Faust, Node-rdkafka) или переход на BPC.
  2. State store размер. RocksDB растёт бесконечно, нет TTL (KIP-1225 ещё не реализован). Нужна ручная очистка через punctuator (глава VII).
  3. Инфраструктурный налог. Нужна Kafka, Schema Registry, Kafka Connect, мониторинг.
  4. Нет savepoints. Обновление топологии = reset local state. Альтернатива — новое имя store (глава VII).
  5. Co-partitioning не проверяется. Тихие ошибки при несовпадении партиций.
  6. Global state. Для больших таблиц не подходит — репликация × N.
Когда Kafka Streams — плохой выбор

Мультиязычные команды (Go, Python) — BPC или Faust. State > 100 GB на инстанс — Flink. Сложные joins десятков потоков — Flink. Строгие SLA на latency — Flink с event-time-first. Kafka Streams — оптимум для Kafka-to-Kafka pipelines средней сложности.

• • •

XIITesting Kafka Streams

TopologyTestDriver

Unit-тесты без брокера. Запускает топологию в том же JVM, подаёт события, проверяет выход. Работает за миллисекунды.

EmbeddedKafkaCluster

Integration-тесты с реальным Kafka в JVM. Проверка end-to-end: input → processing → state → output.

Testcontainers

Full-stack тесты с Kafka, Schema Registry и Connect в Docker. Ближе всего к production.

MockProcessorContext

Тестирование отдельных processor'ов в изоляции. Для низкоуровневого Processor API.

TopologyTestDriver driver = new TopologyTestDriver(topology, config);

TestInputTopic<String, OrderEvent> input = driver.createInputTopic(
    "orders", new StringSerializer(), new OrderEventSerializer());

TestOutputTopic<String, Long> output = driver.createOutputTopic(
    "customer-totals", new StringDeserializer(), new LongDeserializer());

input.pipeInput("order-1", new OrderEvent("cust-42", 150.0));

assertThat(output.readValueToMap()).containsEntry("cust-42", 1L);

driver.close();
• • •

XIIIOperational side: метрики и проблемы

Метрика Что показывает Порог алерта
rocksdb-block-cache-hit-ratio Эффективность кэша < 0.8
rocksdb-estimated-num-keys Количество ключей в store Растёт линейно без TTL
record-lag-max Changelog lag > 100k записей
commit-latency-avg Среднее время commit > 1 сек
rebalance-rate-per-hour Число rebalance за час > 2 в час
process-rate Скорость обработки Падение > 50%

Типичные production-проблемы

  • Rebalancing deadlock. Агрессивное автомасштабирование → постоянные rebalance (см. главу IX).
  • State store growth. RocksDB растёт без TTL — нужен punctuator cleanup.
  • Co-partitioning mismatch. Тихие ошибки в JOIN.
  • Changelog lag. Восстановление может занять часы.
• • •

XIVCosts: Kafka Streams vs BPC vs Flink

Сценарий BPC Kafka Streams Flink
Старт Часы Дни Недели
Операционные расходы Низкие Средние Высокие
Стоимость state Внешняя БД Локальный диск Локальный диск + checkpoint
Complexity Низкая Средняя Высокая
Best for Простые потоки Kafka-to-Kafka Analytics, ML
• • •

XVKSQL и Samza SQL: декларативный streaming

Если ваша команда не хочет писать топологии на Java, индустрия предлагает SQL поверх лёгких библиотек.

ksqlDB (Confluent)

Отдельный сервер, который принимает SQL-запросы и генерирует топологии Kafka Streams. Поддерживает streaming SQL, materialized views, pull и push queries. Идеально для аналитиков.

Samza SQL

Встроенный SQL в Samza. Более ограничен, чем ksqlDB, но работает embedded. Подходит для простых сценариев.

-- ksqlDB: session windows для рекламы
CREATE STREAM ad_views (
    userId VARCHAR KEY,
    adId VARCHAR,
    eventTime TIMESTAMP
) WITH (
    KAFKA_TOPIC = 'ad_views',
    VALUE_FORMAT = 'JSON',
    TIMESTAMP = 'eventTime'
);

CREATE TABLE sessions AS
SELECT userId,
       COUNT(*) AS view_count,
       WINDOWSTART AS session_start,
       WINDOWEND AS session_end
FROM ad_views
WINDOW SESSION (30 MINUTES)
GROUP BY userId
EMIT CHANGES;
Важно

Под капотом ksqlDB генерирует стандартные топологии Kafka Streams. Все преимущества и ограничения (JVM-lock, state store size, no savepoints) наследуются.

• • •

XVIЧеклисты и связь с циклом

Связь с предыдущими главами

Глава Фокус Связь с текущей главой
V. Механика потоковой обработки State stores, rebalance, cooperative sticky Kafka Streams использует те же механизмы на уровне библиотеки
VI. Время в потоках Event time, watermarks, windowing Windowing в Kafka Streams использует event time через retention
VII. Память микросервисов RocksDB, changelog, TTL, IQ Kafka Streams — практическая реализация всех этих механизмов
X. Базовый шаблон Производитель-Потребитель BPC как минимализм Kafka Streams — эволюция BPC с внутренним state
XI. Тяжеловесные фреймворки Flink, Spark, Beam, Storm Kafka Streams — противоположный полюс: embedded вместо кластера
XII. Легковесные фреймворки Kafka Streams, Samza Золотая середина между BPC и тяжёлыми фреймворками

Чеклист выбора Kafka Streams

  • Команда пишет на Java/Scala (или готова использовать Faust для Python)
  • Backend — Kafka, без планов миграции на другой брокер
  • State умеренный (до 50–100 GB на инстанс)
  • Нужны stream-table joins для обогащения
  • Нужны оконные агрегации (tumbling, hopping, sliding, session)
  • Нужна атомарность state + output + offset (exactly_once_v2)
  • Нужны Interactive Queries для CQRS
  • Готовы к инфраструктурному налогу: Kafka, Schema Registry, Connect, мониторинг
  • Готовы к JVM-lock или используете Faust/Node-rdkafka
  • Co-partitioning проверен вручную для всех JOIN
  • Reimplemented TTL через punctuator для растущих stores
  • TopologyTestDriver используется в CI
  • Метрики RocksDB, changelog lag, rebalance rate настроены
  • Standby replicas включены для критичных сервисов
  • Cost сравнён с BPC и Flink для конкретного сценария

Ключевые выводы главы

  1. Брокер как фундамент. Kafka берёт на себя shuffle, changelog и HA. Фреймворк — только библиотека.
  2. Changelog + checkpoint — механизм восстановления state без внешних хранилищ.
  3. Гетерогенное железо — преимущество embedded-модели: NVMe для fraud detection, HDD для аналитики.
  4. Hot standby replicas — recovery за секунды вместо минут.
  5. Kafka Streams vs Samza: Kafka-only vs pluggable backend.
  6. Stream-table joins — killer feature для обогащения в реальном времени.
  7. Co-partitioning не проверяется автоматически. GlobalKTable — альтернатива.
  8. Windowing из коробки: tumbling, hopping, sliding, session.
  9. exactly_once_v2 — атомарность state + output + offset.
  10. Interactive Queries — чтение state извне через REST API.
  11. Ограничения: JVM-lock, state store size, no savepoints, infrastructure tax.
  12. Testing: TopologyTestDriver, EmbeddedKafkaCluster, Testcontainers.
  13. Operational: метрики RocksDB, changelog lag, rebalance rate.
  14. Costs: между BPC и Flink по операционным расходам.
  15. KSQL/ksqlDB — декларативный streaming поверх Kafka Streams.
Финальный тезис

«Легковесные» фреймворки — это «золотое сечение» событийно-управляемой архитектуры. Они дают мощь stateful processing без кластерной инфраструктуры, гибкость микросервисов без внешних state stores, и скорость локальных SSD без сетевых задержек. Для 80% production-сценариев Kafka Streams достаточно. Для остальных 20% есть BPC, FaaS, Flink и Spark — каждый инструмент занимает свою нишу.

Глава XII цикла «Событийно-управляемая архитектура». Источники: Apache Kafka, Confluent, Conduktor, практические кейсы production-систем, FinCodeCow experience.

FinCodeCow · Chapter XII

Anatomy of Lightweight Frameworks

When the event broker becomes your cluster: Kafka Streams, Samza, and the symbiosis with Kubernetes

If heavyweight frameworks (like Apache Spark or Flink) can be compared to a powerful but resource-hungry dedicated power plant, then lightweight frameworks are highly efficient generators embedded directly in your microservices. They don't require a dedicated cluster for processing. Instead, they form an inseparable symbiosis with the event broker (Apache Kafka) and the container management system (CMS, e.g., Kubernetes).

In this chapter, we will explore how this paradigm works, why it has become the "gold standard" for stateful microservices, and how it solves the most complex data enrichment and real-time aggregation tasks.

This is the twelfth chapter of the series. It builds on Chapter V (state stores, rebalance, cooperative sticky), Chapter VII (RocksDB, LSM, changelog, TTL), Chapter X (BPC as minimalism), and Chapter XI (heavyweight frameworks as the opposite pole). Here we close the spectrum: from the simplest poll-process-produce through an embedded library to the distributed titans of Big Data.

• • •

IArchitectural Shift: Broker as Foundation

Event Broker (Kafka)

Takes on the role of the data exchange bus (shuffle) and the reliable state store (changelog). Internal topics redistribute data between instances, and change logs guarantee state preservation during failures.

CMS (Kubernetes)

Handles orchestration, scaling, and fault tolerance. Adding or removing pods automatically triggers partition redistribution and state restoration from logs.

Thanks to this, the application is deployed as a regular microservice. Parallelism is controlled by standard consumer groups, and data of one key is guaranteed to be processed on one physical node, ensuring data locality.

Link to Chapter XI

In a heavyweight framework, the Master Node manages tasks, and Workers do computations. In a lightweight one, these roles are taken by Kafka (shuffle, changelog) and Kubernetes (orchestration). The framework remains just a library — no cluster, no Master, no Zookeeper.

• • •

IIThe Magic of Internal State and Changelog

  1. Every update of local state is asynchronously published to a hidden internal broker topic (changelog).
  2. On failure or scaling, the new instance subscribes to this topic and "replays" events, restoring its state from scratch.
  3. Compaction guarantees that restoration volume is proportional to state size, not to the number of processed events.
  4. Checkpoint file allows starting restoration from the last position, not from scratch.
Link to Chapter VII

In heavyweight frameworks, the checkpoint is saved to S3/HDFS. In Kafka Streams, the analog is the changelog topic. These are two different approaches to one task: state preservation for recovery after a failure.

• • •

IIIHeterogeneous Hardware as Advantage

Since each microservice manages its own local disk and state, you can flexibly choose hardware. One service requiring huge IOPS can run on instances with expensive NVMe SSD, another with large but rarely read state — on cheap HDD.

In heavyweight clusters, such resource separation is extremely difficult: TaskManagers are homogeneous.

• • •

IVSeamless Scaling and Hot Standby

Scaling a lightweight microservice is essentially the same process as recovery after a failure. When you add a new instance, CMS redistributes partitions. The new node must load state from changelog before processing new events.

Solution: Hot Standby Replicas

Kafka Streams supports the concept of hot standby replicas. Background tasks read changelog topics and keep local state "warm" on standby nodes. When the primary node fails, the consumer group controller instantly switches traffic to the node whose state is already fully ready.

Strategy Recovery Time Cost
Changelog replay Minutes–hours Base
Standby replicas Seconds (5–30 s) Double disk
Backup + restore Minutes Extra storage
• • •

VKafka Streams vs Samza: Two Embedded Models

Criterion Kafka Streams Apache Samza (Embedded)
Backend Kafka only Pluggable (Kafka, Kinesis, others)
State backend RocksDB (Kafka-native) Pluggable (RocksDB, custom)
API High-level DSL + Processor API Low-level API, closer to BPC
Exactly-once exactly_once_v2 (Kafka transactions) Via Kafka transactions
Best for Kafka-to-Kafka pipelines Multi-broker environments
• • •

VIEnrichment Pattern: Stream-Table Join in Action

// Materialize ad catalog into a local table (KTable)
KTable<AdId, Advertisement> adCatalog = builder.table("ads-topic");

// Take user sessions stream (KStream)
KStream<UserId, UserAction> userSessions = builder.stream("sessions-topic");

// Group, aggregate, and join with the catalog
userSessions
    .groupByKey()
    .aggregate(...)  // Count conversions
    .join(adCatalog, (conversion, ad) -> {
        if (ad == null) return null;  // Tombstone handling
        return new EnrichedReport(conversion, ad.getClientName());
    })
    .to("enriched-reports-topic");
  1. Co-partitioning. Framework guarantees both streams are partitioned by the same key (AdId) with the same number of partitions.
  2. Locality. Event and table row always land on the same instance. JOIN happens in local memory (RocksDB) in microseconds.
  3. Tombstone handling. Deleted ads come as null — the developer decides what to do with orphaned aggregates.
• • •

VIICo-partitioning and GlobalKTable

Kafka Streams does not check co-partitioning automatically — if partitions don't match, the JOIN silently produces incomplete results. Alternative — GlobalKTable, fully replicated to every instance.

• • •

VIIIWindowing: Windows in the Embedded World

// Tumbling window: orders per hour
stream.groupByKey()
    .windowedBy(TimeWindows.ofSizeWithNoGrace(Duration.ofHours(1)))
    .count();

// Session window: user session with 30-min gap
stream.groupByKey()
    .windowedBy(SessionWindows.ofInactivityGapWithNoGrace(
        Duration.ofMinutes(30)))
    .count();
• • •

IXExactly-once v2: Atomicity Out of the Box

processing.guarantee=exactly_once_v2

Kafka Streams provides exactly_once_v2: an atomic transaction combining state store writes, output topic publications, and offset commits. Either all is committed, or all is rolled back.

• • •

XInteractive Queries: State Store as a Database Replacement

@GetMapping("/customers/{id}/total")
public Long getTotal(@PathVariable String id) {
    ReadOnlyKeyValueStore<String, Long> store = streams.store(
        StoreQueryParameters.fromNameAndType(
            "customer-totals",
            QueryableStoreTypes.keyValueStore()));
    return store.get(id);
}

This gives CQRS without an external DB: the read model lives in the state store, millisecond response, automatic actualization.

• • •

XIKafka Streams Limitations

  1. JVM lock. Java/Scala only. For Go/Python/Node.js — alternatives or BPC.
  2. State store size. RocksDB grows infinitely, no TTL (KIP-1225 not yet implemented).
  3. Infrastructure tax. Kafka, Schema Registry, Kafka Connect, monitoring.
  4. No savepoints. Topology update = reset local state.
  5. Co-partitioning not checked. Silent errors when partitions don't match.
  6. Global state limits. Not suitable for large tables — replication × N.
• • •

XIITesting Kafka Streams

TopologyTestDriver driver = new TopologyTestDriver(topology, config);

TestInputTopic<String, OrderEvent> input = driver.createInputTopic(
    "orders", new StringSerializer(), new OrderEventSerializer());

TestOutputTopic<String, Long> output = driver.createOutputTopic(
    "customer-totals", new StringDeserializer(), new LongDeserializer());

input.pipeInput("order-1", new OrderEvent("cust-42", 150.0));

assertThat(output.readValueToMap()).containsEntry("cust-42", 1L);

driver.close();
• • •

XIIIOperational Side: Metrics and Issues

Metric What It Shows Alert Threshold
rocksdb-block-cache-hit-ratio Cache efficiency < 0.8
record-lag-max Changelog lag > 100k records
commit-latency-avg Average commit time > 1 sec
rebalance-rate-per-hour Rebalances per hour > 2 per hour
• • •

XIVCosts: Kafka Streams vs BPC vs Flink

Scenario BPC Kafka Streams Flink
Start Hours Days Weeks
Operational cost Low Medium High
State cost External DB Local disk Local disk + checkpoint
Complexity Low Medium High
• • •

XVKSQL and Samza SQL: Declarative Streaming

-- ksqlDB: session windows for ads
CREATE TABLE sessions AS
SELECT userId,
       COUNT(*) AS view_count,
       WINDOWSTART AS session_start,
       WINDOWEND AS session_end
FROM ad_views
WINDOW SESSION (30 MINUTES)
GROUP BY userId
EMIT CHANGES;
Important

Under the hood, ksqlDB generates standard Kafka Streams topologies. All advantages and limitations (JVM lock, state store size, no savepoints) are inherited.

• • •

XVIChecklists and Link to the Series

Link to Previous Chapters

Chapter Focus Link to This Chapter
V. The Mechanics of Stream Processing State stores, rebalance, cooperative sticky Kafka Streams uses the same mechanisms at library level
VI. Time in Streams Event time, watermarks, windowing Windowing in Kafka Streams uses event time via retention
VII. The Memory of Microservices RocksDB, changelog, TTL, IQ Kafka Streams — practical implementation of all these mechanisms
X. Basic Producer/Consumer Pattern BPC as minimalism Kafka Streams — evolution of BPC with internal state
XI. Heavyweight Frameworks Flink, Spark, Beam, Storm Kafka Streams — opposite pole: embedded instead of cluster
XII. Lightweight Frameworks Kafka Streams, Samza The golden middle between BPC and heavyweight frameworks

Kafka Streams Selection Checklist

  • Team writes in Java/Scala (or ready to use Faust for Python)
  • Backend is Kafka, no plans to migrate to another broker
  • Moderate state (up to 50–100 GB per instance)
  • Stream-table joins needed for enrichment
  • Windowed aggregations needed (tumbling, hopping, sliding, session)
  • Atomicity of state + output + offset needed (exactly_once_v2)
  • Interactive Queries needed for CQRS
  • Ready for infrastructure tax: Kafka, Schema Registry, Connect, monitoring
  • Ready for JVM lock or using Faust/Node-rdkafka
  • Co-partitioning verified manually for all JOINs
  • TTL reimplemented via punctuator for growing stores
  • TopologyTestDriver used in CI
  • RocksDB metrics, changelog lag, rebalance rate configured
  • Standby replicas enabled for critical services
  • Cost compared with BPC and Flink for specific scenario
Final Thesis

Lightweight frameworks are the "golden middle" of event-driven architecture. They provide the power of stateful processing without cluster infrastructure, the flexibility of microservices without external state stores, and the speed of local SSD without network delays. For 80% of production scenarios, Kafka Streams is enough. For the remaining 20%, there is BPC, FaaS, Flink, and Spark — each tool occupies its niche.

Chapter XII of the "Event-Driven Architecture" series. Sources: Apache Kafka, Confluent, Conduktor, production system case studies, FinCodeCow experience.