FinCodeCow · Глава IX

Бессерверная архитектура

Проектирование событийно-управляемых микросервисов на базе FaaS: от триггеров до rebalancing deadlock

Технология «Функции как сервис» (FaaS), часто ассоциируемая с «бессерверными» вычислениями, произвела революцию в том, как мы развёртываем код. В контексте событийно-управляемых микросервисов (EDM) FaaS выступает не просто как инструмент для обработки веб-хуков, а как мощный механизм для создания реактивных, эластичных и экономически эффективных компонентов.

По сути, FaaS-решение можно представить как базовую связку «производитель-потребитель», которая регулярно завершает свою работу и освобождает ресурсы. Но чтобы вписать этот паттерн в строгую архитектуру EDM, необходимо понимать специфику его жизненного цикла, управления состоянием и масштабирования.

Это девятая глава цикла. Она опирается на главу II (партиционирование, consumer groups), главу V (state stores, rebalance), главу VII (состояние, TTL) и главу VIII (оркестровка, Saga). Здесь мы разберём, как эти механизмы преломляются в мире FaaS.

• • •

IЗолотые правила проектирования FaaS-микросервисов

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

Строгий ограниченный контекст

Функции и их внутренние потоки должны строго принадлежать одному ограниченному контексту. Из-за высокой гранулярности FaaS легко потерять контроль над владением данными. Группируйте функции по бизнес-доменам и храните их код в соответствующих репозиториях.

Фиксация смещений ПОСЛЕ обработки

Это критически важное правило. Смещения (offsets) в брокере событий должны подтверждаться только после успешного завершения обработки пакета. Фиксация до обработки (как делают некоторые провайдеры по умолчанию) при сбоях функции приведёт к безвозвратной потере данных.

Эмпирическое правило: «Меньше — значит больше»

Искушение разбить логику на десятки микро-функций и переиспользовать их везде очень велико. Но это ведёт к фрагментации и усложнению отладки. Стремитесь к тому, чтобы ограниченный контекст реализовывался минимально необходимым количеством функций. Одной функцией управлять и тестировать несоизмеримо проще.

• • •

IIАнатомия запуска: триггеры и жизненный цикл

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

Основные типы триггеров

Event-stream listener

Функция запускается при появлении новых событий. Брокер или провайдер FaaS скрыто управляет потреблением и передаёт массив событий в код функции. Идеально для реактивной обработки.

Consumer Lag trigger

Внешний монитор отслеживает отставание группы потребителей. Если lag превышает порог, провайдер FaaS массово запускает экземпляры функций, которые сами создают клиентские соединения и «разгребают» завал. Отлично для пакетной обработки и сглаживания пиков.

Расписание (cron)

Запуск по таймеру — для периодических задач: агрегация, отчёты, cleanup.

Изменение ресурса

Событие от внешнего ресурса: появление файла в S3, изменение документа в DynamoDB, событие в EventBridge. Классический event-driven паттерн.

• • •

IIIХолодный и тёплый старт

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

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

Практические меры против cold start

  • Provisioned concurrency (AWS Lambda) — держать N экземпляров всегда тёплыми. Стоит денег, но устраняет cold start.
  • Минимальный размер кода — меньше кода, быстрее загрузка. Убирайте неиспользуемые зависимости.
  • Инициализация вне handler'а — соединения с БД, Kafka-клиенты создавать в init-блоке, а не в каждой инвокации.
  • Runtime с быстрым стартом — Go, Rust, Node.js стартуют быстрее, чем Java или .NET.
• • •

IVEvent-source mapping: как функция подключается к потоку

В AWS Lambda event-source mapping — это ключевой механизм, который связывает источник событий (SQS, Kinesis, DynamoDB Streams, MSK) с функцией. Понимание его настроек критично для production.

Параметр Назначение Рекомендация
BatchSize Сколько событий передавать в одной инвокации 10–100 для SQS, 100–1000 для Kinesis
MaximumBatchingWindowInSeconds Сколько ждать накопления batch 0–5 секунд для low-latency, больше для throughput
MaximumConcurrency Ограничение параллельных инвокаций Настраивается для защиты downstream
FunctionResponseTypes Partial batch response ReportBatchItemFailures для гранулярных сбоев
OnFailure Куда отправлять неудачные batch SQS DLQ или SNS topic для ручного разбора

