FinCodeCow · Глава X

Базовый шаблон «Производитель-Потребитель»

Искусство простых событийных микросервисов: Sidecar, Gating, гибридная архитектура

В мире событийно-управляемой архитектуры часто возникает иллюзия, что для создания микросервиса обязательно нужно подключать Apache Flink, Kafka Streams или Spark. Однако правда заключается в том, что базовый шаблон производителя и потребителя (Basic Producer/Consumer — BPC) покрывает потребности огромного количества бизнес-сценариев.

BPC-микросервис — это, по сути, элегантно простой цикл: получить событие → применить бизнес-логику → опубликовать результат. В нём нет встроенных механизмов для водяных знаков, сложного планирования событий или распределённых журналов изменений. Но именно эта простота делает его невероятно гибким инструментом в руках грамотного архитектора.

Это десятая глава цикла. Она опирается на главу V (state stores, rebalance), главу VII (внешние vs внутренние state stores) и главу IX (FaaS как форма простоты). Здесь мы разберём, где BPC выигрывает у «тяжёлых» фреймворков, а где — проигрывает.

• • •

IАнатомия BPC: минимализм как преимущество

В основе BPC лежит использование стандартных клиентских библиотек брокера событий. Весь бизнес-процесс ограниченного контекста инкапсулирован в одном приложении. Это снижает порог входа, позволяет использовать любой язык программирования и избавляет команду от необходимости поддерживать кластеры сложных потоковых фреймворков.

// Базовый цикл BPC-микросервиса
while (running) {
    ConsumerRecords<String, Event> records = consumer.poll(Duration.ofMillis(100));
    
    for (ConsumerRecord<String, Event> record : records) {
        // Применяем кастомную бизнес-логику
        OutputEvent output = processEvent(record.value());
        
        // Публикуем результат
        producer.send(new ProducerRecord<>("output-topic", output));
    }
    
    // Коммитим offset только ПОСЛЕ успешной обработки
    consumer.commitSync();
}

Обратите внимание: commitSync() вызывается после обработки всего батча. Это ключевое отличие BPC от наивной реализации «poll → commit → process», которая при сбое теряет данные.

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

Фиксация offsets после обработки — то же правило, что обсуждалось в главе II для Kafka consumer groups. BPC не отменяет это правило, а делает его ещё более критичным: здесь нет автоматического управления offset'ами, которое предоставляют Kafka Streams или Flink.

• • •

IIПаттерн «Коляска» (Sidecar) для Legacy-систем

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

Паттерн Sidecar позволяет запустить BPC-микросервис в одном контейнере с монолитом. «Коляска» потребляет события из брокера и тихо обновляет локальную БД монолита (или кэш), не требуя изменения ни строчки кода в самом наследии.

// Sidecar: читает поток цен, пишет в БД монолита
while (running) {
    ConsumerRecords<String, PriceEvent> records = consumer.poll(...);
    
    try (Connection conn = legacyDb.getConnection()) {
        for (ConsumerRecord<String, PriceEvent> record : records) {
            PriceEvent event = record.value();
            
            // Upsert в таблицу монолита
            PreparedStatement stmt = conn.prepareStatement(
                "INSERT INTO prices (product_id, price) VALUES (?, ?) " +
                "ON CONFLICT (product_id) DO UPDATE SET price = EXCLUDED.price"
            );
            stmt.setString(1, event.getProductId());
            stmt.setBigDecimal(2, event.getPrice());
            stmt.executeUpdate();
        }
        consumer.commitSync();
    } catch (SQLException e) {
        // Offset НЕ коммитим — повторим batch
        log.error("Failed to update legacy DB", e);
    }
}
Ключевой инсайт

Sidecar позволяет интегрировать legacy-систему в событийный мир без модификации её кода. Монолит продолжает работать как раньше, но теперь получает данные из потоков через промежуточный адаптер. Это прямая связь с главой IV (Data Liberation): Sidecar — один из инструментов освобождения данных.

• • •

IIIGating: состояние без строгого порядка

Многие бизнес-процессы требуют наличия нескольких фактов, но порядок их поступления не имеет значения. Классический пример — книгоиздание.

