FinCodeCow · Глава VI

Время в потоках

Детерминированная обработка и борьба с хаосом: watermarks, окна, late events и reprocessing

В идеальном мире события прибывают в брокер и покидают его строго в том порядке, в котором они были созданы. Часы на всех серверах синхронизированы до наносекунды, сеть никогда не рвётся, а потребители обрабатывают данные мгновенно. Но мы живём в реальности. Сетевые пакеты застревают, серверы перезагружаются, а производители событий временно теряют связь с брокером.

Как гарантировать, что микросервис, обрабатывающий финансовые транзакции или аналитику кликов, выдаст один и тот же результат независимо от того, обрабатывает ли он данные в реальном времени или «переваривает» вчерашний завал? Ответ кроется в концепции детерминированной обработки потоков.

Это шестая глава цикла о событийно-управляемой архитектуре. Мы уже разобрали, почему EDA нужна, как она устроена под капотом, как проектировать контракты, как подключить legacy и как работает потоковая обработка. Теперь — о самом сложном: о времени.

• • •

IИллюзия времени: событие vs обработка

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

Время события (Event Time)

Момент, когда факт действительно произошёл в реальном мире. Когда пользователь нажал «Оплатить» или датчик IoT зафиксировал температуру. Хранится в payload события или в metadata записи Kafka.

Время обработки (Processing Time)

Момент, когда микросервис получил и начал обрабатывать событие. Зависит от задержек сети, очередей, загрузки CPU и сборщика мусора. Нестабильно и недетерминировано.

Время приёма (Ingestion Time)

Момент, когда событие попало в брокер (Kafka timestamp). Промежуточный вариант: детерминирован в рамках одного брокера, но не отражает реальное время события.

Три времени — три роли

Event time — для бизнес-логики. Ingestion time — для брокерной сортировки. Processing time — для метрик мониторинга.

Свойство Event Time Processing Time Ingestion Time
Детерминированность Да Нет В рамках одного брокера
Устойчивость к сбоям Да Нет Частично
Точность бизнес-логики Максимальная Низкая Средняя
Сложность реализации Высокая Низкая Средняя
Классический пример

Банковский счёт. В 10:00 клиент вносит $100 (event time). В 10:05 снимает $150. Если из-за сбоя сети событие снятия пришло в систему первым, использование времени обработки приведёт к ошибочному штрафу за овердрафт. Только время события спасает бизнес-логику.

• • •

IITimestamp Assigner: откуда система берёт event time

Прежде чем двигаться дальше, нужно ответить на фундаментальный вопрос: откуда система знает event time? Событие — это просто байты. Время нужно откуда-то извлечь.

Kafka Streams

По умолчанию Kafka Streams использует timestamp записи Kafka — то, что продюсер положил в metadata. Это может быть время отправки (create time) или время приёма брокером (log append time) — зависит от настройки топика message.timestamp.type.

Если timestamp Kafka не подходит (например, событие реплицировано из другой системы, и метка не отражает реальное время), можно задать свой TimestampExtractor:

public class OrderTimestampExtractor implements TimestampExtractor {
    @Override
    public long extract(ConsumerRecord<Object, Object> record, long partitionTime) {
        OrderEvent order = (OrderEvent) record.value();
        return order.getOccurredAt().toEpochMilli();
    }
}

// В конфигурации:
props.put(StreamsConfig.DEFAULT_TIMESTAMP_EXTRACTOR_CLASS_CONFIG,
          OrderTimestampExtractor.class);

Особенно важно: если payload не содержит корректного timestamp, продюсер может положить отрицательное значение. Тогда Kafka Streams выбросит StreamsException. Для graceful-обработки используйте WallclockTimestampExtractor (fallback на текущее время) или FailOnInvalidTimestamp (для строгой валидации).

Apache Flink

В Flink event time извлекается через TimestampAssigner, который задаётся на источнике:

DataStream<OrderEvent> orders = env
    .addSource(new KafkaSource<>(...))
    .assignTimestampsAndWatermarks(
        WatermarkStrategy.<OrderEvent>forBoundedOutOfOrderness(
                Duration.ofMinutes(2))
            .withTimestampAssigner((event, ts) ->
                event.getOccurredAt().toEpochMilli()));

Ключевое отличие от Kafka Streams: во Flink timestamp assigner и watermark strategy задаются явно. Это требует больше кода, но даёт полный контроль над моделью времени.

Практическое правило

Если payload уже содержит event time — используйте его через TimestampExtractor/TimestampAssigner. Если нет — используйте ingestion time (Kafka timestamp), но помните: он не отражает реальное время события.

• • •

IIIПланировщик событий: дирижёр оркестра

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

Планировщик событий (Event Scheduler) буферизует входящие записи и всегда отдаёт в бизнес-логику событие с наименьшей временной меткой. Это гарантирует, что даже если события перепутались на уровне сети, микросервис восстановит их историческую справедливость.

Цена: буферизация требует памяти. Чем больше окно и чем больше партиций, тем больше буфер. Для session windows с timeout 30 минут и 100 партиций буфер может занимать гигабайты.

Важное ограничение