Partial batch response

Без partial batch response, если один event в batch падает, весь batch переобрабатывается — включая уже успешные события. Это ведёт к дубликатам.

public class Handler implements RequestHandler<SQSEvent, SQSBatchResponse> {
    @Override
    public SQSBatchResponse handleRequest(SQSEvent event, Context context) {
        List<SQSBatchResponse.BatchItemFailure> failures = new ArrayList<>();
        for (SQSEvent.SQSMessage msg : event.getRecords()) {
            try {
                processMessage(msg);
            } catch (Exception e) {
                failures.add(new SQSBatchResponse.BatchItemFailure(msg.getMessageId()));
            }
        }
        return new SQSBatchResponse(failures);
    }
}

Только события из failures будут переобработаны. Успешные — зафиксированы и удалены из очереди.

Правило

Для потоковых источников (Kinesis, MSK) partial batch response особенно важен: без него один сбойный event блокирует всю партицию. С ним — только сбойные события уходят в retry, остальные фиксируются.

• • •

VПарадокс состояния в бессерверном мире

Главная философия FaaS — отсутствие состояния (stateless). Функция эфемерна: она запускается, выполняет работу и исчезает вместе со своей локальной памятью. Но как быть, если бизнес-логика требует агрегации или хранения данных?

В классическом EDM-подходе состояние выносится вовне. Функция явно подключается к внешним хранилищам (Redis, DynamoDB, CosmosDB) через сеть. Это снижает гибкость масштабирования, так как провайдеру FaaS приходится учитывать сетевые задержки и лимиты внешних БД, но зато позволяет масштабировать функции горизонтально без ограничений.

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

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

• • •

VIAzure Durable Functions: stateful FaaS

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

Три типа функций в Durable Functions

Orchestrator functions

Долгоживущие. Хранят состояние процесса, вызывают activity-функции, ждут результатов. Реализуют паттерн Saga с автоматическим сохранением состояния.

Activity functions

Stateless рабочие. Выполняют одну операцию (вызов API, запись в БД). Могут выполняться параллельно, orchestration — синхронизирует.

Entity functions

Stateful actors. Хранят состояние между вызовами, обрабатывают операции последовательно. Идеально для агрегатов — счётчиков, очередей, actor-моделей.

Client functions

Точка входа. Запускает orchestration, отправляет события, опрашивает статус.

Saga на Durable Functions

[FunctionName("OrderSaga")]
public static async Task<string> RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    try {
        await context.CallActivityAsync("ReserveInventory", order);
        await context.CallActivityAsync("ChargePayment", order);
        await context.CallActivityAsync("ShipOrder", order);
        return "completed";
    } catch (Exception) {
        await context.CallActivityAsync("ReleaseInventory", order);
        await context.CallActivityAsync("RefundPayment", order);
        throw;
    }
}

Ключевое: состояние orchestrator персистится автоматически. При сбое worker'а — другой worker подхватывает с того же шага. Это ровно тот механизм, который в Temporal реализуется через durable execution (глава VIII).

Human-in-the-loop

using (var timeoutCts = new CancellationTokenSource())
{
    var approvalTask = context.WaitForExternalEvent<bool>("Approval");
    var timeoutTask = context.CreateTimer(
        context.CurrentUtcDateTime.AddHours(24),
        timeoutCts.Token);

    var winner = await Task.WhenAny(approvalTask, timeoutTask);
    if (winner == approvalTask)
    {
        // Human approved
    }
    else
    {
        // Auto-compensate
    }
}
• • •

VIIОркестровка vs хореография в FaaS

Когда одна функция должна передать эстафету другой, возникает вопрос координации. Здесь мы возвращаемся к паттернам из главы VIII, но с поправкой на эфемерность FaaS.

Хореография (async fire-and-forget)

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

Опасность: если функция А вызывает функцию Б напрямую fire-and-forget, провайдер FaaS может запустить десятки экземпляров Б параллельно. Это гарантированно нарушит порядок обработки событий в партиции.

Оркестровка (sync invoke)

Функция-оркестратор последовательно вызывает функции А и Б, ожидая результата от каждой перед переходом к следующей.