Чтобы отправить книгу в печать, нужны три события: «Текст готов», «Обложка утверждена», «Цена установлена». BPC-сервис материализует эти факты в локальную таблицу. Как только все три ячейки заполнены (независимо от того, в каком порядке они пришли), сервис генерирует событие «Готово к печати».

-- Таблица состояния Gating
CREATE TABLE book_readiness (
    book_id UUID PRIMARY KEY,
    text_ready BOOLEAN DEFAULT FALSE,
    cover_approved BOOLEAN DEFAULT FALSE,
    price_set BOOLEAN DEFAULT FALSE,
    ready_for_print BOOLEAN DEFAULT FALSE,
    updated_at TIMESTAMP DEFAULT NOW()
);

-- Обработка события
UPDATE book_readiness
SET text_ready = TRUE
WHERE book_id = :bookId;

-- Проверка готовности
UPDATE book_readiness
SET ready_for_print = TRUE
WHERE book_id = :bookId
  AND text_ready = TRUE
  AND cover_approved = TRUE
  AND price_set = TRUE
  AND ready_for_print = FALSE
RETURNING book_id;
-- Если вернулась строка — публикуем событие ReadyForPrint

Обратите внимание на AND ready_for_print = FALSE: это защита от повторной публикации события при повторной обработке того же факта (идемпотентность).

Gating vs Watermarks

Gating не требует event time. Здесь важен факт наличия, а не временная метка. Это принципиально отличает Gating от оконных операций из главы VI. Если бизнес-процессу важен только набор фактов, а не их порядок — Gating проще и надёжнее.

• • •

IVКогда «тяжёлую» работу делает уровень данных

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

// BPC как маршрутизатор к внешнему ML-сервису
while (running) {
    ConsumerRecords<String, ImageEvent> records = consumer.poll(...);
    
    for (ConsumerRecord<String, ImageEvent> record : records) {
        ImageEvent event = record.value();
        
        // Отправляем в ML-сервис
        ClassificationResult result = mlService.classify(event.getImageUrl());
        
        // Сохраняем результат
        producer.send(new ProducerRecord<>(
            "image-classifications",
            new ClassificationEvent(event.getImageId(), result.getLabel())
        ));
    }
    consumer.commitSync();
}

Вся сложность инкапсулирована в инфраструктуре данных: ML-сервис, vector DB, геопространственный индекс. BPC здесь — тонкий слой оркестрации.

• • •

VНезависимое масштабирование обработки и данных

Объём событий часто зависит от времени суток (циклы сна и бодрствования пользователей), а вот размер состояния (например, количество уникальных пользователей) остаётся постоянным.

BPC позволяет элегантно решить эту проблему: ночью вы масштабируете количество инстансов обработки до нуля (экономя деньги), но внешнее хранилище состояний (например, DynamoDB или Redis) продолжает хранить профили всех пользователей. Утром BPC-инстансы просто просыпаются и продолжают читать состояния.

Внутреннее state (Kafka Streams)

Состояние живёт в RocksDB на локальном диске инстанса. При масштабировании до нуля состояние теряется и восстанавливается из changelog — долго и дорого.

Внешнее state (BPC)

Состояние живёт в DynamoDB / Redis / PostgreSQL. Инстансы stateless: можно масштабировать до нуля и обратно без потери состояния.

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

Это прямое следствие архитектурного выбора: BPC использует только внешние state stores. В отличие от Kafka Streams, где state store защищён changelog и может быть восстановлен, BPC не имеет внутреннего состояния вообще. Цена — network latency к внешним хранилищам. Выигрыш — абсолютная гибкость масштабирования.

• • •

VIГибридные приложения: аутсорсинг стрим-процессинга

Что делать, если вам нужно сделать сложный JOIN трёх потоков в реальном времени, но ваш основной язык программирования не имеет хорошей библиотеки для этого?

Вы используете гибридный паттерн. BPC-микросервис запускает внешнее задание в потоковом фреймворке (например, в KSQL или Flink), который выполняет «грязную» работу по объединению данных. Результат сбрасывается в промежуточный поток событий, который ваш BPC-микросервис забирает и использует для обслуживания REST API или загрузки в витрину данных.

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│  Raw Events │────>│  Flink Job   │────>│  Joined     │
│  (3 streams)│     │  (JOIN)      │     │  Stream     │
└─────────────┘     └──────────────┘     └──────┬──────┘
                                                 │
                                                 ▼
                                          ┌─────────────┐
                                          │ BPC Service │
                                          │ (serve API) │
                                          └─────────────┘