Планировщик сортирует события только в пределах одной партиции. Между партициями глобального порядка нет. Если нужно собрать все события одного пользователя — используйте repartitioning (см. главу V).

• • •

IVВодяные знаки: как понять, что «прошлое» уже прошло

Если мы буферизуем события для сортировки, возникает вечный вопрос: как долго ждать опоздавших? Бесконечно ждать нельзя — память не резиновая, а бизнесу нужны результаты.

Водяной знак (Watermark) — это монотонно возрастающий timestamp, который объявляет: «Все события с временем T и ранее уже гарантированно прибыли». Если после водяного знака приходит событие со временем T−1, система понимает: это запоздалое событие (Late Event).

Водяной знак вычисляется на основе наблюдаемых временных меток событий. Стратегия генерации определяет, насколько агрессивно система «сдвигает» границу.

Periodic Watermark

Водяной знак генерируется периодически (например, каждые 200 мс), на основе максимального event time, наблюдённого за период, минус допустимая задержка:

watermark = max(eventTime) - allowedLateness

Например, если максимальное наблюдённое event time — 10:05, а allowed lateness — 2 минуты, watermark = 10:03. Все окна до 10:03 можно закрывать.

Punctuated Watermark

Водяной знак генерируется при каждом специальном событии (например, маркер конца сессии). Редко используется, подходит для потоков с нерегулярными интервалами.

Пример настройки во Flink

WatermarkStrategy.<OrderEvent>forBoundedOutOfOrderness(
        Duration.ofSeconds(30))
    .withTimestampAssigner((event, ts) -> event.getOccurredAt().toEpochMilli())
    .withIdleness(Duration.ofMinutes(5));

Здесь forBoundedOutOfOrderness означает: «мы ожидаем, что события опаздывают не более чем на 30 секунд».

• • •

VWatermark propagation и watermark alignment

Watermark движется по топологии: каждый оператор получает водяные знаки от всех входных потоков и берёт минимум.

Source A: watermark = 10:05
Source B: watermark = 10:02  →  Operator: watermark = min(10:05, 10:02) = 10:02
Source C: watermark = 10:07

Это объясняет, почему один медленный или молчащий поток блокирует всю топологию: если один источник не шлёт события, его watermark не двигается, и общий минимум застывает.

Watermark alignment — механизм «притормаживания»

В больших топологиях разные источники могут обрабатывать данные с разной скоростью. Если один источник отстаёт на часы, а остальные — на секунды, минимум watermark застывает, и окна не закрываются.

Flink предоставляет watermark alignment: операторы могут «сообщать» upstream, что они отстают, и upstream-операторы притормаживают свои watermarks, чтобы дать отстающему догнать.

WatermarkStrategy.<OrderEvent>forBoundedOutOfOrderness(
        Duration.ofSeconds(30))
    .withWatermarkAlignment(
        "alignment-group-1",
        Duration.ofMinutes(5),   // максимальное расхождение
        Duration.ofSeconds(30)); // интервал проверки

Это более продвинутый механизм, чем withIdleness: он не исключает источник, а временно «придерживает» остальные, чтобы сохранить консистентность.

Когда использовать

withIdleness — когда источник может полностью замолчать (ночной трафик). Watermark alignment — когда источники работают с разной скоростью (например, одна партиция читает быстрее другой).

• • •

VIПроблема простаивающих источников (Idle Sources)

Если одна партиция замолчала (например, ночной трафик по одному региону прекратился), её watermark перестаёт двигаться. Поскольку оператор берёт минимум по всем входам, общая watermark застывает, и окна не закрываются.

Решение в Flink: withIdleness(Duration.ofMinutes(5)) — если источник не получает событий 5 минут, он исключается из расчёта watermark. Окна продолжают закрываться по остальным источникам.

В Kafka Streams этой проблемы нет в явном виде — окна закрываются по wall-clock time, а не по watermark. Но есть другая проблема: если партиция замолчала, окно не получит новых событий и будет закрыто по истечении grace period.

• • •

VIIОкна: нарезка времени на куски

Бизнес-логика редко работает с абстрактным «потоком». Ей нужны агрегаты: «сколько кликов за последний час», «какова сессия пользователя». Для этого используются оконные функции.

Тип окна Перекрытие Привязка к сетке Событий в нескольких окнах Где используется
Tumbling Нет Да 1 Периодические отчёты
Hopping Да Да N Rolling-метрики
Sliding Да Нет (к событиям) N JOIN, корреляция
Session Динамическое Нет Зависит Поведение пользователей

Переворачивающиеся (Tumbling) — фиксированные, непересекающиеся интервалы. Каждое событие попадает ровно в одно окно. Идеально для периодических отчётов: «выручка за каждый час».

Прыгающие (Hopping) — фиксированный размер, но шаг меньше размера. Окно «перекатывается» с заданным интервалом. Одно событие может попасть в несколько окон. Используется для rolling-метрик: «5-минутное скользящее среднее, обновление каждую минуту».

Скользящие (Sliding) — окно привязано не к фиксированной сетке, а к временным меткам самих событий. Два события агрегируются, если их временные метки в пределах размера окна. В Kafka Streams sliding windows используются только для JOIN-операций. Идеально для корреляции: «дважды неудачный логин в течение 30 секунд».