Преимущество: строгий порядок. Каждое событие полностью проходит весь бизнес-процесс до того, как оркестратор возьмётся за следующее. Offset фиксируется только после полного успеха.

public int orchestrationFunction(Event[] events, Context context) {
    for (Event event : events) {
        // Строгий синхронный вызов
        Result resA = invokeFunctionA(event);
        Result resB = invokeFunctionB(event, resA);

        // Публикуем финальный результат
        producer.produce("Output-Stream", composeOutput(resA, resB));
    }
    // Сообщаем фреймворку, что можно зафиксировать смещения
    context.success();
    return 0;
}
• • •

VIIIИдемпотентность FaaS-функций

Функция может быть вызвана повторно: retry от провайдера, at-least-once доставка, partial batch failure. Как гарантировать, что повторный вызов не создаст дубликатов?

Три подхода к идемпотентности

  1. Idempotency key в заголовке. Для HTTP-триггеров — передавать Idempotency-Key в заголовке (Stripe, Adyen поддерживают нативно). Для SQS — использовать MessageId как ключ.
  2. Conditional writes. Для DynamoDB — attribute_not_exists(event_id) в condition expression. Для PostgreSQL — INSERT ... ON CONFLICT DO NOTHING.
  3. Дедупликация в state store. Хранить обработанные event_id в Redis с TTL (например, 24 часа). Перед обработкой — проверка, после обработки — запись.
// DynamoDB conditional write
PutItemRequest request = PutItemRequest.builder()
    .tableName("processed-events")
    .item(Map.of(
        "event_id", AttributeValue.builder().s(eventId).build(),
        "processed_at", AttributeValue.builder().n(String.valueOf(now)).build()
    ))
    .conditionExpression("attribute_not_exists(event_id)")
    .build();

try {
    dynamoDb.putItem(request);
    processEvent(event);  // только если condition passed
} catch (ConditionalCheckFailedException e) {
    // Уже обработано — пропустить
}
• • •

IXМасштабирование и ловушка «виртуального тупика»

FaaS предоставляет невероятные возможности для горизонтального масштабирования. Если порядок событий не важен (например, обработка очереди), провайдер может запустить тысячи экземпляров функции одновременно.

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

Опасность: Rebalancing Deadlock

Если вы используете агрессивные политики автомасштабирования на основе lag, функция будет постоянно запускаться и завершаться. Каждый запуск и остановка заставляют брокер переназначать разделы (rebalance). В экстремальных случаях группа потребителей попадает в «тупик»: функция тратит весь свой lifespan только на то, чтобы получить назначение разделов, и даже не успевает начать их обрабатывать.

Три способа избежать тупика

Петля гистерезиса

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

Статическое назначение

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

Управление batch

Тщательно настраивайте размер batch и окно ожидания. Если функция не успевает обработать batch за lifespan — она упадёт с таймаутом.

Cooperative sticky assignor

Используйте cooperative sticky assignor (глава V), который перераспределяет только изменившиеся партиции, а не останавливает всех потребителей.

• • •

XVPC и network latency

Если функция работает внутри VPC — она теряет доступ к managed-сервисам (S3, DynamoDB, SQS) без NAT Gateway или VPC Endpoint. Это добавляет latency и стоит денег.

Опция Latency Стоимость
NAT Gateway +2–5 ms Оплата за час + трафик
VPC Endpoint (PrivateLink) +1–2 ms Оплата за час + трафик
Lambda вне VPC Нативный Без доплат

Правило: если функция не требует доступа к ресурсам внутри VPC (RDS, ElastiCache), не помещайте её в VPC. Это упрощает архитектуру и снижает latency.

• • •

XICost model FaaS: когда это дешевле контейнеров

FaaS оплачивается за:

  • Количество вызовов — обычно $0.20 за 1M вызовов.
  • GB-seconds — память × время выполнения.
  • Cold starts — каждая инициализация считается как вызов + GB-seconds.
  • Network egress — трафик между регионами и в интернет.
Сценарий FaaS дешевле? Почему
Редкие триггеры Да Оплата только за вызовы, контейнер 24/7 — простаивал бы
Переменная нагрузка Да Auto-scaling без выделения пиковых ресурсов
Постоянная высокая нагрузка Нет Контейнеры с reserved capacity дешевле
High-throughput потоки Нет Kafka Streams / Flink на контейнерах эффективнее
Stateful агрегации Нет Network latency к внешним stores убивает производительность
• • •

