FinCodeCow · Глава XIII

Мост между мирами

Интеграция событийных микросервисов с синхронными API, UI и микрофронтендами

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

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

Это тринадцатая глава цикла. Она опирается на главу I (закон Конвея, bounded context), главу II (command/event distinction), главу III (strict schemas), главу V (Interactive Queries, state stores), главу VII (RocksDB, CQRS read models), главу VIII (human-in-the-loop, паттерн одобрения) и главу X (BPC — где начинается синхронность и где заканчивается).

• • •

ICommand vs Event vs Query: фундаментальное различие

Прежде чем говорить об интеграции, нужно чётко разграничить три типа сообщений, которые часто путают:

Command

«Сделай что-то». Императив. Имеет целевого получателя. Может быть отклонён. Примеры: PlaceOrder, CancelSubscription, ReserveInventory. Называется в повелительном наклонении.

Event

«Что-то произошло». Констатация факта. Прошедшее время. Не может быть отклонён (уже случился). Примеры: OrderPlaced, SubscriptionCancelled. Уже произошёл.

Query

«Дай мне данные». Синхронный запрос. Не изменяет состояние. Примеры: GetOrderStatus, ListCustomerOrders. Идёт через read model, не через event stream.

Правило

API принимает Command от клиента. Внутри система публикует Event в поток. Клиент читает через Query из read model.

Критическое различие

«Отправить событие OrderRequested» — это неправильная формулировка. OrderRequested — это команда, а не событие. Событие — это OrderPlaced, которое уже произошло. Разграничение важно для корректного дизайна API: команды могут быть отклонены, события — нет.

• • •

IIПриём внешних событий: на краю сети

Предоставлять внешним клиентам прямой доступ к брокеру событий (например, Apache Kafka) — плохая идея с точки зрения безопасности и архитектуры. Вместо этого используется синхронный API-шлюз, который принимает данные и конвертирует их в события.

Два типа внешних данных

  1. Автономные (аналитические) события. Клики, просмотры, телеметрия. Отправляются по принципу «выстрелил и забыл». Главное здесь — использовать строгие схемы (Avro или Protobuf) на стороне клиента, чтобы мусор не попал в ваш единый источник истины.
  2. Реактивные события. Ответы от сторонних API (например, статус платежа от банка). Данные из синхронного ответа должны быть немедленно конвертированы в событие и опубликованы в поток, чтобы бухгалтерия и система уведомлений могли на него отреагировать.

Пример: API endpoint, генерирующий событие

@PostMapping("/orders")
public ResponseEntity<OrderResponse> placeOrder(
        @RequestBody PlaceOrderRequest request,
        @RequestHeader("Idempotency-Key") String idempotencyKey) {
    
    // 1. Идемпотентность: проверяем, не обрабатывали ли уже
    if (idempotencyStore.contains(idempotencyKey)) {
        return ResponseEntity.ok(idempotencyStore.get(idempotencyKey));
    }
    
    // 2. Валидация через схему
    PlaceOrderCommand command = validate(request);
    
    // 3. Публикация команды в поток (не событие!)
    producer.send(new ProducerRecord<>("order-commands", command));
    
    // 4. Ответ клиенту: 202 Accepted + ID для отслеживания
    OrderResponse response = new OrderResponse(command.getOrderId(), "PENDING");
    idempotencyStore.put(idempotencyKey, response);
    
    return ResponseEntity.accepted().body(response);
}

Ключевое: API возвращает 202 Accepted, а не 200 OK. Клиент понимает, что команда принята, но результат ещё не готов.

• • •

IIIIdempotency на API: защита от повторов

Клиент может повторить запрос (retry, double-click, network timeout). Без идемпотентности это создаст дублирующие команды.

Три подхода

Idempotency-Key в заголовке

Клиент генерирует UUID и передаёт в заголовке. API хранит маппинг key → response. При повторе возвращает сохранённый ответ без повторной обработки. Stripe, Adyen используют этот подход.