Пример настройки в Kafka Streams

TimeWindows windows = TimeWindows
    .ofSizeAndGrace(Duration.ofMinutes(5), Duration.ofMinutes(1));

stream.groupByKey()
    .windowedBy(windows)
    .count();

Здесь окно — 5 минут, grace period — 1 минута: опоздавшие события в пределах 1 минуты после закрытия окна будут приняты и вызовут корректирующий результат.

• • •

VIIISession windows и механизм слияния

Сеансовые окна (Session Windows) — динамические окна, не привязанные к часам. Окно открыто, пока пользователь активен. Пауза (timeout бездействия, например 30 минут) закрывает окно.

Ключевая особенность session windows — слияние (merge). Если новая активность «дотягивается» до предыдущей (gap < timeout), два окна объединяются в одно.

Session 1: [10:00 — 10:05]
Session 2: [10:07 — 10:15]
Gap = 2 минуты < timeout (30 минут)
→ Объединённая сессия: [10:00 — 10:15]

Это основа для анализа пользовательского поведения: клики, просмотры, покупки в рамках одной сессии могут быть «склеены» в единое событие.

В Kafka Streams session windows используются так:

stream.groupByKey()
    .windowedBy(SessionWindows.ofInactivityGapWithNoGrace(
        Duration.ofMinutes(30)))
    .count();
Стоимость session windows

Session windows держат состояние до тех пор, пока сессия активна. Для пользователя, который не закрывает браузер неделю, сессия будет расти. Настройте максимальный размер сессии или используйте grace period для принудительного закрытия.

• • •

IXOut-of-order vs Late: две разные проблемы

Не все «неправильные» события одинаковы.

Out-of-order

События пришли не в порядке event time, но до watermark. Например, событие 10:03 пришло раньше события 10:01, но watermark ещё 09:55. Планировщик переупорядочит их — это нормальная работа системы.

Late

Событие пришло после watermark. Окно может быть уже закрыто. Это требует стратегии (см. следующий раздел).

Различие критично: out-of-order — штатная ситуация, late — исключительная. Система должна быть спроектирована так, чтобы out-of-order обрабатывались прозрачно, а late — осознанно.

• • •

XТри стратегии борьбы с запоздалыми событиями

Рано или поздно watermark уйдёт вперёд, и прилетит событие, которое «опоздало». Что с ним делать — это не только технический, но и бизнес-вопрос.

Отбросить (Drop)

Окно закрыто, результат ушёл. Позднее событие игнорируется. Подходит для телеметрии, где важна актуальность, а не история.

В Kafka Streams — режим по умолчанию при отсутствии grace period. В Flink — события уходят в side output для анализа.

Льготный период (Grace Period / Allowed Lateness)

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

Разница между фреймворками:

  • Kafka Streams: grace period — единый буфер до финального закрытия.
  • Flink: allowed lateness — окно может переоткрываться многократно, пока allowed lateness не исчерпан.

Перенаправить (Side Output)

Позднее событие не отбрасывается молча, а направляется в отдельный поток (side output). Этот поток можно анализировать, репроцессить или применять fallback-логику.

OutputTag<OrderEvent> lateOutputTag = new OutputTag<>("late-orders"){};

SingleOutputStreamOperator<Result> result = stream
    .keyBy(OrderEvent::getCustomerId)
    .window(TumblingEventTimeWindows.of(Time.minutes(5)))
    .allowedLateness(Time.minutes(1))
    .sideOutputLateData(lateOutputTag)
    .aggregate(new OrderAggregator());

DataStream<OrderEvent> lateOrders = result.getSideOutput(lateOutputTag);
Стратегия Kafka Streams Flink Когда выбирать
Drop По умолчанию По умолчанию Телеметрия, клики, неважные данные
Grace / Allowed lateness TimeWindows.grace(...) .allowedLateness(...) Финансы, биллинг, инвентаризация
Side output Вручную через branch Встроено Аудит поздних данных, fallback-логика
Wait (бесконечное ожидание) Не поддерживается Не поддерживается Не рекомендуется — рост состояния
• • •

XIВыбор размера grace period: latency vs completeness

Grace period — это компромисс между задержкой выдачи результата (latency) и полнотой данных (completeness). Как выбрать?

  1. Анализируйте distribution of delay. Если 95% событий приходят в течение 30 секунд, grace period 1 минуты покроет 95% случаев. Grace period 5 минут покроет 99.9%, но добавит 5 минут latency.
  2. Различайте критичность. Для real-time дашбордов (клики, трафик) — grace 30 секунд, completeness 95%. Для биллинга — grace 10 минут, completeness 99.99%. Для ежедневной финансовой отчётности — grace 24 часа, completeness 100%.
  3. Учитывайте cost of reprocessing. Если позднее событие отброшено, но данные важны — всегда можно reprocessing-ом. Grace period = буфер между «достаточно быстро» и «достаточно точно».
  4. Измеряйте. Метрика для мониторинга: late-events-rate — доля событий, пришедших после watermark. Если растёт — grace period нужно увеличить.
Правило по критичности