XIITesting FaaS-функций

Тестирование FaaS — отдельная дисциплина. Функции эфемерны, зависят от триггеров и внешних сервисов.

LocalStack

Локальная эмуляция AWS-сервисов (SQS, S3, DynamoDB, Lambda). Testcontainers + LocalStack — для интеграционных тестов.

AWS SAM Local

Локальный запуск Lambda-функций с эмуляцией API Gateway и event sources. Быстрая итерация без деплоя.

Serverless Framework offline

Плагин для локального запуска HTTP-триггеров. Для не-AWS провайдеров — аналоги.

Unit-тесты с mock events

Функция принимает JSON — можно тестировать с mock-событиями без брокера. Быстро, но не проверяет интеграцию.

• • •

XIIIСравнение провайдеров FaaS

Провайдер Max execution Triggers Stateful
AWS Lambda 15 минут SQS, Kinesis, MSK, DynamoDB Streams, EventBridge, S3 Внешние stores (DynamoDB, ElastiCache)
Azure Functions 10 минут (default)
Unlimited для Durable
Event Hub, Service Bus, Event Grid, Blob Storage Durable Functions (orchestrator, entity)
GCP Cloud Functions 9 минут Pub/Sub, Firestore, Cloud Storage, Eventarc Внешние stores (Firestore, Memorystore)
Cloudflare Workers 30 секунд (CPU) HTTP, Queues, Durable Objects Durable Objects
Выбор провайдера

Если нужны долгие процессы (дни) и human-in-the-loop — Azure Durable Functions. Если нужна минимальная latency и edge — Cloudflare Workers. Если нужна зрелая экосистема и максимум интеграций — AWS Lambda. Если всё в GCP — Cloud Functions с Eventarc.

• • •

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

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

Глава Фокус Связь с текущей главой
II. Анатомия EDA Партиционирование, consumer groups FaaS consumer ограничен числом партиций
V. Механика потоковой обработки Rebalance, state stores Rebalancing deadlock — следствие rebalance-политик
VII. Память микросервисов State stores, TTL, дедупликация В FaaS state — только внешний, дедупликация через conditional writes
VIII. Механика бизнес-процессов Saga, оркестровка Durable Functions реализуют Saga с автоматическим state
IX. Бессерверная архитектура FaaS в EDA Когда FaaS, а когда Kafka Streams / Flink

Чеклист проектирования FaaS-микросервиса

  • Функция принадлежит одному ограниченному контексту
  • Offset фиксируется ПОСЛЕ успешной обработки
  • Partial batch response включён для потоковых источников
  • Failure destination (DLQ, SNS) настроен
  • Инициализация соединений вне handler'а (reuse тёплых экземпляров)
  • Идемпотентность через idempotency key / conditional write / дедуп-таблицу
  • State вынесен во внешнее хранилище (Redis, DynamoDB)
  • Для долгих процессов — Durable Functions или workflow engine
  • Ordering гарантирован через оркестровку, если порядок критичен
  • Автомасштабирование с гистерезисом, а не агрессивное
  • Static partition assignment там, где нагрузка предсказуема
  • Batch size согласован с lifespan функции
  • Функция вне VPC, если не нужен доступ к VPC-ресурсам
  • Стоимость оценена: FaaS vs контейнеры
  • LocalStack / SAM Local для интеграционных тестов

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

  1. FaaS идеален для дискретных событий — очереди, вебхуки, реактивное обогащение.
  2. State выносится вовне. FaaS stateless по философии; состояние — во внешних stores с network latency.
  3. Durable Functions (Azure) — stateful FaaS с автоматическим сохранением состояния. Реализуют Saga с human-in-the-loop.
  4. Event-source mapping — ключ к production. Batch size, batch window, partial batch response, DLQ.
  5. Идемпотентность — обязательна. Idempotency key, conditional writes, дедуп-таблица с TTL.
  6. Rebalancing deadlock — реальная ловушка. Гистерезис, static assignment, cooperative sticky assignor.
  7. Ordering в хореографии нарушается при параллельном запуске. Оркестровка — только способ сохранить порядок.
  8. VPC latency — плата за доступ к RDS/ElastiCache. Не помещайте функцию в VPC без необходимости.
  9. Cost model — FaaS дешевле при переменной нагрузке и редких триггерах, дороже при постоянной высокой нагрузке.
  10. Testing — LocalStack, SAM Local для интеграционных тестов. Mock events — для unit.