Conditional write

Если команда уже обработана — вернуть тот же результат. Через attribute_not_exists(command_id) в DynamoDB или ON CONFLICT DO NOTHING в PostgreSQL.

Natural idempotency

Если команда идемпотентна по природе (Set price = 100, а не Increment price by 100) — дубликаты не страшны. Но это редкость.

Окно дедупликации

Хранить обработанные ключи N дней (например, 30). Достаточно, чтобы покрыть retry-окно клиента + запас на сетевые сбои.

Правило

Idempotency-Key должен генерироваться на клиенте, а не на API. Иначе retry сгенерирует новый ключ и создаст дубликат. Клиент генерирует UUID один раз и переиспользует его при retry.

• • •

IVSecurity и auth на границе

API-шлюз — это не только routing. Это точка контроля для всех внешних запросов.

Слой Что проверяет Инструменты
Аутентификация Кто ты? OAuth 2.0, JWT, mTLS, API keys
Авторизация Что тебе можно? RBAC, ABAC, OPA, scope-based
Rate limiting Сколько запросов? Token bucket, leaky bucket, sliding window
WAF Это безопасный запрос? ModSecurity, AWS WAF, Cloudflare
Input validation Данные корректны? JSON Schema, Protobuf validation

Авторизация на уровне команды

Недостаточно «пользователь авторизован». Нужно проверить: имеет ли он право выполнить конкретную команду?

// Проверка scope для команды
@PreAuthorize("hasAuthority('orders:write')")
@PostMapping("/orders")
public ResponseEntity<?> placeOrder(...) { ... }

// Проверка владения ресурсом
if (!command.getCustomerId().equals(principal.getCustomerId())) {
    throw new AccessDeniedException("Cannot place order for another customer");
}
• • •

VИнтеграция со сторонними API: осторожно, сетевой вызов!

Когда вашему микросервису нужно вызвать внешний REST API, вы вводите в детерминированный событийный процесс элемент хаоса. Внешний сервис может быть недоступен, а его ответ — измениться.

Опасность «грома» при переработке

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

Защита от «грома»

  1. Rate limiter. Ограничить количество исходящих запросов в секунду (token bucket).
  2. Circuit breaker. При N неудачах подряд — перестать слать запросы на cooldown период.
  3. Backoff с jitter. При retry — экспоненциальный backoff + случайный jitter.
  4. Кэширование ответов. Если данные не критичны, кэшировать на N минут.
  5. Bulkheading. Изолировать пулы соединений для разных внешних API.
  6. Timeout на каждый вызов. Не ждать бесконечно.

Внешний вызов — это side effect. Никогда не вызывайте внешние API во время reprocessing. Если событие требует внешнего вызова — используйте паттерн обработки с outbox: сначала сохранить команду, потом асинхронно выполнить вызов.

• • •

VICQRS и обслуживание запросов

Если данные живут в потоках событий, как нам быстро отдать их фронтенду по REST-запросу? Ответ — CQRS (Command Query Responsibility Segregation): команды пишут через event stream, запросы читают из read model.

Write model

Command → Event → Event store. Append-only. Единственный источник истины. Не оптимизирован для чтения. Не отвечает на Query.

Read model

Материализованная проекция. Оптимизирована под конкретные запросы. Может быть денормализована. Допускает eventual consistency.

Ключевое: read model — это проекция, а не реплика. Данные не просто копируются — они фильтруются, агрегируются и пересчитываются под нужды конкретного потребителя.

// Read model для запроса «заказы клиента»
CREATE TABLE customer_orders_read_model (
    customer_id UUID,
    order_id UUID,
    status VARCHAR(50),
    total_amount DECIMAL(10,2),
    created_at TIMESTAMP,
    PRIMARY KEY (customer_id, created_at)
);

// Обновляется асинхронно из потока OrderEvents
// Не является источником истины
// Оптимизирован под запрос "покажи все заказы клиента X"
• • •