Real-time дашборды: grace 30 секунд. Биллинг и инвентаризация: grace 10 минут. Финансовая отчётность: grace 24 часа.

• • •

XIIFlink vs Kafka Streams: две модели времени

Фреймворки принципиально расходятся в подходе к времени, и это влияет на выбор под задачу.

Критерий Flink Kafka Streams
Модель времени Event-time-first, watermarks Wall-clock + event time через retention
Watermarks Встроенные, настраиваемые стратегии Нет в классическом понимании
Late events Allowed lateness + side outputs Grace period, drop после закрытия
Idle sources withIdleness() Нет явной проблемы (wall-clock)
Watermark alignment Да Нет
Timestamp assigner Явный Через TimestampExtractor
Out-of-order Планировщик переупорядочивает Ограничено (только в пределах партиции)
Event-time JOIN Да, через watermark alignment Да, через windowed join
Сложность Выше (настройка watermarks) Ниже (wall-clock fallback)

Flink — когда нужна точная event-time обработка с watermarks, сложная аналитика, multi-source JOIN, ML-пайплайны. Kafka Streams — когда вход и выход в Kafka, нужна простота деплоя и приемлем wall-clock fallback.

• • •

XIIIСуперсила EDM: reprocessing

Неизменяемые журналы событий дают потрясающую возможность. Если найдена ошибка в бизнес-логике или выпущена новая схема, можно просто перемотать offset на начало и прогнать данные через микросервис заново. Благодаря детерминированной обработке и планировщику событий, переработка исторических данных выдаст точно такой же порядок и результаты, как если бы обработка шла в реальном времени.

Но reprocessing — не «нажать кнопку». Нужно учесть:

  1. Очистить state store. Перед reprocessing локальный RocksDB нужно сбросить, иначе агрегаты будут некорректно наложены на старое состояние.
  2. Output topic. Либо очистить выходной топик, либо писать в новый. Запись в тот же топик поверх старых данных создаст дубликаты.
  3. Schema evolution. События годичной давности могут быть записаны по старой схеме. Новая версия кода может не десериализовать их. Решение — upcasting: функция-преобразователь, которая читает старую версию события и переводит в новую in-memory.
  4. Побочные эффекты. Если топология включает отправку email, вызов стороннего API или платёжного шлюза, reprocessing приведёт к катастрофе: клиенты получат дублирующие письма за прошлые годы. Перед reprocessing всегда отключайте или ставьте на заглушки все неидемпотентные внешние вызовы.

Пример upcasting

public OrderEvent upcast(OrderEventV1 old) {
    return new OrderEvent(
        old.getId(),
        old.getCustomerId(),
        old.getAmount(),
        null,                       // paymentMethod — новое поле
        old.getOccurredAt()
    );
}

Оценка времени reprocessing

Reprocessing 1 ТБ данных через Kafka Streams при 50k msg/sec ≈ 30–60 минут. Но это очень приблизительно. Ключевые факторы:

  • Bottleneck может быть в state store, а не в обработке. RocksDB на медленном диске читает в 10 раз медленнее, чем на NVMe.
  • Batch size — большие батчи ускоряют throughput, но увеличивают memory pressure.
  • Параллелизм — ограничен числом партиций. Если партиций 6, а вы запустили 12 инстансов — 6 простаивают.

Чеклист перед reprocessing

  • State store очищен
  • Output topic — новый или очищен
  • Все внешние вызовы (email, SMS, платёжные API) отключены или идемпотентны
  • Schema upcasters настроены для старых версий событий
  • Offset сброшен на начало (или на нужную позицию)
  • Мониторинг lag включён — reprocessing создаёт всплеск нагрузки
  • Оценено время: reprocessing 1 ТБ данных ≈ 30–60 минут при 50k msg/sec
  • Диск под state store имеет достаточный запас
• • •

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

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

Глава Фокус Связь с текущей главой
I. Среда формирует архитектуру Почему EDA и как это меняет организации Детерминированность — это контракт между командами
II. Анатомия EDA Топологии, брокеры, идемпотентность Timestamp в Kafka — основа ingestion time
III. Анатомия идеального события Контракты и Schema Registry Upcasting при reprocessing — эволюция схем в действии
IV. Data Liberation Legacy-интеграция и событизация Reprocessing — инструмент для переигрывания legacy-данных
V. Механика потоковой обработки State, windows, EOS Windowing в этой главе — углубление раздела V
VI. Время в потоках Event time, watermarks, late events Детерминизм — фундамент надёжной EDA