Flink решает узкую задачу, которая плохо ложится на BPC. BPC решает всё остальное: обслуживание API, запись в витрину, интеграцию с другими сервисами.

• • •

VIIИдемпотентность в BPC

BPC-микросервис работает в режиме at-least-once: если обработка batch упала до коммита offset, тот же batch будет обработан снова. Значит, шаги должны быть идемпотентны.

Три подхода

  1. Idempotency key. Использовать event_id или бизнес-ключ как idempotency key. Сервис хранит таблицу обработанных ID и пропускает дубликаты.
  2. Conditional writes. В PostgreSQL — INSERT ... ON CONFLICT DO UPDATE. В DynamoDB — attribute_not_exists(event_id).
  3. Естественная идемпотентность. Если бизнес-операция идемпотентна по природе (SET price = 100, а не price += 100) — дубликаты не страшны.
// Дедупликация через таблицу обработанных событий
Connection conn = db.getConnection();
conn.setAutoCommit(false);
try {
    // Проверка дубликата и бизнес-операция в одной транзакции
    PreparedStatement check = conn.prepareStatement(
        "INSERT INTO processed_events (event_id) VALUES (?) " +
        "ON CONFLICT DO NOTHING RETURNING event_id"
    );
    check.setString(1, event.getId());
    ResultSet rs = check.executeQuery();
    
    if (!rs.next()) {
        // Уже обработано — пропускаем
        conn.commit();
        continue;
    }
    
    // Бизнес-операция
    updateBusinessState(conn, event);
    conn.commit();
} catch (SQLException e) {
    conn.rollback();
    // Offset НЕ коммитим
}
Правило

Каждый BPC-шаг, который может быть выполнен повторно, должен быть идемпотентен. Дедупликация должна жить в той же транзакции, что и бизнес-операция — иначе возможен сбой между ними.

• • •

VIIIGraceful shutdown

Что происходит, когда BPC-контейнер получает SIGTERM при деплое? Если просто выключиться — broker будет ждать session.timeout.ms, прежде чем передаст partitions другому потребителю. Это создаёт искусственные задержки в обработке.

public class BpcService {
    private volatile boolean running = true;
    
    public void run() {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            running = false;
        }));
        
        try {
            while (running) {
                ConsumerRecords<String, Event> records = consumer.poll(...);
                processRecords(records);
                consumer.commitSync();
            }
        } finally {
            // Ключевое: закрыть consumer → отказаться от partitions
            consumer.close(Duration.ofSeconds(10));
            producer.flush();
            producer.close(Duration.ofSeconds(10));
            db.close();
        }
    }
}

Правило: при любых сомнениях всегда очищайте соединения перед завершением. Это ускоряет rebalance и снижает нагрузку на broker.

• • •

IXRebalance в BPC

BPC-микросервис на контейнере — это тоже consumer group. При деплое, при падении инстанса — rebalance. Все проблемы из главы V применимы и здесь.

Стратегия Эффект Когда использовать
Eager rebalance Все потребители останавливаются, партиции перераспределяются Default, но устаревший
Cooperative sticky assignor Только изменившиеся партиции перераспределяются BPC с несколькими инстансами
Static group membership Потребитель при рестарте получает те же partitions BPC с предсказуемой нагрузкой
// Настройка consumer для минимизации rebalance
props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
    "org.apache.kafka.clients.consumer.CooperativeStickyAssignor");
props.put(ConsumerConfig.GROUP_INSTANCE_ID_CONFIG,
    "bpc-instance-" + System.getenv("HOSTNAME"));
props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "30000");
props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, "10000");
• • •

XObservability BPC-микросервиса

BPC в production без мониторинга — чёрный ящик. Что обязательно нужно измерять:

Метрика Что показывает Порог алерта
consumer_lag Отставание от хвоста потока > 10000 или растёт за 15 мин
processing_time_p99 99-й перцентиль обработки события > SLA
error_rate Доля упавших обработок > 1% за 5 мин
dlq_rate Доля событий, ушедших в DLQ > 0 должно алертить
commit_latency Время на commit offsets > 1 сек
rebalance_count Число rebalance за час > 2 в час
Правило трёх алертов

Минимум три алерта: (1) consumer lag выше порога, (2) error rate выше 1%, (3) DLQ не пуста. Без них вы узнаете о проблеме только от пользователей.

• • •

XIBPC vs Kafka Streams vs FaaS: decision tree

Критерий BPC Kafka Streams FaaS
Состояние Внешнее (Redis, DB) Внутреннее (RocksDB) + changelog Внешнее (Redis, DB)
Event time / watermarks Нет Полная поддержка Нет
Окна Ручные Tumbling, hopping, sliding, session Ручные
Joins Через внешнее state Встроены (co-partitioned) Через внешнее state
Exactly-once Ручная идемпотентность exactly_once_v2 Ручная идемпотентность
Масштабирование Ручное (K8s) Автоматическое (partition-based) Автоматическое (provider-managed)
Стоимость при простое Нулевая (scale to zero) Есть (кластер работает) Нулевая (pay-per-invocation)
Сложность Низкая Средняя Низкая

Decision tree

Нужны watermarks / event time / windows?
    ├── Да → Kafka Streams или Flink
    └── Нет ↓

Нужны сложные joins нескольких потоков?
    ├── Да → Kafka Streams или Flink (или гибридный BPC + Flink)
    └── Нет ↓

Нужна детерминированная обработка с exactly-once?
    ├── Да → Kafka Streams (exactly_once_v2)
    └── Нет ↓

Нагрузка переменная, нужна экономия при простое?
    ├── Да → FaaS (глава IX)
    └── Нет ↓

BPC на контейнере + внешнее state
Правило 80/20

Прежде чем подключать Apache Flink или Kafka Streams и разворачивать полноценные кластеры, задайте себе вопрос: «Смогу ли я решить эту задачу с помощью простого цикла poll-process-produce и внешней базы данных?» В 80% случаев ответ будет «да».

• • •

XIIОграничения BPC: когда простота не спасает

Базовый шаблон не был бы универсальным, если бы не имел своих границ. Отказываясь от «тяжеловесных» и «легковесных» фреймворков, вы берёте на себя ответственность за реализацию сложных паттернов самостоятельно.

Главная ловушка

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

Кроме того, в BPC-паттерне внешние хранилища состояний используются чаще внутренних. Это означает, что сетевые задержки (latency) при чтении/записи в БД становятся вашим главным врагом производительности, и архитектуру нужно проектировать с учётом кэширования и пакетной обработки.

Когда BPC — плохой выбор

  • Streaming joins трёх и более потоков — Kafka Streams или Flink справятся лучше.
  • Скользящие окна с event time — нужны watermarks, которых в BPC нет.
  • Exactly-once с транзакциями — Kafka Streams предоставляет из коробки.
  • Очень высокая нагрузка (> 100k msg/s) — фреймворк с in-memory state эффективнее.
  • Interactive Queries — нет встроенной поддержки чтения state извне.
• • •

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

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

Глава Фокус Связь с текущей главой
II. Анатомия EDA Consumer groups, offsets, идемпотентность BPC использует те же принципы без фреймворка
IV. Data Liberation Legacy-интеграция, CDC, событизация Sidecar — один из инструментов освобождения данных
V. Механика потоковой обработки State stores, rebalance, cooperative sticky BPC применяет те же rebalance-стратегии
VII. Память микросервисов Внутренние vs внешние state stores BPC всегда использует внешние stores
IX. Бессерверная архитектура FaaS в EDA FaaS — тот же BPC, но managed
X. Базовый шаблон «Производитель-Потребитель» Простота как стратегия Когда BPC достаточно, а когда нужен фреймворк