VIIТри подхода к материализации состояния

1. Внутренние хранилища (Embedded State)

Состояние хранится локально на диске каждого экземпляра (RocksDB).

Плюсы: микросекундная задержка.

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

2. Внешние хранилища (External State)

События потребляются и записываются в общую БД (Redis, PostgreSQL, DynamoDB).

Плюсы: любой инстанс может ответить на любой запрос. Легко масштабировать.

Минусы: сетевая задержка и стоимость транзакций.

3. Разделённые сервисы

Один микросервис (фоновый обработчик) жадно потребляет события и обновляет внешнее хранилище. Другой микросервис (REST-шлюз) читает из этого хранилища.

Плюс: изолирует сбои обработки от доступности API.

Когда что выбирать

Embedded: очень низкая latency критична, ключей немного. External: простая масштабируемость, ключей много. Разделённые: критичные API должны работать независимо от обработки.

• • •

VIIIAPI Gateway и BFF

API Gateway — это не только routing. Это edge service, который объединяет данные из нескольких микросервисов для конкретного клиента.

Backend for Frontend (BFF)

Один API Gateway не подходит для всех. Мобильный клиент и веб-клиент имеют разные потребности:

  • Mobile BFF — минимум данных, агрессивное кэширование, медленные сети.
  • Web BFF — больше данных, меньшая задержка, шире payload.
  • Public API BFF — версионирование, документация, rate limits.
GraphQL Federation

Альтернатива REST — GraphQL federation, где клиент одним запросом собирает данные из нескольких микросервисов. Для событийного бэкенда часто удобнее: клиент запрашивает read models напрямую, без «BFF-склейки» вручную.

• • •

IXЗапросы как события: смена парадигмы UI

Самый радикальный подход — вообще отказаться от прямого изменения БД по REST-запросу. Когда пользователь нажимает «Оформить заказ», API не пишет в БД. Он создаёт команду PlaceOrder и отправляет её в поток.

Это гарантирует, что каждое действие пользователя становится частью неизменяемого журнала. Но как быть с интерфейсом? Пользователи не привыкли ждать.

Асинхронный UI

Фронтенд должен быть спроектирован так, чтобы ожидать асинхронного результата. Используйте спиннеры, скелетоны, оптимистичный UI или WebSocket/SSE для push-уведомлений, когда событие будет окончательно обработано и материализовано в состояние.

• • •

XEventual consistency на клиенте

Клиент отправил команду → она обрабатывается асинхронно → состояние материализуется. Это может занять секунды. Как UI должен работать с этим?

Стратегия Как работает Плюсы Минусы
Optimistic UI Показать результат сразу, откатить при ошибке Мгновенный отклик Откат смущает пользователя
Polling Периодически опрашивать статус команды Простота Нагрузка на API
Long polling Держать запрос открытым, пока событие не обработается Меньше нагрузки Сложнее в реализации
WebSocket / SSE Сервер push-ит обновление клиенту Реалтайм, минимальная нагрузка Сложность, state на клиенте

Пример: SSE для обновления статуса заказа

// Client
const eventSource = new EventSource(`/orders/${orderId}/stream`);

eventSource.addEventListener("status", (event) => {
    const data = JSON.parse(event.data);
    updateOrderStatus(data.status);
});

// Server (Spring WebFlux)
@GetMapping(value = "/orders/{orderId}/stream", produces = TEXT_EVENT_STREAM_VALUE)
public Flux<OrderStatus> streamOrderStatus(@PathVariable String orderId) {
    return kafkaStreams
        .stream("order-events")
        .filter((key, event) -> event.getOrderId().equals(orderId))
        .map(this::toOrderStatus);
}
• • •

XIЧеловеческий фактор: паттерн одобрения

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