Чеклист по настройке grace period

  • Distribution of delay измерен (95-й, 99-й перцентиль)
  • Критичность данных определена (real-time vs reporting)
  • SLA на latency зафиксирован
  • Cost of late data оценён (audit, fallback)
  • Fallback-стратегия определена (side output, drop, replay)
  • Метрика late-events-rate добавлена в мониторинг
  • Idle sources настроены (withIdleness во Flink)
  • Watermark alignment настроен, если источники разной скорости

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

  1. Event time — единственное детерминированное время. Processing time зависит от инфраструктуры и непредсказуем.
  2. Timestamp assigner определяет, откуда система берёт event time — из payload или из metadata Kafka.
  3. Планировщик событий переупорядочивает out-of-order события по event time. Это штатная работа, не ошибка.
  4. Водяные знаки (Flink) и grace period (Kafka Streams) — механизмы определения границы «прошлое прошло».
  5. Watermark propagation — минимум по всем входам. Watermark alignment — механизм «притормаживания» для отстающих источников.
  6. Idle sources могут заморозить watermark. Flink решает через withIdleness, Kafka Streams — через wall-clock fallback.
  7. Late events — три стратегии: drop, grace period (корректирующий результат), side output (audit/fallback).
  8. Session windows сливаются при gap < timeout. Это основа для анализа пользовательского поведения.
  9. Flink vs Kafka Streams — две модели времени. Flink: event-time-first, watermarks, alignment. Kafka Streams: wall-clock + grace.
  10. Reprocessing — суперсила неизменяемых журналов, но требует очистки state store, нового output topic, upcasting схем и отключения побочных эффектов.
Финальный тезис

Детерминизм в потоковой обработке — это не про идеальные условия. Это про создание архитектуры, которая ожидает сбоев, задержек и пересортицы. В событийно-управляемом мире время — это не просто часы на стене. Это данные. И тот, кто умеет с ними обращаться, получает абсолютную власть над историей.

Глава VI цикла «Событийно-управляемая архитектура». Источники: Apache Flink, Confluent, Conduktor, RisingWave, Software Patterns Lexicon, BigDataSchool, Habr, практические кейсы production-систем.

FinCodeCow · Chapter VI

Time in Streams

Deterministic processing and fighting chaos: watermarks, windows, late events, and reprocessing

In an ideal world, events arrive at the broker and leave it strictly in the order they were created. Clocks on all servers are synchronized to the nanosecond, the network never breaks, and consumers process data instantly. But we live in reality. Network packets get stuck, servers restart, and event producers temporarily lose connection to the broker.

How do we guarantee that a microservice processing financial transactions or click analytics produces the same result whether it processes data in real time or "digests" yesterday's backlog? The answer lies in the concept of deterministic stream processing.

This is the sixth chapter of the event-driven architecture series. We've covered why EDA is needed, how it works under the hood, how to design contracts, how to connect legacy, and how stream processing works. Now — the hardest part: time.

• • •

IThe Illusion of Time: Event vs Processing

The main trap when moving from batch to stream processing is confusing two types of time. To build a reliable system, we must strictly distinguish:

Event Time

The moment the fact actually happened in the real world. When the user clicked "Pay" or the IoT sensor recorded the temperature. Stored in the event payload or in Kafka record metadata.

Processing Time

The moment the microservice received and started processing the event. Depends on network delays, queues, CPU load, and garbage collector. Unstable and non-deterministic.

Ingestion Time

The moment the event entered the broker (Kafka timestamp). An intermediate option: deterministic within one broker, but doesn't reflect the real time of the event.

Three Times — Three Roles

Event time — for business logic. Ingestion time — for broker sorting. Processing time — for monitoring metrics.

Property Event Time Processing Time Ingestion Time
Determinism Yes No Within one broker
Fault tolerance Yes No Partial
Business logic accuracy Maximum Low Medium
Implementation complexity High Low Medium
Classic Example

A bank account. At 10:00 the client deposits $100 (event time). At 10:05 they withdraw $150. If, due to a network failure, the withdrawal event reaches the system first, using processing time leads to an erroneous overdraft fee. Only event time saves the business logic.

• • •

IITimestamp Assigner: Where the System Gets Event Time

Before going further, we need to answer a fundamental question: where does the system learn event time? An event is just bytes. Time must be extracted from somewhere.

Kafka Streams

By default, Kafka Streams uses the Kafka record timestamp — what the producer put in the metadata. This can be the send time (create time) or the broker receive time (log append time), depending on the topic setting message.timestamp.type.

If the Kafka timestamp doesn't fit (e.g., the event was replicated from another system and the label doesn't reflect real time), you can set your own TimestampExtractor:

public class OrderTimestampExtractor implements TimestampExtractor {
    @Override
    public long extract(ConsumerRecord<Object, Object> record, long partitionTime) {
        OrderEvent order = (OrderEvent) record.value();
        return order.getOccurredAt().toEpochMilli();
    }
}

// In configuration:
props.put(StreamsConfig.DEFAULT_TIMESTAMP_EXTRACTOR_CLASS_CONFIG,
          OrderTimestampExtractor.class);

Important: if the payload doesn't contain a valid timestamp, the producer may put a negative value. Kafka Streams will throw a StreamsException. For graceful handling, use WallclockTimestampExtractor (fallback to current time) or FailOnInvalidTimestamp (strict validation).

Apache Flink

In Flink, event time is extracted via a TimestampAssigner set on the source:

DataStream<OrderEvent> orders = env
    .addSource(new KafkaSource<>(...))
    .assignTimestampsAndWatermarks(
        WatermarkStrategy.<OrderEvent>forBoundedOutOfOrderness(
                Duration.ofMinutes(2))
            .withTimestampAssigner((event, ts) ->
                event.getOccurredAt().toEpochMilli()));

The key difference from Kafka Streams: in Flink, timestamp assigner and watermark strategy are set explicitly. This requires more code but gives full control over the time model.