Чеклист BPC-микросервиса

  • Offset коммитится ПОСЛЕ успешной обработки batch
  • Обработка batch в транзакции с дедупликацией (idempotency key + processed_events)
  • Graceful shutdown: SIGTERM → drain → close consumer → commit offsets
  • Cooperative sticky assignor в конфигурации consumer
  • Static group membership (group.instance.id) для предсказуемых деплоев
  • State вынесен во внешнее хранилище (Redis, PostgreSQL, DynamoDB)
  • Кэширование часто читаемых данных, чтобы не бить в БД на каждом событии
  • Batch processing для снижения числа round-trip к внешнему state
  • DLQ для событий, которые не удаётся обработать после N ретраев
  • Метрики: consumer lag, processing time p99, error rate, dlq_rate, commit latency, rebalance count
  • Три обязательных алерта: consumer lag, error rate, DLQ non-empty
  • Decision tree применён: не нужен ли Kafka Streams / Flink / FaaS
  • Documentation: какие сценарии покрыты, какие — нет

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

  1. BPC покрывает 80% бизнес-сценариев. Простой цикл poll → process → produce + внешнее state — этого достаточно для большинства задач.
  2. Sidecar — интеграция legacy-систем без модификации их кода.
  3. Gating — состояние без строгого порядка: достаточно факта наличия, а не времени.
  4. Тяжёлая работа — на уровень данных. BPC — тонкий слой оркестрации, ML и vector DB делают остальное.
  5. Масштабирование до нуля — главное преимущество внешнего state. Контейнеры stateless, state в Redis/DynamoDB.
  6. Гибридный BPC + Flink — когда нужен сложный join, но не хочется переписывать весь пайплайн.
  7. Идемпотентность обязательна. At-least-once — реальность, шаги должны быть идемпотентны.
  8. Graceful shutdown ускоряет rebalance. Consumer.close() перед завершением.
  9. Rebalance-стратегии те же, что в Kafka Streams: cooperative sticky, static membership.
  10. Observability: три обязательных алерта (lag, error rate, DLQ).
  11. Decision tree: BPC vs Kafka Streams vs FaaS — по потребности в watermarks, joins, EOS, переменной нагрузке.
Финальный тезис

Шаблон Basic Producer/Consumer — это рабочий инструмент, а не «упрощённая версия» для новичков. Он идеально подходит для интеграции с legacy-системами, для сценариев, где порядок событий не важен, и для случаев, когда основную вычислительную нагрузку берёт на себя уровень данных. Простота — высшая форма изощрённости.

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

FinCodeCow · Chapter X

Basic Producer/Consumer Pattern

The art of simple event-driven microservices: Sidecar, Gating, hybrid architecture

In the world of event-driven architecture, there is often an illusion that building a microservice requires Apache Flink, Kafka Streams, or Spark. But the truth is that the Basic Producer/Consumer pattern (BPC) covers a huge number of business scenarios.

A BPC microservice is essentially an elegantly simple loop: receive event → apply business logic → publish result. It has no built-in mechanisms for watermarks, complex event scheduling, or distributed changelogs. But it is precisely this simplicity that makes it an incredibly flexible tool in the hands of a skilled architect.

This is the tenth chapter of the series. It builds on Chapter V (state stores, rebalance), Chapter VII (external vs internal state stores), and Chapter IX (FaaS as a form of simplicity). Here we explore where BPC wins over "heavy" frameworks and where it loses.

• • •

IBPC Anatomy: Minimalism as Advantage

BPC is based on standard broker client libraries. The entire business process of a bounded context is encapsulated in a single application. This lowers the entry barrier, allows using any programming language, and frees the team from maintaining complex streaming framework clusters.

// Basic BPC microservice loop
while (running) {
    ConsumerRecords<String, Event> records = consumer.poll(Duration.ofMillis(100));
    
    for (ConsumerRecord<String, Event> record : records) {
        OutputEvent output = processEvent(record.value());
        producer.send(new ProducerRecord<>("output-topic", output));
    }
    
    // Commit offset only AFTER successful processing
    consumer.commitSync();
}
Link to Chapter II

Committing offsets after processing is the same rule discussed in Chapter II for Kafka consumer groups. BPC does not cancel this rule — it makes it even more critical.

• • •

IISidecar Pattern for Legacy Systems

Imagine an old monolith that cannot work directly with event streams, but critically needs data from them. The Sidecar pattern allows running a BPC microservice in the same container as the monolith. The "sidecar" consumes events from the broker and quietly updates the monolith's local DB (or cache), without requiring a single line of code change in the legacy itself.