Финальный тезис

Используйте FaaS там, где события дискретны, а масштабирование должно происходить мгновенно в ответ на всплески нагрузки. Но всегда помните о «налоге» на перебалансирование, о network latency к внешним stores и строго соблюдайте дисциплину фиксации смещений. Для сложной детерминированной обработки нескольких потоков, exactly-once или локальных агрегаций — Kafka Streams или Flink остаются предсказуемее.

Глава IX цикла «Событийно-управляемая архитектура». Источники: AWS Lambda, Azure Durable Functions, GCP Cloud Functions, Cloudflare Workers, LocalStack, AWS SAM, практические кейсы production-систем.

FinCodeCow · Chapter IX

Serverless Architecture

Designing event-driven microservices on FaaS: from triggers to rebalancing deadlock

Function-as-a-Service (FaaS), often associated with "serverless" computing, has revolutionized how we deploy code. In the context of event-driven microservices (EDM), FaaS is not just a tool for handling webhooks but a powerful mechanism for creating reactive, elastic, and cost-effective components.

Essentially, a FaaS solution can be viewed as a producer-consumer pair that regularly completes its work and releases resources. But to fit this pattern into a strict EDM architecture, one must understand the specifics of its lifecycle, state management, and scaling.

This is the ninth chapter of the series. It builds on Chapter II (partitioning, consumer groups), Chapter V (state stores, rebalance), Chapter VII (state, TTL), and Chapter VIII (orchestration, Saga). Here we explore how these mechanisms translate into the FaaS world.

• • •

IGolden Rules of FaaS Microservice Design

Strict Bounded Context

Functions and their streams must strictly belong to one bounded context. Due to high granularity of FaaS, it is easy to lose control over data ownership. Group functions by business domain.

Commit Offsets AFTER Processing

Critical rule. Offsets must be confirmed only after successful batch processing. Committing before processing (as some providers do by default) causes irrecoverable data loss on failure.

Rule of Thumb: "Less Is More"

The temptation to split logic into dozens of micro-functions and reuse them everywhere is very high. But this leads to fragmentation and debugging complexity. Aim for the minimum necessary number of functions per bounded context.

• • •

IITrigger Anatomy and Lifecycle

Event-stream listener

The function triggers on new events. The broker or FaaS provider transparently manages consumption and passes an array of events to the function code.

Consumer Lag trigger

An external monitor tracks consumer group lag. If lag exceeds a threshold, the provider mass-launches function instances that create their own client connections and clear the backlog.

Schedule (cron)

Periodic trigger for aggregation, reports, cleanup.

Resource change

Event from an external resource: file in S3, document in DynamoDB, EventBridge event.

• • •

IIICold and Warm Start

On first launch or after long idle, the function goes through a cold start: the provider allocates a container, loads code, and establishes connections. This takes time and can cause delays.

However, most providers cache warm instances. If the function processes a stable stream, it periodically "sleeps" and "wakes," reusing existing connections.

Practical Measures Against Cold Start

  • Provisioned concurrency (AWS Lambda) — keep N instances always warm.
  • Minimal code size — less code, faster loading.
  • Init outside handler — DB connections, Kafka clients in init block, not per invocation.
  • Fast-start runtimes — Go, Rust, Node.js start faster than Java or .NET.
• • •

IVEvent-Source Mapping: How a Function Connects to a Stream