Practical Rule

If the payload already contains event time — use it via TimestampExtractor/TimestampAssigner. If not — use ingestion time (Kafka timestamp), but remember: it doesn't reflect the real time of the event.

• • •

IIIEvent Scheduler: The Orchestra Conductor

A microservice often consumes data from multiple partitions and different streams simultaneously. If we process on a "first come, first served" basis, the result depends on network delay, not business logic.

The Event Scheduler buffers incoming records and always passes to business logic the event with the smallest timestamp. This guarantees that even if events got shuffled at the network level, the microservice restores their historical justice.

Cost: buffering requires memory. The larger the window and the more partitions, the larger the buffer. For session windows with a 30-minute timeout and 100 partitions, the buffer can take gigabytes.

Important Limitation

The scheduler sorts events only within one partition. There is no global order between partitions. If you need to gather all events of one user — use repartitioning (see Chapter V).

• • •

IVWatermarks: How to Know the Past Has Passed

If we buffer events for sorting, an eternal question arises: how long to wait for latecomers? We can't wait forever — memory is finite, and the business needs results.

A Watermark is a monotonically increasing timestamp that declares: "All events with time T and earlier have definitely arrived." If after the watermark an event with time T−1 arrives, the system understands: this is a Late Event.

The watermark is computed from observed event timestamps. The generation strategy determines how aggressively the system "moves" the boundary.

Periodic Watermark

The watermark is generated periodically (e.g., every 200 ms), based on the maximum event time observed during the period, minus the allowed delay:

watermark = max(eventTime) - allowedLateness

For example, if the maximum observed event time is 10:05 and allowed lateness is 2 minutes, watermark = 10:03. All windows up to 10:03 can be closed.

Punctuated Watermark

The watermark is generated on every special event (e.g., end-of-session marker). Rarely used, suitable for streams with irregular intervals.

Example in Flink

WatermarkStrategy.<OrderEvent>forBoundedOutOfOrderness(
        Duration.ofSeconds(30))
    .withTimestampAssigner((event, ts) -> event.getOccurredAt().toEpochMilli())
    .withIdleness(Duration.ofMinutes(5));

Here forBoundedOutOfOrderness means: "we expect events to be late by no more than 30 seconds."

• • •

VWatermark Propagation and Alignment

The watermark moves through the topology: each operator receives watermarks from all input streams and takes the minimum.

Source A: watermark = 10:05
Source B: watermark = 10:02  →  Operator: watermark = min(10:05, 10:02) = 10:02
Source C: watermark = 10:07

This explains why one slow or silent stream blocks the entire topology: if one source doesn't send events, its watermark doesn't move, and the overall minimum freezes.

Watermark Alignment — The "Slow Down" Mechanism

In large topologies, different sources may process data at different speeds. If one source lags by hours while others lag by seconds, the watermark minimum freezes, and windows don't close.

Flink provides watermark alignment: operators can "report" to upstream that they are lagging, and upstream operators slow down their watermarks to let the laggard catch up.

WatermarkStrategy.<OrderEvent>forBoundedOutOfOrderness(
        Duration.ofSeconds(30))
    .withWatermarkAlignment(
        "alignment-group-1",
        Duration.ofMinutes(5),   // maximum divergence
        Duration.ofSeconds(30)); // check interval

This is a more advanced mechanism than withIdleness: it doesn't exclude the source but temporarily "holds" the others to preserve consistency.

When to Use

withIdleness — when the source can go completely silent (night traffic). Watermark alignment — when sources run at different speeds (e.g., one partition reads faster than another).

• • •

VIIdle Sources Problem

If one partition goes silent (e.g., night traffic in one region stopped), its watermark stops moving. Since the operator takes the minimum across all inputs, the overall watermark freezes, and windows don't close.

Solution in Flink: withIdleness(Duration.ofMinutes(5)) — if the source receives no events for 5 minutes, it is excluded from watermark calculation. Windows continue to close based on the remaining sources.

Kafka Streams doesn't have this problem explicitly — windows close by wall-clock time, not by watermark. But there's another issue: if a partition goes silent, the window won't receive new events and will be closed when the grace period expires.

• • •

VIIWindows: Slicing Time into Pieces

Business logic rarely works with an abstract "stream." It needs aggregates: "how many clicks in the last hour," "what is the user's session." For this, windowing functions are used.

Window Type Overlap Grid-bound Events in Multiple Windows Where Used
Tumbling No Yes 1 Periodic reports
Hopping Yes Yes N Rolling metrics
Sliding Yes No (event-bound) N JOIN, correlation
Session Dynamic No Depends User behavior

Tumbling — fixed, non-overlapping intervals. Each event lands in exactly one window. Ideal for periodic reports: "revenue per hour."

Hopping — fixed size, but step smaller than size. The window "rolls" with a given interval. One event can land in several windows. Used for rolling metrics: "5-minute moving average, updated every minute."

Sliding — window bound not to a fixed grid but to event timestamps. Two events aggregate if their timestamps are within the window size. In Kafka Streams, sliding windows are used only for JOIN operations. Ideal for correlation: "two failed logins within 30 seconds."