В событийной архитектуре каждый шаг — это не обновление строки в БД, а публикация нового события (LayoutApprovedByEditor, AdRejectedByAdvertiser). Это позволяет:

  • «Отмотать» историю согласований — кто и когда заблокировал процесс.
  • Построить прозрачный аудит без сложных таблиц статусов.
  • Интегрировать human-in-the-loop с Saga (глава VIII) — через wait-for-signal в Temporal или Durable Functions.
• • •

XIIМикрофронтенды: зеркальное отражение бэкенда

Если ваш бэкенд разбит на независимые bounded contexts (микросервисы), то и монолитный фронтенд станет узким местом. Здесь на сцену выходят микрофронтенды — прямая реализация закона Конвея (глава I) на уровне UI.

Микрофронтенд — это независимый кусок UI, жёстко привязанный к своему бэкенд-контексту. На странице поиска отелей один микрофронтенд отвечает за карту и геолокацию (общаясь со своим сервисом поиска), другой — за блок отзывов (общаясь с сервисом ревью).

Плюсы и минусы композиции

Независимый деплой

Команда отзывов может выкатить новый виджет, не пересобирая весь сайт. Это прямое следствие Team Topologies (глава I).

Изоляция сбоев

Если сервис отзывов упал, блок карты и бронирования продолжит работать.

Проблемы стилизации

Сложно обеспечить единый дизайн, когда разные команды используют свои UI-библиотеки. Решение — Design System как shared component library.

Нагрузка на браузер

Оркестрация загрузки множества независимых модулей требует сложного роутинга на клиенте. Решение — Module Federation (Webpack 5).

Четыре подхода к композиции

Подход Где собирается Инструменты
Client-side composition В браузере Module Federation, single-spa, qiankun
Server-side composition На сервере Next.js, Nuxt, Piral
Edge-side composition На CDN Edge Workers, Cloudflare Pages
Web Components В браузере Custom Elements, Shadow DOM
• • •

XIIIContract testing: API и events не должны расходиться

Когда API генерирует команды и события, контракт между API и event schema должен быть протестирован. Иначе API изменится, а схема останется старой — или наоборот.

Три подхода к contract testing

  1. Consumer-driven contracts (Pact). Потребитель описывает ожидания, продюсер проверяет их в CI. Прямая связь с главой III (Schema Registry).
  2. Schema validation в CI. API Gateway валидирует команды против JSON Schema перед публикацией в Kafka. Schema Registry валидирует события против Avro/Protobuf.
  3. Integration tests через Testcontainers. Полный pipeline: API → Command → Event → Read model.
Правило

Любое изменение API-схемы или event-схемы должно проходить через тот же CI-пайплайн, что и код. Без автоматической проверки контрактов вы узнаете о breaking change только в production.

• • •

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

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

Глава Фокус Связь с текущей главой
I. Среда формирует архитектуру Закон Конвея, Team Topologies Микрофронтенды — зеркальное отражение bounded contexts на UI
II. Анатомия EDA Command vs Event, topic design API принимает команды, публикует события
III. Анатомия идеального события Strict schemas, Schema Registry Contract testing между API и events
V. Механика потоковой обработки State stores, Interactive Queries CQRS read models для API
VII. Память микросервисов RocksDB, changelog, CQRS Три подхода к материализации состояния для чтения
VIII. Механика бизнес-процессов Human-in-the-loop, Saga Паттерн одобрения через события
X. Базовый шаблон BPC BPC как минимализм API-шлюз как BPC-сервис на границе
XIII. Мост между мирами API и UI Гибридная архитектура: async внутри, sync на границах