Parameter Purpose Recommendation
BatchSize How many events per invocation 10–100 for SQS, 100–1000 for Kinesis
MaximumBatchingWindowInSeconds How long to wait for batch accumulation 0–5 seconds for low-latency
MaximumConcurrency Limit on parallel invocations Configure to protect downstream
FunctionResponseTypes Partial batch response ReportBatchItemFailures for granular failures
OnFailure Where to send failed batches SQS DLQ or SNS topic
public class Handler implements RequestHandler<SQSEvent, SQSBatchResponse> {
    @Override
    public SQSBatchResponse handleRequest(SQSEvent event, Context context) {
        List<SQSBatchResponse.BatchItemFailure> failures = new ArrayList<>();
        for (SQSEvent.SQSMessage msg : event.getRecords()) {
            try {
                processMessage(msg);
            } catch (Exception e) {
                failures.add(new SQSBatchResponse.BatchItemFailure(msg.getMessageId()));
            }
        }
        return new SQSBatchResponse(failures);
    }
}
• • •

VThe State Paradox in the Serverless World

The main philosophy of FaaS is statelessness. The function is ephemeral: it launches, does work, and disappears along with its local memory.

In the classic EDM approach, state is externalized. The function explicitly connects to external stores (Redis, DynamoDB, CosmosDB) over the network. This reduces scaling flexibility — the FaaS provider must account for network latency and external DB limits — but allows horizontal scaling without restrictions.

Link to Chapter VII

If a function must remember state, it must not try to store it in its own container. This fundamentally differs from Kafka Streams, where the state store lives on local disk and is protected by a changelog. In FaaS there is no such option — only external stores with network latency.

• • •

VIAzure Durable Functions: Stateful FaaS

The evolution of the approach — some providers began offering abstractions that automatically persist local function state to external storage. This allows developers to write code as if it had memory, delegating the routine of saving and restoring to the framework.

Orchestrator functions

Long-lived. Store process state, invoke activity functions, wait for results. Implement Saga with automatic state persistence.

Activity functions

Stateless workers. Perform one operation (API call, DB write). Can run in parallel; orchestration synchronizes.

Entity functions

Stateful actors. Store state between calls, process operations sequentially. Ideal for aggregates.

Client functions

Entry point. Start orchestration, send events, poll status.

[FunctionName("OrderSaga")]
public static async Task<string> RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    try {
        await context.CallActivityAsync("ReserveInventory", order);
        await context.CallActivityAsync("ChargePayment", order);
        await context.CallActivityAsync("ShipOrder", order);
        return "completed";
    } catch (Exception) {
        await context.CallActivityAsync("ReleaseInventory", order);
        await context.CallActivityAsync("RefundPayment", order);
        throw;
    }
}
• • •

VIIOrchestration vs Choreography in FaaS

Choreography (async fire-and-forget)

Function A processes event, publishes result to a new stream, and completes. Function B listens and picks up.

Danger: if A calls B directly fire-and-forget, the FaaS provider may launch dozens of B instances in parallel. This guarantees violation of ordering within a partition.

Orchestration (sync invoke)

An orchestrator function sequentially invokes A and B, waiting for each result before moving to the next.

Advantage: strict ordering. Offset commits only after full success.

public int orchestrationFunction(Event[] events, Context context) {
    for (Event event : events) {
        Result resA = invokeFunctionA(event);
        Result resB = invokeFunctionB(event, resA);
        producer.produce("Output-Stream", composeOutput(resA, resB));
    }
    context.success();
    return 0;
}
• • •

VIIIIdempotency of FaaS Functions

  1. Idempotency key in header. For HTTP triggers, pass Idempotency-Key. For SQS — use MessageId.
  2. Conditional writes. For DynamoDB — attribute_not_exists(event_id). For PostgreSQL — INSERT ... ON CONFLICT DO NOTHING.
  3. Dedup in state store. Store processed event_id in Redis with TTL.
PutItemRequest request = PutItemRequest.builder()
    .tableName("processed-events")
    .item(Map.of(
        "event_id", AttributeValue.builder().s(eventId).build(),
        "processed_at", AttributeValue.builder().n(String.valueOf(now)).build()
    ))
    .conditionExpression("attribute_not_exists(event_id)")
    .build();

try {
    dynamoDb.putItem(request);
    processEvent(event);
} catch (ConditionalCheckFailedException e) {
    // Already processed — skip
}
• • •

IXScaling and the Rebalancing Deadlock

Danger: Rebalancing Deadlock

If you use aggressive lag-based auto-scaling, the function will constantly start and stop. Each start and stop forces a broker partition reassignment (rebalance). In extreme cases, the consumer group hits a "deadlock": the function spends its entire lifespan just getting partition assignments and never even begins processing.