Example in Kafka Streams

TimeWindows windows = TimeWindows
    .ofSizeAndGrace(Duration.ofMinutes(5), Duration.ofMinutes(1));

stream.groupByKey()
    .windowedBy(windows)
    .count();

Here the window is 5 minutes, grace period — 1 minute: late events within 1 minute after the window closes will be accepted and produce a corrective result.

• • •

VIIISession Windows and the Merge Mechanism

Session Windows are dynamic windows not bound to a clock. The window is open while the user is active. A pause (idle timeout, e.g., 30 minutes) closes the window.

The key feature of session windows is merge. If new activity "reaches" the previous one (gap < timeout), two windows combine into one.

Session 1: [10:00 — 10:05]
Session 2: [10:07 — 10:15]
Gap = 2 minutes < timeout (30 minutes)
→ Merged session: [10:00 — 10:15]

This is the foundation for user behavior analysis: clicks, views, purchases within one session can be "glued" into a single event.

In Kafka Streams, session windows are used like this:

stream.groupByKey()
    .windowedBy(SessionWindows.ofInactivityGapWithNoGrace(
        Duration.ofMinutes(30)))
    .count();
Cost of Session Windows

Session windows hold state as long as the session is active. For a user who doesn't close the browser for a week, the session will grow. Set a maximum session size or use a grace period for forced closure.

• • •

IXOut-of-order vs Late: Two Different Problems

Not all "wrong" events are the same.

Out-of-order

Events arrived out of event-time order but before the watermark. For example, event 10:03 arrived before event 10:01, but the watermark is still 09:55. The scheduler will reorder them — this is normal operation.

Late

The event arrived after the watermark. The window may already be closed. This requires a strategy (see next section).

The distinction is critical: out-of-order is normal, late is exceptional. The system must be designed so out-of-order events are handled transparently and late — consciously.

• • •

XThree Strategies for Late Events

Sooner or later the watermark moves forward, and an event arrives that is "late." What to do with it is not only a technical but also a business question.

Drop

The window is closed, the result is out. The late event is ignored. Suitable for telemetry, where freshness matters more than history.

Grace Period / Allowed Lateness

The window is closed but stays "ajar" for N minutes. If a late event arrives during this period, the aggregate is recomputed and a corrective result is emitted.

Difference between frameworks:

  • Kafka Streams: grace period — a single buffer until final closure.
  • Flink: allowed lateness — the window can reopen multiple times until allowed lateness is exhausted.

Side Output

The late event is not silently dropped but routed to a separate stream (side output). This stream can be analyzed, reprocessed, or used for fallback logic.

OutputTag<OrderEvent> lateOutputTag = new OutputTag<>("late-orders"){};

SingleOutputStreamOperator<Result> result = stream
    .keyBy(OrderEvent::getCustomerId)
    .window(TumblingEventTimeWindows.of(Time.minutes(5)))
    .allowedLateness(Time.minutes(1))
    .sideOutputLateData(lateOutputTag)
    .aggregate(new OrderAggregator());

DataStream<OrderEvent> lateOrders = result.getSideOutput(lateOutputTag);
Strategy Kafka Streams Flink When to Choose
Drop Default Default Telemetry, clicks, non-critical data
Grace / Allowed lateness TimeWindows.grace(...) .allowedLateness(...) Finance, billing, inventory
Side output Manual via branch Built-in Audit of late data, fallback logic
Wait (infinite) Not supported Not supported Not recommended — state growth
• • •

XIHow to Choose Grace Period: Latency vs Completeness

Grace period is a trade-off between result latency and data completeness. How to choose?

  1. Analyze the distribution of delay. If 95% of events arrive within 30 seconds, a 1-minute grace period covers 95% of cases. A 5-minute grace period covers 99.9% but adds 5 minutes of latency.
  2. Distinguish criticality. For real-time dashboards (clicks, traffic) — grace 30 seconds, completeness 95%. For billing — grace 10 minutes, completeness 99.99%. For daily financial reporting — grace 24 hours, completeness 100%.
  3. Consider the cost of reprocessing. If a late event is dropped but the data matters — you can always reprocess. Grace period = buffer between "fast enough" and "accurate enough."
  4. Measure. Monitoring metric: late-events-rate — the share of events arriving after the watermark. If it grows — the grace period needs to increase.
Rule by Criticality

Real-time dashboards: grace 30 seconds. Billing and inventory: grace 10 minutes. Financial reporting: grace 24 hours.

• • •

XIIFlink vs Kafka Streams: Two Time Models

Frameworks fundamentally diverge in their approach to time, and this affects the choice for the task.

Criterion Flink Kafka Streams
Time model Event-time-first, watermarks Wall-clock + event time via retention
Watermarks Built-in, configurable strategies None in the classical sense
Late events Allowed lateness + side outputs Grace period, drop after closure
Idle sources withIdleness() No explicit problem (wall-clock)
Watermark alignment Yes No
Timestamp assigner Explicit Via TimestampExtractor
Out-of-order Scheduler reorders Limited (within partition)
Event-time JOIN Yes, via watermark alignment Yes, via windowed join
Complexity Higher (watermark setup) Lower (wall-clock fallback)