Чеклист проектирования API-границы

  • API принимает Command, не публикует «событие»
  • Ответ — 202 Accepted с ID для отслеживания
  • Idempotency-Key в заголовке (генерируется клиентом)
  • Idempotency окно ≥ retry-окно клиента (обычно 24–72 часа)
  • Аутентификация: OAuth 2.0 / JWT / mTLS
  • Авторизация: RBAC / ABAC / scope-based
  • Rate limiting per user / per API key
  • WAF перед API Gateway
  • Input validation через JSON Schema / Protobuf
  • External API calls — только через outbox, не в reprocessing
  • Rate limiter + circuit breaker для внешних API
  • CQRS: команды через stream, запросы из read model
  • Read model — проекция, не реплика
  • API Gateway / BFF для композиции данных
  • Eventual consistency на клиенте: optimistic UI / SSE / WebSocket
  • Contract testing в CI (Pact / schema validation)
  • Микрофронтенды по bounded contexts, с Design System

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

  1. Command vs Event vs Query. API принимает команды, публикует события, отдаёт данные через query.
  2. 202 Accepted вместо 200 OK — клиент понимает асинхронность.
  3. Idempotency-Key защищает от повторов при retry.
  4. Security на границе — auth, авторизация, rate limit, WAF, validation.
  5. Внешние API — через outbox. Никогда не вызывать внешние API в reprocessing.
  6. CQRS — команды через stream, запросы из read model.
  7. Три подхода к материализации: embedded, external, разделённые сервисы.
  8. API Gateway / BFF — композиция данных для конкретного клиента.
  9. Запросы как события — радикальная смена парадигмы UI.
  10. Eventual consistency на клиенте — optimistic UI, polling, SSE, WebSocket.
  11. Паттерн одобрения — human-in-the-loop через события.
  12. Микрофронтенды — зеркальное отражение bounded contexts.
  13. Contract testing — API и events не должны расходиться.
Финальный тезис

Построение событийно-управляемых микросервисов не означает отказ от синхронных API. Наоборот, грамотная интеграция позволяет использовать лучшее из обоих миров. Используйте синхронные REST/gRPC на границах системы для взаимодействия с внешним миром и UI, но внутри периметра домена конвертируйте эти действия в события. Материализуйте состояния для быстрого чтения, проектируйте асинхронные интерфейсы и применяйте микрофронтенды, чтобы ваша архитектура оставалась гибкой, масштабируемой и готовой к любым бизнес-вызовам.

Глава XIII цикла «Событийно-управляемая архитектура». Источники: Apache Kafka, Confluent, Microservices.io, Pact, Temporal, AWS Architecture Center, практические кейсы production-систем, FinCodeCow experience.

FinCodeCow · Chapter XIII

A Bridge Between Worlds

Integrating event-driven microservices with synchronous APIs, UI, and micro-frontends

Event-Driven Architecture (EDA) works wonders inside the enterprise perimeter, decoupling microservices and providing unprecedented scalability. But at system boundaries, the world still speaks the language of synchrony. Users click buttons in mobile apps, IoT sensors send telemetry, and third-party payment gateways require immediate HTTP responses.

In this chapter, we will explore how to build a reliable bridge between the asynchronous internal world of events and the synchronous external world of request-response, without violating the principles of loose coupling and single source of truth.