Hysteresis loop

Configure scaling policies to require sustained threshold exceedance for several minutes before adding instances.

Static assignment

If load is predictable, configure functions to know their partitions in advance. Eliminates rebalance overhead.

Batch management

If the function cannot process a batch within its lifespan — it fails with timeout. Reduce batch size or increase execution limit.

Cooperative sticky assignor

Use cooperative sticky assignor (Chapter V) which redistributes only changed partitions.

• • •

XVPC and Network Latency

Option Latency Cost
NAT Gateway +2–5 ms Hourly + traffic
VPC Endpoint (PrivateLink) +1–2 ms Hourly + traffic
Lambda outside VPC Native No extra cost

Rule: if the function doesn't need access to VPC resources (RDS, ElastiCache), don't put it in a VPC.

• • •

XIFaaS Cost Model

Scenario FaaS Cheaper? Why
Rare triggers Yes Pay only for invocations; container 24/7 would idle
Variable load Yes Auto-scaling without peak reservation
Constant high load No Reserved-capacity containers are cheaper
High-throughput streams No Kafka Streams / Flink on containers more efficient
Stateful aggregations No Network latency to external stores kills performance
• • •

XIITesting FaaS Functions

LocalStack

Local emulation of AWS services (SQS, S3, DynamoDB, Lambda). Testcontainers + LocalStack for integration tests.

AWS SAM Local

Local Lambda execution with API Gateway and event source emulation. Fast iteration without deploy.

Serverless Framework offline

Plugin for local HTTP trigger execution.

Unit tests with mock events

Function accepts JSON — test with mock events. Fast but doesn't verify integration.

• • •

XIIIComparing FaaS Providers

Provider Max Execution Triggers Stateful
AWS Lambda 15 min SQS, Kinesis, MSK, DynamoDB Streams, EventBridge, S3 External stores (DynamoDB, ElastiCache)
Azure Functions 10 min (default)
Unlimited for Durable
Event Hub, Service Bus, Event Grid, Blob Storage Durable Functions (orchestrator, entity)
GCP Cloud Functions 9 min Pub/Sub, Firestore, Cloud Storage, Eventarc External stores (Firestore, Memorystore)
Cloudflare Workers 30 sec (CPU) HTTP, Queues, Durable Objects Durable Objects
• • •

XIVChecklists and Link to the Series

Link to Previous Chapters

Chapter Focus Link to This Chapter
II. The Anatomy of EDA Partitioning, consumer groups FaaS consumer limited by partition count
V. The Mechanics of Stream Processing Rebalance, state stores Rebalancing deadlock is a consequence of rebalance policies
VII. The Memory of Microservices State stores, TTL, dedup In FaaS state is only external; dedup via conditional writes
VIII. The Mechanics of Business Processes Saga, orchestration Durable Functions implement Saga with automatic state
IX. Serverless Architecture FaaS in EDA When FaaS, and when Kafka Streams / Flink

FaaS Microservice Design Checklist

  • Function belongs to one bounded context
  • Offset commits AFTER successful processing
  • Partial batch response enabled for stream sources
  • Failure destination (DLQ, SNS) configured
  • Connection initialization outside handler (warm reuse)
  • Idempotency via idempotency key / conditional write / dedup table
  • State externalized (Redis, DynamoDB)
  • Durable Functions or workflow engine for long processes
  • Ordering guaranteed via orchestration if order matters
  • Auto-scaling with hysteresis, not aggressive
  • Static partition assignment where load is predictable
  • Batch size aligned with function lifespan
  • Function outside VPC if VPC resources not needed
  • Cost evaluated: FaaS vs containers
  • LocalStack / SAM Local for integration tests
Final Thesis

Use FaaS where events are discrete and scaling must happen instantly in response to bursts. But always remember the "tax" of rebalancing, the network latency to external stores, and strictly follow the discipline of offset commits. For complex deterministic processing of multiple streams, exactly-once, or local aggregations — Kafka Streams or Flink remain more predictable.

Chapter IX of the "Event-Driven Architecture" series. Sources: AWS Lambda, Azure Durable Functions, GCP Cloud Functions, Cloudflare Workers, LocalStack, AWS SAM, production system case studies.