Flink — when precise event-time processing with watermarks, complex analytics, multi-source JOIN, ML pipelines are needed. Kafka Streams — when input and output are in Kafka, deployment simplicity is needed, and a wall-clock fallback is acceptable.

• • •

XIIIThe Superpower of EDM: Reprocessing

Immutable event logs give an amazing capability. If a bug is found in business logic or a new schema is released, you can simply rewind the offset to the beginning and re-run the data through the microservice. Thanks to deterministic processing and the event scheduler, reprocessing historical data produces exactly the same order and results as if processing had been in real time.

But reprocessing is not "press a button." You need to consider:

  1. Clear the state store. Before reprocessing, the local RocksDB must be reset, otherwise aggregates will be incorrectly applied on top of old state.
  2. Output topic. Either clear the output topic or write to a new one. Writing to the same topic over old data creates duplicates.
  3. Schema evolution. Events from a year ago may be written with an old schema. The new code version may not deserialize them. Solution — upcasting: a transformer that reads the old version of the event and converts it to the new one in memory.
  4. Side effects. If the topology includes sending emails, calling third-party APIs, or payment gateways, reprocessing leads to catastrophe: clients will receive duplicate emails from years past. Before reprocessing, always disable or stub out all non-idempotent external calls.

Upcasting Example

public OrderEvent upcast(OrderEventV1 old) {
    return new OrderEvent(
        old.getId(),
        old.getCustomerId(),
        old.getAmount(),
        null,                       // paymentMethod — new field
        old.getOccurredAt()
    );
}

Estimating Reprocessing Time

Reprocessing 1 TB of data through Kafka Streams at 50k msg/sec ≈ 30–60 minutes. But this is very approximate. Key factors:

  • The bottleneck may be in the state store, not processing. RocksDB on a slow disk reads 10× slower than on NVMe.
  • Batch size — larger batches speed up throughput but increase memory pressure.
  • Parallelism — limited by the number of partitions. If there are 6 partitions and you launch 12 instances, 6 are idle.

Pre-Reprocessing Checklist

  • State store cleared
  • Output topic — new or cleared
  • All external calls (email, SMS, payment APIs) disabled or idempotent
  • Schema upcasters configured for old event versions
  • Offset reset to the beginning (or to the needed position)
  • Lag monitoring enabled — reprocessing creates a load spike
  • Time estimated: reprocessing 1 TB ≈ 30–60 minutes at 50k msg/sec
  • Disk for state store has sufficient capacity
• • •

XIVChecklists and Link to the Series

Link to Previous Chapters

Chapter Focus Link to This Chapter
I. The Medium Shapes the Architecture Why EDA and how it changes organizations Determinism is a contract between teams
II. The Anatomy of EDA Topologies, brokers, idempotency Kafka timestamp is the basis of ingestion time
III. The Anatomy of an Ideal Event Contracts and Schema Registry Upcasting during reprocessing — schema evolution in action
IV. Data Liberation Legacy integration and eventification Reprocessing is a tool for replaying legacy data
V. The Mechanics of Stream Processing State, windows, EOS Windowing here deepens Chapter V
VI. Time in Streams Event time, watermarks, late events Determinism is the foundation of reliable EDA

Grace Period Configuration Checklist

  • Distribution of delay measured (95th, 99th percentile)
  • Data criticality defined (real-time vs reporting)
  • Latency SLA fixed
  • Cost of late data assessed (audit, fallback)
  • Fallback strategy defined (side output, drop, replay)
  • Metric late-events-rate added to monitoring
  • Idle sources configured (withIdleness in Flink)
  • Watermark alignment configured if sources run at different speeds

Key Takeaways of the Chapter

  1. Event time is the only deterministic time. Processing time depends on infrastructure and is unpredictable.
  2. Timestamp assigner defines where the system gets event time — from payload or Kafka metadata.
  3. Event scheduler reorders out-of-order events by event time. This is normal operation, not an error.
  4. Watermarks (Flink) and grace period (Kafka Streams) — mechanisms for defining the "past has passed" boundary.
  5. Watermark propagation — minimum across all inputs. Watermark alignment — a "slow down" mechanism for lagging sources.
  6. Idle sources can freeze the watermark. Flink solves via withIdleness, Kafka Streams — via wall-clock fallback.
  7. Late events — three strategies: drop, grace period (corrective result), side output (audit/fallback).
  8. Session windows merge when gap < timeout. This is the foundation for user behavior analysis.
  9. Flink vs Kafka Streams — two time models. Flink: event-time-first, watermarks, alignment. Kafka Streams: wall-clock + grace.
  10. Reprocessing — the superpower of immutable logs, but requires clearing the state store, a new output topic, schema upcasting, and disabling side effects.
Final Thesis

Determinism in stream processing is not about ideal conditions. It is about building an architecture that expects failures, delays, and reshuffles. In the event-driven world, time is not just a clock on the wall. It is data. And whoever knows how to handle it gains absolute power over history.

Chapter VI of the "Event-Driven Architecture" series. Sources: Apache Flink, Confluent, Conduktor, RisingWave, Software Patterns Lexicon, BigDataSchool, Habr, production system case studies.