// Sidecar: reads price stream, writes to monolith DB
while (running) {
    ConsumerRecords<String, PriceEvent> records = consumer.poll(...);
    
    try (Connection conn = legacyDb.getConnection()) {
        for (ConsumerRecord<String, PriceEvent> record : records) {
            PriceEvent event = record.value();
            
            PreparedStatement stmt = conn.prepareStatement(
                "INSERT INTO prices (product_id, price) VALUES (?, ?) " +
                "ON CONFLICT (product_id) DO UPDATE SET price = EXCLUDED.price"
            );
            stmt.setString(1, event.getProductId());
            stmt.setBigDecimal(2, event.getPrice());
            stmt.executeUpdate();
        }
        consumer.commitSync();
    } catch (SQLException e) {
        // Offset NOT committed — retry batch
    }
}
Key Insight

Sidecar allows integrating a legacy system into the event world without modifying its code. This is a direct link to Chapter IV (Data Liberation).

• • •

IIIGating: State Without Strict Order

Many business processes require the presence of several facts, but the order of their arrival does not matter. A classic example is book publishing.

To send a book to print, three events are needed: "Text ready," "Cover approved," "Price set." A BPC service materializes these facts into a local table. As soon as all three cells are filled (regardless of the order they arrived in), the service generates a "Ready for Print" event.

-- Gating state table
CREATE TABLE book_readiness (
    book_id UUID PRIMARY KEY,
    text_ready BOOLEAN DEFAULT FALSE,
    cover_approved BOOLEAN DEFAULT FALSE,
    price_set BOOLEAN DEFAULT FALSE,
    ready_for_print BOOLEAN DEFAULT FALSE
);

UPDATE book_readiness
SET ready_for_print = TRUE
WHERE book_id = :bookId
  AND text_ready = TRUE
  AND cover_approved = TRUE
  AND price_set = TRUE
  AND ready_for_print = FALSE
RETURNING book_id;
Gating vs Watermarks

Gating does not require event time. What matters is the presence of a fact, not the timestamp. This fundamentally differs from windowing in Chapter VI.

• • •

IVWhen the Data Layer Does the Heavy Work

Why write complex aggregation logic in microservice code if a specialized DB can do it? If your scenario includes geospatial search or ML-model classification, BPC acts as a "router": it takes the event from the stream, sends it to a vector DB or ML service, and stores the result.

• • •

VIndependent Scaling of Processing and Data

Event volume often depends on time of day, while state size (e.g., number of unique users) stays constant. BPC solves this elegantly: at night you scale processing instances down to zero (saving money), but the external state store (DynamoDB or Redis) continues to hold profiles. In the morning, BPC instances wake up and continue reading states.

Internal state (Kafka Streams)

State lives in RocksDB on local disk. Scaling to zero loses state; recovery from changelog is slow and costly.

External state (BPC)

State lives in DynamoDB / Redis / PostgreSQL. Instances are stateless: scale to zero and back without losing state.

• • •

VIHybrid Applications: Outsourcing Stream Processing

What if you need a complex JOIN of three streams in real time, but your primary language lacks a good library for it? Use the hybrid pattern. A BPC microservice launches an external job in a streaming framework (KSQL, Flink), which does the "dirty" join work. The result is flushed to an intermediate event stream, which your BPC microservice consumes and uses for REST API or data mart.

• • •

VIIIdempotency in BPC

  1. Idempotency key. Use event_id as key. Service keeps a table of processed IDs and skips duplicates.
  2. Conditional writes. In PostgreSQL — INSERT ... ON CONFLICT DO UPDATE. In DynamoDB — attribute_not_exists(event_id).
  3. Natural idempotency. If the operation is idempotent (SET price = 100, not price += 100) — duplicates are harmless.
• • •

VIIIGraceful Shutdown

What happens when a BPC container receives SIGTERM on deploy? If it just shuts down, the broker waits for session.timeout.ms before reassigning partitions. This creates artificial delays.

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    running = false;
}));

try {
    while (running) {
        ConsumerRecords<String, Event> records = consumer.poll(...);
        processRecords(records);
        consumer.commitSync();
    }
} finally {
    consumer.close(Duration.ofSeconds(10));
    producer.flush();
    producer.close(Duration.ofSeconds(10));
}
• • •