This is the thirteenth chapter of the series. It builds on Chapter I (Conway's Law, bounded contexts), Chapter II (command/event distinction), Chapter III (strict schemas), Chapter V (Interactive Queries, state stores), Chapter VII (RocksDB, CQRS read models), Chapter VIII (human-in-the-loop, approval pattern), and Chapter X (BPC — where synchrony starts and ends).

• • •

ICommand vs Event vs Query: A Fundamental Distinction

Command

"Do something." Imperative. Has a target recipient. Can be rejected. Examples: PlaceOrder, CancelSubscription. Named in imperative mood.

Event

"Something happened." Statement of fact. Past tense. Cannot be rejected (already occurred). Examples: OrderPlaced, SubscriptionCancelled.

Query

"Give me data." Synchronous request. Does not change state. Examples: GetOrderStatus, ListCustomerOrders. Goes through read model, not event stream.

Rule

API accepts a Command from the client. Inside, the system publishes an Event to the stream. The client reads via Query from the read model.

Critical Distinction

"Send an event OrderRequested" is a wrong formulation. OrderRequested is a command, not an event. An event is OrderPlaced, which has already happened.

• • •

IIReceiving External Events: At the Edge

@PostMapping("/orders")
public ResponseEntity<OrderResponse> placeOrder(
        @RequestBody PlaceOrderRequest request,
        @RequestHeader("Idempotency-Key") String idempotencyKey) {
    
    if (idempotencyStore.contains(idempotencyKey)) {
        return ResponseEntity.ok(idempotencyStore.get(idempotencyKey));
    }
    
    PlaceOrderCommand command = validate(request);
    producer.send(new ProducerRecord<>("order-commands", command));
    
    OrderResponse response = new OrderResponse(command.getOrderId(), "PENDING");
    idempotencyStore.put(idempotencyKey, response);
    
    return ResponseEntity.accepted().body(response);
}
• • •

IIIIdempotency on the API

Idempotency-Key Header

Client generates UUID, passes in header. API stores key → response. On retry, returns stored response without re-processing.

Conditional Write

If command already processed — return same result. Via attribute_not_exists(command_id) in DynamoDB or ON CONFLICT DO NOTHING in PostgreSQL.

Natural Idempotency

If the command is idempotent by nature (Set price = 100, not Increment price by 100) — duplicates are harmless.

Deduplication Window

Store processed keys for N days (e.g., 30). Enough to cover client retry window + network failure reserve.

• • •

IVSecurity and Auth at the Edge

Layer What It Checks Tools
Authentication Who are you? OAuth 2.0, JWT, mTLS, API keys
Authorization What are you allowed? RBAC, ABAC, OPA, scope-based
Rate Limiting How many requests? Token bucket, leaky bucket, sliding window
WAF Is this a safe request? ModSecurity, AWS WAF, Cloudflare
Input Validation Is data valid? JSON Schema, Protobuf validation
• • •

VThird-Party API Integration

Thundering Herd Danger

If you made a logic error and decide to replay the event stream backwards, your microservice may start making millions of synchronous requests to an external API. This can lead to IP bans or crashing the partner's service.

  1. Rate limiter. Limit outgoing requests per second.
  2. Circuit breaker. After N failures — stop sending for cooldown.
  3. Backoff with jitter. Exponential backoff + random jitter on retry.
  4. Caching. Cache responses if data is not critical.
  5. Bulkheading. Isolate connection pools for different external APIs.
  6. Timeout on every call. Never wait indefinitely.
• • •

VICQRS and Serving Queries

Write Model

Command → Event → Event store. Append-only. Single source of truth. Not optimized for reading.

Read Model

Materialized projection. Optimized for specific queries. Can be denormalized. Eventual consistency allowed.

• • •

VIIThree Approaches to State Materialization

Embedded State

State lives locally on each instance (RocksDB). Microsecond latency, but complex routing.

External State

Events consumed and written to shared DB (Redis, PostgreSQL). Easy to scale, but network latency.

Separated Services

One service consumes events and updates external state. Another service reads and serves API. Isolates failures.

When to Choose

Embedded: very low latency critical. External: simple scalability. Separated: API must work independently of processing.

• • •

VIIIAPI Gateway and BFF

Backend for Frontend (BFF) — one API Gateway doesn't fit all. Mobile and web clients have different needs:

  • Mobile BFF — minimal data, aggressive caching, slow networks.
  • Web BFF — more data, lower latency, wider payload.
  • Public API BFF — versioning, documentation, rate limits.
• • •

IXQueries as Events: A Paradigm Shift for UI

The most radical approach is to abandon direct database writes via REST. When the user clicks "Place Order," the API does not write to the DB. It creates a PlaceOrder command and sends it to the stream.

Asynchronous UI

The frontend must be designed to expect an asynchronous result. Use spinners, skeletons, optimistic UI, or WebSocket/SSE for push notifications.

• • •

XEventual Consistency on the Client

Strategy How It Works Pros Cons
Optimistic UI Show result immediately, roll back on error Instant feedback Rollback confuses users
Polling Periodically query command status Simplicity API load
Long Polling Keep request open until processed Less load Harder to implement
WebSocket / SSE Server pushes updates to client Realtime, minimal load Complexity, client state
• • •

XIApproval Pattern: Human-in-the-Loop

Each step is not a row update, but a new event (LayoutApprovedByEditor, AdRejectedByAdvertiser). This allows:

  • Rewind approval history — who blocked the process and when.
  • Transparent audit without complex status tables.
  • Integrate human-in-the-loop with Saga (Chapter VIII).
• • •

XIIMicro-Frontends: Mirror of the Backend

If your backend is split into bounded contexts, monolithic frontend becomes a bottleneck. Micro-frontends — direct implementation of Conway's Law (Chapter I) at the UI level.

Approach Where Composed Tools
Client-side composition In browser Module Federation, single-spa, qiankun
Server-side composition On server Next.js, Nuxt, Piral
Edge-side composition On CDN Edge Workers, Cloudflare Pages
Web Components In browser Custom Elements, Shadow DOM
• • •

XIIIContract Testing

  1. Consumer-driven contracts (Pact). Consumer describes expectations, producer verifies in CI. Link to Chapter III (Schema Registry).
  2. Schema validation in CI. API Gateway validates commands against JSON Schema before publishing to Kafka.
  3. Integration tests via Testcontainers. Full pipeline: API → Command → Event → Read model.
• • •

XIVChecklists and Link to the Series

Chapter Focus Link to This Chapter
I. The Medium Shapes the Architecture Conway's Law, Team Topologies Micro-frontends mirror bounded contexts at UI level
II. The Anatomy of EDA Command vs Event, topic design API accepts commands, publishes events
III. The Anatomy of an Ideal Event Strict schemas, Schema Registry Contract testing between API and events
V. The Mechanics of Stream Processing State stores, Interactive Queries CQRS read models for API
VII. The Memory of Microservices RocksDB, changelog, CQRS Three approaches to state materialization for reading
VIII. The Mechanics of Business Processes Human-in-the-loop, Saga Approval pattern via events
X. Basic Producer/Consumer Pattern BPC as minimalism API gateway as BPC service at the edge
XIII. A Bridge Between Worlds API and UI Hybrid architecture: async inside, sync at edges

API Boundary Design Checklist

  • API accepts Command, doesn't publish an "event"
  • Response is 202 Accepted with ID for tracking
  • Idempotency-Key in header (generated by client)
  • Idempotency window ≥ client retry window (typically 24–72 hours)
  • Authentication: OAuth 2.0 / JWT / mTLS
  • Authorization: RBAC / ABAC / scope-based
  • Rate limiting per user / per API key
  • WAF in front of API Gateway
  • Input validation via JSON Schema / Protobuf
  • External API calls only via outbox, never in reprocessing
  • Rate limiter + circuit breaker for external APIs
  • CQRS: commands via stream, queries from read model
  • Read model is a projection, not a replica
  • API Gateway / BFF for data composition
  • Eventual consistency on client: optimistic UI / SSE / WebSocket
  • Contract testing in CI (Pact / schema validation)
  • Micro-frontends per bounded context, with Design System
Final Thesis

Building event-driven microservices doesn't mean abandoning synchronous APIs. On the contrary, thoughtful integration lets you use the best of both worlds. Use synchronous REST/gRPC at the system's edges to interact with the outside world and UI, but inside the domain perimeter, convert these actions into events. Materialize states for fast reads, design asynchronous interfaces, and apply micro-frontends to keep your architecture flexible, scalable, and ready for any business challenges.

Chapter XIII of the "Event-Driven Architecture" series. Sources: Apache Kafka, Confluent, Microservices.io, Pact, Temporal, AWS Architecture Center, production system case studies, FinCodeCow experience.