IXRebalance in BPC

Strategy Effect When to Use
Eager rebalance All consumers stop, partitions reassigned Default but outdated
Cooperative sticky assignor Only changed partitions reassigned BPC with multiple instances
Static group membership Consumer gets same partitions on restart BPC with predictable load
• • •

XObservability of a BPC Microservice

Metric What It Shows Alert Threshold
consumer_lag Lag from stream tail > 10000 or growing in 15 min
processing_time_p99 99th percentile processing time > SLA
error_rate Failed processing share > 1% in 5 min
dlq_rate Events sent to DLQ > 0 must alert
rebalance_count Rebalances per hour > 2 per hour
• • •

XIBPC vs Kafka Streams vs FaaS: Decision Tree

Criterion BPC Kafka Streams FaaS
State External (Redis, DB) Internal (RocksDB) + changelog External (Redis, DB)
Event time / watermarks No Full support No
Windowing Manual Tumbling, hopping, sliding, session Manual
Joins Via external state Built-in (co-partitioned) Via external state
Exactly-once Manual idempotency exactly_once_v2 Manual idempotency
Cost at idle Zero (scale to zero) Exists (cluster runs) Zero (pay-per-invocation)
Need watermarks / event time / windows?
    ├── Yes → Kafka Streams or Flink
    └── No ↓

Need complex joins of several streams?
    ├── Yes → Kafka Streams or Flink (or hybrid BPC + Flink)
    └── No ↓

Need deterministic processing with exactly-once?
    ├── Yes → Kafka Streams (exactly_once_v2)
    └── No ↓

Variable load, need to save at idle?
    ├── Yes → FaaS (Chapter IX)
    └── No ↓

BPC on container + external state
• • •

XIIBPC Limits: When Simplicity Doesn't Save

Main Trap

If your business logic requires deterministic processing of events from different streams with time semantics (watermarks), or if you need complex sliding windows with precise time control, BPC cannot handle this out of the box.

  • Streaming joins of three or more streams — Kafka Streams or Flink are better.
  • Sliding windows with event time — you need watermarks, which BPC lacks.
  • Exactly-once with transactions — Kafka Streams provides out of the box.
  • Very high load (> 100k msg/s) — framework with in-memory state is more efficient.
  • Interactive Queries — no built-in support for reading state from outside.
• • •

XIIIChecklists and Link to the Series

Link to Previous Chapters

Chapter Focus Link to This Chapter
II. The Anatomy of EDA Consumer groups, offsets, idempotency BPC uses the same principles without a framework
IV. Data Liberation Legacy integration, CDC, eventification Sidecar is one of the data liberation tools
V. The Mechanics of Stream Processing State stores, rebalance, cooperative sticky BPC applies the same rebalance strategies
VII. The Memory of Microservices Internal vs external state stores BPC always uses external stores
IX. Serverless Architecture FaaS in EDA FaaS is the same BPC but managed
X. Basic Producer/Consumer Pattern Simplicity as strategy When BPC is enough and when a framework is needed

BPC Microservice Checklist

  • Offset commits AFTER successful batch processing
  • Batch processing in transaction with dedup (idempotency key + processed_events)
  • Graceful shutdown: SIGTERM → drain → close consumer → commit offsets
  • Cooperative sticky assignor in consumer config
  • Static group membership (group.instance.id) for predictable deploys
  • State externalized (Redis, PostgreSQL, DynamoDB)
  • Caching of frequently read data to avoid hitting DB on every event
  • Batch processing to reduce round-trips to external state
  • DLQ for events that fail after N retries
  • Metrics: consumer lag, processing time p99, error rate, dlq_rate, commit latency, rebalance count
  • Three mandatory alerts: consumer lag, error rate, DLQ non-empty
  • Decision tree applied: whether Kafka Streams / Flink / FaaS is needed
  • Documentation: which scenarios are covered, which are not
Final Thesis

The Basic Producer/Consumer pattern is a working tool, not a "simplified version" for beginners. It is ideal for legacy system integration, for scenarios where event order does not matter, and for cases where the data layer does the heavy lifting. Simplicity is the highest form of sophistication.

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