Архитектура backend: от базовой логики до высоконагруженных систем (на Node.js)

Курс помогает перейти от знания Node.js к системному пониманию архитектуры backend: как проектировать доменную логику, API, данные и интеграции. Вы научитесь строить надежные и масштабируемые сервисы, готовые к высоким нагрузкам, с учетом наблюдаемости, отказоустойчивости и безопасности.

1. Как мыслит backend: требования, доменная модель и бизнес-логика

Как мыслит backend: требования, доменная модель и бизнес-логика

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

Эта статья — фундамент курса. Мы разложим мышление backend-разработчика на три опоры:

  • Требования: что и зачем мы строим
  • Доменная модель: как мы описываем предметную область
  • Бизнес-логика: где живут правила и как они исполняются
  • Требования: превращаем «хочу фичу» в спецификацию

    Требования бывают разными, но для архитектуры важнее всего разделить их на функциональные и нефункциональные.

  • Функциональные требования отвечают на вопрос: что система должна делать.
  • Нефункциональные требования отвечают на вопрос: как система должна это делать (скорость, надежность, безопасность, масштабируемость).
  • Как задавать правильные вопросы

    Если вам сказали: «Сделай оформление заказа», backend мыслит не кодом, а вопросами.

  • Какой объект мы создаем: заказ? бронь? сделка?
  • Какие состояния он проходит: черновик → оплачено → доставлено?
  • Кто может делать переходы между состояниями?
  • Какие правила должны соблюдаться всегда: нельзя оплатить пустой заказ, нельзя доставить неоплаченный, нельзя списать больше остатка.
  • Какие ошибки возможны и что система обязана гарантировать при сбое: «деньги списались, а заказ не создался» — недопустимо.
  • Фиксируем требования через сценарии (use cases)

    Сценарий (use case) — это описание цели пользователя и шагов системы.

    Хороший сценарий:

  • Имеет актора (кто делает действие)
  • Имеет предусловия (что уже должно быть истинно)
  • Имеет основной поток (happy path)
  • Имеет альтернативы (ошибки, отклонения)
  • Имеет постусловия (что стало истинно после выполнения)
  • Пример (очень кратко):

  • Актор: клиент
  • Цель: оплатить заказ
  • Предусловия: заказ в статусе PENDING_PAYMENT, сумма > 0
  • Основной поток: создать платеж → получить подтверждение → пометить заказ как PAID
  • Альтернативы: платеж отклонен → заказ остается PENDING_PAYMENT
  • Постусловия: оплаченный заказ нельзя оплатить повторно
  • > Подход с use case хорошо сочетается с архитектурой, где бизнес-логика организована вокруг сценариев (application layer), а не вокруг «толстых контроллеров».

    !Диаграмма превращения требований в сценарии и правила

    Нефункциональные требования, которые влияют на архитектуру

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

  • Производительность: сколько запросов в секунду, какие SLA по времени ответа
  • Надежность: что считается «допустимым сбоем», как восстанавливаться
  • Консистентность: где нужна строгая консистентность, а где допустима eventual consistency
  • Безопасность: аутентификация, авторизация, аудит
  • Масштабирование: вертикальное/горизонтальное, статeless/состояние
  • На старте курса важно запомнить правило:

  • Функциональные требования определяют что мы делаем.
  • Нефункциональные требования часто определяют архитектуру того, как мы это делаем.
  • Доменная модель: переводим бизнес на язык кода

    Домен — это предметная область: интернет-магазин, доставка, финансы, обучение.

    Доменная модель — это набор понятий и правил домена в виде структуры кода: сущности, значения, связи, состояния, ограничения.

    Хорошая доменная модель делает две вещи:

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

    #### Сущность (Entity) Сущность — объект с идентичностью, который меняется со временем.

    Примеры:

  • Пользователь (userId)
  • Заказ (orderId)
  • Подписка (subscriptionId)
  • Ключевое: два заказа с одинаковыми полями — это все равно разные заказы, если у них разные идентификаторы.

    #### Объект-значение (Value Object) Объект-значение — объект без идентичности, важны только данные, обычно он неизменяемый.

    Примеры:

  • Деньги: amount + currency
  • Email (валидированный)
  • Диапазон дат
  • Ключевое: два Money(100, 'RUB') — одинаковые значения.

    #### Инвариант Инвариант — правило, которое должно быть истинным всегда для доменного объекта.

    Примеры:

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

    Почему «таблица в БД» не равна «доменная сущность»

    Таблица — это способ хранения. Доменная сущность — способ думать.

    Типичная ошибка новичка: строить архитектуру от БД.

  • Сначала создают таблицы
  • Потом пишут CRUD
  • Потом добавляют правила в контроллеры «по месту»
  • Итог: правила размазаны, изменения ломают систему, тестировать больно
  • Правильнее (особенно для сложных систем): строить от сценариев и инвариантов.

    Единый язык (ubiquitous language)

    Чтобы архитектура не развалилась, договоритесь о словах:

  • Если бизнес говорит «заказ», не называйте это в коде то Purchase, то CartOrder, то Deal без причины
  • Если есть «оплата» и «списание» — это могут быть разные сущности и разные состояния
  • Слова — это часть архитектуры.

    Полезное чтение:

  • Domain Model (Martin Fowler)
  • Бизнес-логика: где живут правила и как они исполняются

    Бизнес-логика — это то, что отвечает на вопрос: «какое поведение правильное по правилам домена?»

    Важно отличать бизнес-логику от «технического кода».

  • Бизнес-логика: «нельзя оплатить заказ с нулевой суммой»
  • Технический код: «как сходить в Postgres», «как отправить HTTP-запрос», «как сериализовать JSON»
  • Принцип слоев: UI/API, приложение, домен, инфраструктура

    Даже если вы не используете сложные паттерны, полезно разделять ответственность:

  • API-слой (контроллеры/роуты)
  • 1. Принять запрос 2. Проверить формат (schema/DTO) 3. Вызвать сценарий 4. Вернуть ответ
  • Слой приложения (use cases)
  • 1. Оркестрация шагов сценария 2. Транзакция, идемпотентность, вызовы домена
  • Доменный слой (модель и правила)
  • 1. Сущности, объекты-значения 2. Инварианты 3. Доменные методы, отражающие бизнес
  • Инфраструктура
  • 1. Репозитории, БД, очереди, внешние API

    Ключевая идея: домен не должен зависеть от инфраструктуры. Сущность «Заказ» не должна знать, что она хранится в PostgreSQL.

    !Схема слоев и направления зависимостей

    Где именно писать бизнес-правила

    Правило должно жить там, где его сложнее всего нарушить.

  • Правила, относящиеся к конкретной сущности, удобно держать в доменной сущности
  • Правила, которые координируют несколько сущностей, часто живут в сценарии (use case) или доменном сервисе
  • Пример: «оплатить заказ» — это не просто UPDATE orders SET status='PAID'.

  • Надо проверить статус
  • Надо убедиться, что сумма корректна
  • Надо создать запись платежа
  • Надо обновить заказ
  • Надо корректно обработать повторы (клиент нажал кнопку дважды)
  • Минимальный пример модели и use case (Node.js)

    Ниже пример идеологический, не «идеальная DDD-реализация». Его цель — показать, как отделять доменные правила от HTTP.

    #### Доменная сущность

    Здесь зашит инвариант: заказ нельзя оплатить в неправильном статусе или с нулевой суммой.

    #### Сценарий (use case)

    Обратите внимание:

  • Доменная модель решает можно ли оплатить
  • Use case решает как это сделать пошагово, включая внешние вызовы
  • HTTP здесь вообще не нужен — use case можно тестировать без сервера
  • #### HTTP-роут как тонкая оболочка

    Контроллер не содержит бизнес-правил. Он только связывает HTTP и сценарий.

    Типичные ошибки в архитектуре бизнес-логики

  • Толстые контроллеры: правила размазаны по роутам, сложно тестировать
  • Анемичная модель: сущности — просто набор полей, а правила живут где попало
  • Смешивание уровней: домен импортирует ORM/HTTP-клиент и становится непереносимым
  • Валидация только на входе: формат проверили, а инварианты забыли
  • Полезное чтение:

  • Anemic Domain Model (Martin Fowler)
  • Как связать требования, домен и код: практический алгоритм

    Когда вы делаете новый модуль backend, действуйте так:

  • Соберите требования и выпишите сценарии (use cases)
  • Для каждого сценария выпишите:
  • 1. Какие сущности участвуют 2. Какие инварианты должны соблюдаться 3. Какие внешние системы нужны (БД, платежи, сообщения)
  • Нарисуйте минимальную доменную модель:
  • 1. Сущности и их состояния 2. Объекты-значения 3. Связи между сущностями
  • Определите границы ответственности:
  • 1. Что в домене 2. Что в use case 3. Что в инфраструктуре
  • Реализуйте сценарий так, чтобы:
  • 1. Инварианты проверялись внутри домена 2. Контроллеры были тонкими 3. Ошибки были предсказуемыми

    Что дальше по курсу

    Дальше мы будем превращать эту основу в устойчивую архитектуру:

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

    2. Слои и границы: Clean Architecture, DDD и модульность

    Слои и границы: Clean Architecture, DDD и модульность

    В прошлой статье мы разобрали, как backend мыслит через требования → сценарии (use cases) → доменную модель → инварианты. Теперь следующий шаг: как разложить код по слоям и провести границы так, чтобы проект рос без хаоса.

    Эта статья про три взаимодополняющие идеи:

  • Clean Architecture: как разделять код по слоям и управлять зависимостями
  • DDD (Domain-Driven Design): как резать систему на смысловые части по предметной области
  • Модульность: как превратить всё это в структуру репозитория и правила разработки (особенно в Node.js)
  • Зачем вообще слои и границы

    Если границ нет, происходит типичный сценарий:

  • бизнес-правила размазываются по контроллерам, ORM-хукам и сервисам
  • изменения в БД ломают доменные сценарии
  • тестировать сложно, потому что всё зависит от всего
  • выделить часть системы в отдельный сервис или масштабировать точечно почти невозможно
  • Слои отвечают на вопрос: какая ответственность у кода?.

    Границы отвечают на вопрос: что является отдельной частью системы и как части общаются?.

    Clean Architecture простыми словами

    Clean Architecture (часто рядом упоминают Hexagonal Architecture и Ports & Adapters) предлагает два ключевых правила:

  • Бизнес-логика не должна зависеть от инфраструктуры
  • Зависимости направлены внутрь, к более “чистому” коду
  • Источник: The Clean Architecture (Robert C. Martin)

    !Слои Clean Architecture и направление зависимостей

    Типовые слои и что в них лежит

    Ниже практичное разбиение, которое хорошо ложится на Node.js-проекты.

    #### Доменный слой (domain)

    Это код, который описывает предметную область и её правила.

  • сущности (Entity): объекты с идентичностью, которые меняются со временем (например, Order)
  • объекты-значения (Value Object): данные без идентичности, где важны значения (например, Money)
  • инварианты: правила, которые должны быть истинны всегда (например, “оплаченный заказ нельзя оплатить повторно”)
  • Важно:

  • домен не импортирует Express, Prisma, PostgreSQL-клиент, HTTP-клиенты
  • домен максимально “чистый” и легко тестируемый
  • #### Слой приложения (application)

    Это сценарии (use cases): оркестрация шагов.

  • загружаем данные через репозитории
  • вызываем доменные методы
  • координируем несколько сущностей
  • открываем транзакции, обеспечиваем идемпотентность, публикуем события (в дальнейшем по курсу)
  • Важный ориентир:

  • контроллер не должен решать “можно ли оплатить заказ”
  • use case вызывает домен, а не заменяет его
  • #### Слой интерфейсов и адаптеров (interfaces/adapters)

    Это прослойка между “миром HTTP/очередей” и “миром use cases”.

  • HTTP-контроллеры, роуты
  • маппинг DTO ↔ доменные типы
  • обработка ошибок и перевод их в HTTP-коды
  • #### Инфраструктура (infrastructure)

    Это детали реализации.

  • конкретные репозитории на PostgreSQL, Redis
  • интеграции с внешними API
  • брокеры сообщений
  • реализации логирования, метрик
  • Правило зависимостей (самое важное)

    Если упростить до одного правила:

  • внутренний код ничего не знает о внешнем
  • Практически это означает:

  • domain не импортирует infrastructure
  • application не импортирует Express/fastify
  • инфраструктура подключается к приложению через внедрение зависимостей
  • Порты и адаптеры: как “оторвать” бизнес-логику от технологий

    Чтобы бизнес-логика не зависела от БД и внешних API, используют идею:

  • порт (port): интерфейс, который нужен приложению (например, OrderRepository)
  • адаптер (adapter): конкретная реализация порта (например, PostgresOrderRepository)
  • В JavaScript нет интерфейсов на уровне языка, но в архитектуре “порт” всё равно полезен:

  • в TypeScript это может быть interface
  • в Node.js (JS) это может быть “контракт” через документацию + тесты + согласованные методы
  • Ссылка на первоисточник подхода: Hexagonal Architecture (Alistair Cockburn)

    Минимальный пример на Node.js: порт, use case, адаптер

    #### Порт (контракт репозитория)

    #### Use case

    #### Адаптер репозитория (инфраструктура)

    Ключевая мысль: use case не знает, какая БД используется. Он знает только порт.

    DDD: границы по смыслу, а не по технологиям

    DDD (Domain-Driven Design) полезен не “как набор паттернов”, а как способ:

  • выстроить модель предметной области
  • договориться о терминах
  • отделить части системы, которые меняются по разным причинам
  • Официальный ресурс сообщества: Domain Language

    Bounded Context: что это и зачем

    Bounded Context (ограниченный контекст) — это граница, внутри которой термины и модель имеют однозначный смысл.

    Например, слово “заказ” может означать разное:

  • в контексте продаж заказ — набор позиций и цена
  • в контексте логистики заказ — адрес, коробки, трекинг
  • в контексте поддержки заказ — обращения, возвраты
  • Если пытаться держать один “универсальный” Order на всё, модель раздувается, правила конфликтуют, и изменения становятся дорогими.

    Хорошее вводное объяснение: Bounded Context (Martin Fowler)

    Aggregate: как защитить инварианты при изменениях

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

  • есть корень агрегата (Aggregate Root) — главная сущность, через которую разрешены изменения
  • другие объекты внутри агрегата не должны изменяться “напрямую” извне
  • Пример:

  • Order как корень агрегата
  • OrderItem внутри
  • правило “нельзя оплатить заказ с пустым списком позиций” проверяется в Order
  • Это особенно важно в конкурентных сценариях и при транзакциях: вы хотите иметь одно место, где гарантируется целостность.

    Как Clean Architecture и DDD сочетаются

    Они решают разные задачи и хорошо стыкуются.

  • Clean Architecture отвечает: как разделить ответственность и зависимости в коде
  • DDD отвечает: как правильно нарезать предметную область на части и как моделировать правила
  • Практическая связка:

  • Bounded Context → отдельный модуль в коде
  • Use cases → слой application внутри модуля
  • Entities/Value Objects/Aggregates → слой domain внутри модуля
  • Adapters/Repositories implementations → infrastructure, привязанная к модулю
  • Модульность в backend: как сделать архитектуру “ощутимой” в репозитории

    Архитектура без структуры проекта и правил импорта быстро превращается в “красивую диаграмму, которую никто не соблюдает”. Модульность делает границы реальными.

    Модульный монолит как лучшая стартовая точка

    Для большинства продуктов правильный старт:

  • один деплой (монолит)
  • внутри строгие модули (как будто мини-сервисы)
  • Это дает:

  • простую разработку и деплой
  • четкие границы
  • возможность позже выделять части в микросервисы по реальной необходимости
  • !Как выглядят модули и их публичные API в монолитном репозитории

    Пример структуры папок для модульного монолита (Node.js)

    Один из рабочих вариантов:

    Идея:

  • modules/<name>/index.js — публичный API модуля (что разрешено импортировать извне)
  • всё остальное внутри модуля считается внутренними деталями
  • Правила, которые стоит ввести сразу

  • импортировать другой модуль можно только через его публичный index.js
  • домен модуля не импортирует код других модулей напрямую
  • если нужен обмен данными между модулями:
  • - либо через use cases (сервисный слой модуля) - либо через доменные события (позже в курсе) - либо через общий shared kernel (очень аккуратно и минимально)

    Shared kernel: когда “shared” полезен, а когда вреден

    shared/ часто превращается в помойку. Чтобы этого не произошло:

  • храните там только действительно общие стабильные вещи
  • не кладите туда бизнес-сущности конкретного контекста
  • Хорошие кандидаты для shared:

  • Email, Money (если это реально одинаковые правила для всех контекстов)
  • базовые ошибки и типы результатов
  • инфраструктурные утилиты (логирование, трассировка), не завязанные на домен
  • Плохие кандидаты:

  • Order, Payment, Subscription (они почти всегда контекстные)
  • Граница “домен” vs “данные”: как не построить архитектуру от таблиц

    В прошлой статье мы фиксировали важную мысль: таблица в БД не равна доменная модель.

    На практике это проявляется так:

  • доменная сущность может хранить данные иначе, чем таблица
  • доменная модель может скрывать детали хранения
  • маппинг между доменом и БД — ответственность инфраструктуры
  • Полезный прием:

  • делайте явные функции mapRowToDomain и mapDomainToRow
  • не протаскивайте ORM-модели внутрь домена
  • Как понять, что границы проведены правильно

    Признаки, что архитектура становится лучше:

  • use case тестируется без поднятия HTTP-сервера
  • домен тестируется без БД
  • смена PostgreSQL на другую БД не требует переписывать домен
  • вы можете добавить второй интерфейс (например, обработчик сообщений из очереди) и переиспользовать те же use cases
  • Признаки проблем:

  • доменные сущности импортируют Prisma/Sequelize
  • контроллеры принимают решения “по правилам бизнеса”
  • репозитории возвращают “сырой” формат БД без маппинга, и этот формат расползается по проекту
  • Практический алгоритм: как проектировать новый модуль

  • Выпишите сценарии (use cases) и правила (инварианты)
  • Определите границу контекста: где модель и термины однозначны
  • Спроектируйте домен: сущности, объекты-значения, инварианты
  • Спроектируйте порты: какие зависимости нужны use case
  • Реализуйте адаптеры в инфраструктуре
  • Сделайте публичный API модуля и запретите прямые импорты “внутренностей”
  • Что дальше по курсу

    Дальше мы будем углублять то, что выстроили здесь:

  • репозитории, транзакции и согласованность данных
  • надежные интеграции: очереди, outbox, саги
  • идемпотентность и обработка конкурентных сценариев
  • подготовка к высокой нагрузке: кэширование, горизонтальное масштабирование, деградация
  • Слои и границы дают вам основу: код начинает отражать устройство системы, а не набор случайных файлов.

    3. Проектирование API: REST, GraphQL, контракты и версии

    Проектирование API: REST, GraphQL, контракты и версии

    API — это граница между вашим backend и внешним миром: фронтендом, мобильными клиентами, партнёрами, другими сервисами. Если в прошлых статьях мы строили домен и use cases и проводили границы по Clean Architecture, то здесь мы разберём, как эту бизнес-логику правильно выставлять наружу.

    Ключевая мысль:

  • API — это часть слоя интерфейсов (adapters/interfaces)
  • use cases и домен не должны “подстраиваться” под HTTP/GraphQL
  • контракт API важнее конкретного фреймворка
  • !API как адаптер: переводит внешний протокол в вызов сценариев

    Откуда начинать проектирование API

    Начинать стоит не с роутов и не с таблиц в БД, а со сценариев.

  • Выпишите use cases (например, “Создать заказ”, “Оплатить заказ”, “Отменить заказ”).
  • Определите, какие данные нужны клиенту для каждого сценария.
  • Решите, где нужна строгая консистентность, а где допустима задержка (это влияет на синхронность ответов, очереди, статусные операции).
  • Зафиксируйте контракт: форматы запросов/ответов, ошибки, пагинация, версии.
  • Практическое правило:

  • API должно быть удобным для потребителя, но не разрушать доменную модель
  • Если вы чувствуете, что домен “кривится” из-за того, что клиенту хочется другой формат — это задача маппинга в адаптере, а не повод менять инварианты в домене.

    REST: ресурсный стиль без мифов

    REST часто путают с “HTTP + JSON”. На практике для backend-архитектуры важны более приземлённые принципы:

  • вы проектируете ресурсы (то, что имеет идентичность) и операции над ними
  • вы используете возможности HTTP: методы, коды статусов, кеширование
  • вы делаете интерфейс предсказуемым и совместимым
  • Ресурсы и операции

    Ресурс — это то, что можно “назвать” и адресовать. Обычно это сущность домена, но не всегда.

    Примеры ресурсов:

  • users
  • orders
  • payments
  • Типовые операции:

  • GET /orders/{id} получить представление заказа
  • POST /orders создать заказ
  • PATCH /orders/{id} частично изменить
  • DELETE /orders/{id} удалить (если в домене вообще допустимо удаление)
  • Важно: use case и “REST-операция” не обязаны совпадать 1 в 1.

  • REST удобен, когда операция естественно ложится на ресурс.
  • Но в бизнесе часто есть команды: “оплатить”, “отгрузить”, “заблокировать”. Это не всегда “обновить поле”.
  • Команды в REST: как моделировать “действия”

    Есть два распространённых подхода.

  • Команда как подресурс
  • 1. POST /orders/{id}/payment (создать попытку оплаты) 2. POST /orders/{id}/cancel (запросить отмену)
  • Команда как ресурс операции
  • 1. POST /payment-intents (создать намерение оплаты) 2. POST /cancellations (создать запрос на отмену)

    Второй вариант часто лучше для высоконагруженных и надёжных систем, потому что “команда” получает идентичность и статус.

  • вы можете сделать операцию асинхронной
  • вы можете переотправлять команды идемпотентно
  • вы можете хранить аудит и историю
  • Коды ответов и ошибки

    HTTP даёт общий язык, но его легко испортить “всем возвращаем 200”. Договоритесь хотя бы о базовом:

  • 200 успешный запрос (есть тело ответа)
  • 201 создан ресурс (часто с заголовком Location)
  • 204 успешно, но без тела
  • 400 формат запроса неверный
  • 401 не аутентифицирован
  • 403 нет прав
  • 404 не найдено
  • 409 конфликт (например, нарушение инварианта или конкурентный конфликт)
  • 422 семантически неверно (часто используют для нарушений доменных правил)
  • Чтобы ошибки были машиночитаемыми и стабильными, используйте единый формат. Хорошая база — стандарт Problem Details.

  • спецификация: RFC 9457: Problem Details for HTTP APIs
  • Пример ответа об ошибке:

    Дополнение, полезное для домена:

  • добавьте стабильный code, чтобы фронтенд не парсил detail
  • Пагинация и сортировка

    Для списков почти всегда нужна пагинация. Есть два основных стиля.

  • Offset pagination: ?limit=20&offset=40
  • - проще - хуже на больших данных и при изменениях в выборке
  • Cursor pagination: ?limit=20&cursor=...
  • - стабильнее при пролистывании - обычно лучше для высоких нагрузок

    С точки зрения контракта важно:

  • фиксировать дефолты (limit по умолчанию)
  • ограничивать максимальный limit
  • документировать сортировку (например, sort=createdAt:desc)
  • Идемпотентность для “создать/оплатить”

    Идемпотентность означает: повтор одного и того же запроса не приводит к повторному эффекту.

    В реальных системах повторы неизбежны:

  • пользователь нажал кнопку дважды
  • мобильное приложение повторило запрос из-за сети
  • балансировщик/прокси ретраит
  • Популярный контрактный приём:

  • клиент передаёт Idempotency-Key
  • сервер хранит результат для ключа в рамках операции
  • Пример:

  • POST /payments с Idempotency-Key: 9f1...
  • повтор с тем же ключом возвращает тот же результат
  • Важно: идемпотентность обычно реализуется в application layer (use case) и инфраструктуре (хранилище ключей), а не в контроллере.

    GraphQL: когда и зачем

    GraphQL — это не “замена REST”, а другой способ представить контракт.

    Суть:

  • клиент сам описывает, какие поля ему нужны
  • сервер отвечает ровно этими данными
  • контракт выражен в схеме (schema)
  • Спецификация: GraphQL Specification

    Базовые понятия GraphQL

  • Schema: описание типов и операций
  • Query: чтение данных
  • Mutation: изменение данных
  • Resolver: функция, которая реально достаёт/считает данные
  • Пример схемы:

    Важно для архитектуры:

  • GraphQL-резолверы должны быть такими же “тонкими”, как REST-контроллеры
  • резолвер вызывает use case, а не реализует бизнес-правила
  • Плюсы и минусы GraphQL с точки зрения backend-архитектуры

    Плюсы:

  • меньше “overfetching/underfetching” (когда REST отдаёт лишнее или не отдаёт нужного)
  • удобно для сложных UI, где на одном экране много связанных данных
  • схема — живой контракт, удобный для автогенерации типов
  • Минусы и риски:

  • сложнее контролировать нагрузку (клиент может запросить “слишком много”)
  • риск проблемы N+1 запросов (когда резолверы ходят в БД много раз)
  • кеширование на уровне HTTP обычно сложнее
  • Если вам нужен GraphQL, заранее продумайте:

  • лимиты глубины запроса и сложности
  • батчинг и DataLoader-подход
  • какие операции должны быть асинхронными
  • Контракты: как сделать API “договором”, а не “угадайкой”

    Контракт — это то, что позволяет развивать backend и клиентов независимо.

    Хороший контракт фиксирует:

  • эндпоинты или schema
  • форматы запросов/ответов
  • коды ошибок
  • правила пагинации
  • требования к авторизации
  • совместимость изменений
  • OpenAPI для REST

    OpenAPI — это стандарт описания REST API.

    Спецификация: OpenAPI Specification

    Мини-пример (фрагмент):

    Практические выгоды:

  • документация не расходится с реальностью, если вы делаете проверки
  • можно генерировать клиентов
  • можно валидировать вход/выход
  • JSON Schema для входной валидации

    JSON Schema удобно использовать для проверки формата входных данных.

    Официальный сайт: JSON Schema

    Важно не путать:

  • валидация формата (JSON Schema): типы, обязательные поля, ограничения длины
  • валидация домена (инварианты): “нельзя оплатить оплаченный заказ”
  • Формат можно проверять в API-слое, а инварианты должны оставаться в домене/use case.

    Контракт как часть модульности

    Если у вас модульный монолит, контракт модуля удобно фиксировать так:

  • внешний контракт: OpenAPI/GraphQL schema
  • внутренний контракт: публичный API модуля (например, modules/orders/index.js)
  • Тогда изменения в домене не “протекают” наружу без осознанного решения.

    Версионирование: как менять API, не ломая клиентов

    Версия нужна не “всегда”, а когда вы не можете обеспечить обратную совместимость.

    Цель версионирования:

  • выпускать изменения предсказуемо
  • позволять клиентам мигрировать постепенно
  • Стратегии версионирования REST

    Основные варианты.

  • Версия в URL: GET /v1/orders/123
  • - просто - легко поддерживать параллельно - но URL “зашивается” во всё
  • Версия в заголовке: например X-API-Version: 1
  • - URL чистый - сложнее дебажить и кешировать
  • Версия через media type: Accept: application/vnd.example.v1+json
  • - ближе к HTTP-семантике - сложнее для большинства команд

    Специфика HTTP и представлений описана в стандарте: RFC 9110: HTTP Semantics

    Что считать ломающим изменением

    Ломающие изменения почти всегда такие:

  • переименовали поле
  • изменили тип поля
  • изменили смысл значения (семантику) без изменения названия
  • удалили поле или статус
  • Неломающие (обычно):

  • добавили новое необязательное поле
  • добавили новый эндпоинт
  • добавили новый статус так, чтобы старые клиенты могли его безопасно игнорировать
  • Важно: если клиент написан строго (например, строгое декодирование без “лишних полей”), то даже добавление поля может стать проблемой. Поэтому контракт должен включать ожидания по “tolerant reader” подходу: клиент должен уметь игнорировать неизвестные поля.

    Версионирование в GraphQL

    В GraphQL обычно стараются избегать версий API целиком и эволюционировать схему:

  • добавлять новые поля
  • помечать старые поля как устаревшие
  • Это поддерживается механизмом @deprecated.

    Пример:

    Удаление поля делается позже, после периода миграции.

    Практические правила хорошего API-контракта

    Стабильные имена и единый словарь

    API должно говорить на языке домена (из прошлых статей):

  • используйте единый термин для одного понятия
  • не называйте одно и то же разными словами в разных местах
  • Разделяйте DTO и доменные объекты

    DTO — это “транспортный формат” для API. Доменные сущности живут внутри.

  • DTO может быть плоским и удобным для клиента
  • доменный объект может быть сложнее и содержать поведение
  • Маппинг должен быть в адаптере.

    Стабильный формат ошибок

    Договоритесь и зафиксируйте:

  • где лежит code
  • где текст для человека
  • какие поля всегда присутствуют
  • Набросок “тонкого” REST-контроллера (Node.js)

    Пример показывает идею: контроллер переводит HTTP в вызов use case.

    Обратите внимание:

  • бизнес-правила не живут в контроллере
  • контроллер возвращает договорённые коды и формат
  • Как выбрать между REST и GraphQL

    Не существует “всегда правильного” выбора. Выбор зависит от продукта и нагрузки.

    Таблица для ориентира:

    | Критерий | REST | GraphQL | |---|---|---| | Простота старта | Высокая | Средняя | | Контроль нагрузки | Обычно проще | Нужны лимиты и защита | | Кеширование | Естественно через HTTP | Обычно сложнее | | Сложные UI-экраны | Может требовать много запросов | Часто удобнее | | Контракт | OpenAPI | Schema | | Версионирование | Часто через версии API | Обычно через эволюцию схемы |

    Практический совет для курса:

  • если вы учитесь строить архитектуру и хотите “универсальную базу”, начинайте с REST + OpenAPI
  • GraphQL добавляйте, когда есть понятная боль: много разных экранов, сложные выборки, частая смена требований к данным
  • Что дальше по курсу

    Дальше мы будем связывать API-контракты с более “жёсткими” темами архитектуры:

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

    4. Данные и консистентность: SQL/NoSQL, транзакции и миграции

    Данные и консистентность: SQL/NoSQL, транзакции и миграции

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

    В прошлых статьях мы построили базу:

  • требования → сценарии (use cases) → доменная модель → инварианты
  • слои и границы (Clean Architecture, модули)
  • API как контракт и тонкий адаптер
  • Теперь фиксируем следующий ключевой слой реальности:

  • как хранить и менять данные так, чтобы инварианты не ломались
  • как выбирать между SQL и NoSQL осознанно
  • как проектировать транзакции и конкурентные изменения
  • как делать миграции схемы безопасно
  • !Где живёт транзакция в Clean Architecture и как она проходит через use case и репозитории

    Консистентность: что именно мы хотим гарантировать

    Слово консистентность используют в разных смыслах, и это источник постоянных ошибок.

    Консистентность как целостность данных

    Это про то, что данные не могут перейти в запрещённое состояние.

    Примеры:

  • оплаченный заказ не может снова стать неоплаченным
  • сумма позиций заказа должна совпадать с итоговой суммой
  • нельзя создать две оплаты с одним и тем же idempotencyKey
  • Эти правила живут в домене и сценариях, но часто требуют поддержки БД:

  • UNIQUE индекс (для уникальности)
  • внешние ключи (для ссылочной целостности)
  • CHECK ограничения (для простых инвариантов)
  • Важно:

  • БД не заменяет доменную модель, но может быть второй линией защиты
  • если инвариант критичный, лучше иметь и доменную проверку, и ограничение в БД
  • Консистентность как согласованность при распределённости

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

    Классический ориентир — CAP-теорема: в распределённой системе при сетевых разделениях невозможно одновременно гарантировать и доступность, и строгую согласованность.

    Источник: CAP theorem

    Практический вывод для backend:

  • внутри одной БД вы часто можете позволить себе строгие транзакции
  • между сервисами чаще используют асинхронность и eventual consistency
  • Дальше в курсе это продолжится темами очередей, outbox и саг, но сейчас нам важна база: транзакции и миграции.

    SQL и NoSQL: как выбирать без религии

    Короткая карта выбора

    | Вопрос | SQL (PostgreSQL/MySQL) | NoSQL (MongoDB/Cassandra и другие) | |---|---|---| | Нужны сложные связи и джоины | Обычно сильная сторона | Часто придётся денормализовать | | Нужны транзакции и строгие инварианты | Обычно проще и естественнее | Зависит от конкретной БД, часто сложнее | | Схема данных стабильно описуема | Да (DDL, миграции) | Может быть schema-less, но это не значит «без правил» | | Очень высокая запись по ключу, горизонтальное масштабирование | Возможно, но требует проектирования | Часто одна из сильных сторон | | Отчёты, агрегации, аналитические запросы | Обычно удобнее | Зависит от продукта и модели данных |

    Практическое правило для большинства продуктовых backend:

  • если вы строите систему со сценариями, инвариантами, заказами, оплатами, балансами, правами доступа, то SQL (часто PostgreSQL) — лучший дефолт
  • NoSQL выбирают, когда есть конкретная причина: модель данных, масштаб записи, требования к латентности, распределённость
  • ACID и почему это важно для архитектуры

    ACID — набор свойств транзакций, который помогает думать о гарантиях.

    Источник: ACID

    Разберём на человеческом языке:

  • Atomicity: либо выполнилось всё, либо ничего
  • Consistency: после транзакции данные не нарушают правил целостности БД
  • Isolation: параллельные транзакции не ломают друг друга
  • Durability: после коммита данные не пропадут при падении процесса
  • Ключевой для backend-архитектуры пункт — Isolation: именно он определяет, что происходит при одновременных запросах.

    Транзакция: единица целостного изменения

    В терминах прошлых статей:

  • use case описывает сценарий и оркестрацию шагов
  • транзакция часто должна покрывать критическую часть сценария, где инварианты могут быть нарушены конкурентно
  • Где открывать транзакцию в архитектуре

    Обычно транзакцию открывают в application layer, потому что:

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

  • use case: withTransaction(async (tx) => { ... })
  • репозитории: принимают tx как контекст выполнения
  • Минимальный пример транзакции в Node.js (PostgreSQL)

    Ниже пример без ORM, чтобы было видно, где границы.

    Use case держит транзакционную границу:

    Что здесь важно архитектурно:

  • доменное правило order.markPaid() не знает о транзакции
  • транзакция покрывает проверку идемпотентности и запись результатов
  • репозитории получают tx и выполняют запросы на одном соединении
  • Изоляция: почему параллельные запросы ломают инварианты

    Даже если ваш код «всё проверил», два запроса могут прийти одновременно.

    Пример гонки:

  • два запроса оплатить один и тот же заказ
  • оба увидели статус PENDING_PAYMENT
  • оба провели списание
  • оба обновили заказ
  • Без дополнительных мер вы получите двойную оплату.

    База даёт два основных инструмента:

  • уровень изоляции транзакции
  • блокировки и атомарные операции
  • Документация PostgreSQL по изоляции: PostgreSQL: Transaction Isolation

    Уровни изоляции в SQL на практике

    | Уровень | Что обычно означает | Типичный риск | |---|---|---| | READ COMMITTED | каждый запрос внутри транзакции видит уже закоммиченные изменения | гонки возможны, нужно явно блокировать строки или использовать уникальные ограничения | | REPEATABLE READ | транзакция видит «снимок» данных на момент старта | может быть проще рассуждать, но конфликты проявляются иначе | | SERIALIZABLE | БД старается обеспечить эффект как будто транзакции шли строго по очереди | возможны ошибки сериализации, которые надо корректно ретраить |

    Практический вывод:

  • чаще всего начинают с READ COMMITTED и решают гонки через UNIQUE + блокировки/атомарные запросы
  • SERIALIZABLE полезен, когда критична корректность, но требует дисциплины обработки конфликтов
  • Пессимистическая блокировка: SELECT ... FOR UPDATE

    Когда вы меняете сущность и хотите гарантировать, что её не меняют параллельно, вы можете заблокировать строку.

    Плюсы:

  • нет долгих блокировок
  • хорошо подходит для высоконагруженных систем
  • Минусы:

  • сценарий должен уметь обрабатывать конфликты (409 Conflict на API-уровне или ретрай внутри)
  • SQL-схема как часть архитектуры: ограничения и индексы

    Если в прошлых статьях инварианты жили в домене, то здесь важное усиление:

  • некоторые инварианты выгодно и правильно дублировать в БД
  • Типовые примеры:

  • уникальность: UNIQUE (idempotency_key)
  • ссылочная целостность: FOREIGN KEY (order_id) REFERENCES orders(id)
  • недопустимые значения: CHECK (total_amount > 0)
  • Индексы — это не «оптимизация потом», а часть проектирования данных, потому что они влияют на доступность системы под нагрузкой.

    Практические ориентиры:

  • индексируйте поля, по которым ищете и сортируете в горячих запросах
  • помните, что каждый индекс замедляет запись
  • уникальные индексы одновременно дают и производительность, и гарантию инварианта
  • NoSQL и консистентность: что важно понимать заранее

    NoSQL — это не «просто быстрее».

    Частые причины выбирать NoSQL:

  • модель данных естественно документная (например, сложные вложенные структуры, которые читаются целиком)
  • запись по ключу и горизонтальное масштабирование важнее сложных запросов
  • вы готовы к денормализации и переносите часть согласованности в приложение
  • Важно заранее выяснить:

  • есть ли транзакции и какие у них ограничения
  • как устроена репликация и какие гарантии чтения
  • какие операции атомарны
  • Пример: MongoDB поддерживает транзакции, но это влияет на архитектуру и производительность, и важно понимать ограничения.

    Документация: MongoDB Transactions

    Миграции: как менять схему без боли и простоев

    Миграция — это управляемое изменение структуры данных и схемы.

    Опасная ошибка начинающих:

  • «поменяю схему, задеплою, всё заработает»
  • В реальности у вас почти всегда есть:

  • несколько версий приложения одновременно (rolling deploy)
  • фоновые задачи, которые читают старые поля
  • кэш и реплики
  • большие таблицы, где ALTER TABLE может быть дорогим
  • Базовые принципы миграций

    Источник с хорошими практиками: Evolutionary Database Design

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

  • миграции должны быть воспроизводимыми и версионированными
  • изменения делайте маленькими шагами
  • по возможности делайте миграции backward compatible на период раскатки
  • Стратегия expand/contract для изменений без даунтайма

    Один из самых надёжных подходов для продакшена.

  • Expand
  • 1. Добавляем новое поле/таблицу/индекс 2. Код начинает писать в новое место (иногда параллельно со старым)
  • Backfill
  • 1. Переносим исторические данные фоном 2. Проверяем метриками и выборками корректность
  • Switch
  • 1. Код начинает читать из нового места
  • Contract
  • 1. Удаляем старые поля/таблицы после периода стабильности

    !Безопасная миграция схемы без простоя

    Пример: переименование поля без простоя

    Хотим заменить total на totalAmount.

  • Expand
  • 1. добавить колонку total_amount
  • Switch write
  • 1. приложение пишет и в total, и в total_amount
  • Backfill
  • 1. фоновая задача заполняет total_amount для старых строк
  • Switch read
  • 1. приложение читает total_amount
  • Contract
  • 1. удалить total

    Миграции данных и миграции схемы

    Два разных типа изменений:

  • схема: ALTER TABLE, индексы, ограничения
  • данные: пересчёт, заполнение новых колонок, исправление форматов
  • Архитектурный совет:

  • тяжёлые миграции данных делайте отдельными фоновых джобами, а не внутри DDL-миграции
  • Миграции и модульность

    Если у вас модульный монолит, удобно, когда каждый модуль владеет своими таблицами и миграциями.

    Практическая дисциплина:

  • migrations лежат рядом с модулем или строго разложены по контекстам
  • таблицы именуются с префиксом контекста, чтобы границы были видны (например, orders_orders, payments_payments или отдельные схемы)
  • Это продолжает идеи из статьи про границы: данные тоже должны уважать bounded context.

    Как это связано с API и доменом

    Ошибки консистентности должны отражаться в контракте

    Если транзакция не прошла из-за конфликта (например, UNIQUE нарушен или optimistic locking), это не «500».

    Чаще всего это:

  • 409 Conflict
  • иногда 422 для доменных правил
  • Это стыкуется с тем, что мы обсуждали в статье про стабильный формат ошибок и идемпотентность.

    Домен и БД: кто за что отвечает

    Рабочая модель ответственности:

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

    Что дальше по курсу

    Дальше из темы данных логично вырастают «производственные» темы архитектуры:

  • идемпотентность и повторяемость операций под нагрузкой
  • outbox и гарантированная публикация событий из транзакции
  • очереди, саги и согласованность между модулями/сервисами
  • кэширование и чтение под нагрузкой
  • Если резюмировать эту статью в одном принципе:

  • сценарий должен быть корректен при повторах, сбоях и конкуренции, а не только в happy path
  • 5. Интеграции и асинхронность: очереди, события, саги и идемпотентность

    Интеграции и асинхронность: очереди, события, саги и идемпотентность

    Backend почти никогда не живёт в вакууме. Он интегрируется с платёжками, уведомлениями, поиском, аналитикой, другими сервисами и внутренними модулями. На этом этапе «просто вызвать API» начинает ломаться об реальность:

  • внешние системы падают или отвечают медленно
  • сеть ненадёжна, запросы повторяются
  • под нагрузкой синхронные цепочки становятся узким местом
  • между модулями появляются операции, которые нельзя «завернуть в одну транзакцию БД»
  • В прошлых статьях мы зафиксировали фундамент:

  • бизнес-правила и инварианты живут в домене и use cases
  • API слой тонкий и не содержит правил
  • транзакции и конкурентность нужно проектировать осознанно
  • Теперь добавим ещё один слой зрелости: асинхронные интеграции и распределённые сценарии. Здесь появятся четыре ключевые идеи:

  • очереди и брокеры сообщений
  • события и команды
  • идемпотентность как обязательная гарантия
  • саги как замена «распределённой транзакции»
  • !Схема надёжной публикации событий через outbox и идемпотентного потребителя

    Синхронные и асинхронные интеграции

    Синхронная интеграция это когда ваш сценарий блокируется, пока не ответит внешняя система.

  • пример: POST /orders/{id}/pay сразу вызывает платёжный шлюз
  • плюс: проще, результат известен сразу
  • минусы: зависимость от доступности и латентности внешней системы, сложнее выдерживать пики нагрузки
  • Асинхронная интеграция это когда вы фиксируете намерение, а выполнение происходит позже.

  • пример: вы создаёте команду «провести оплату», публикуете её в очередь, а воркер обрабатывает
  • плюс: выравнивание нагрузки, устойчивость к временным сбоям
  • минусы: сложнее UX и модель статусов, нужно проектировать повторы и консистентность
  • Практическое правило:

  • если пользователю нужен ответ прямо сейчас и операция быстрая, синхронность допустима
  • если операция может быть долгой, зависит от внешних систем или должна переживать пики, вам почти неизбежно понадобится асинхронность
  • Базовые термины: сообщение, команда, событие

    Чтобы не путаться, договоримся о смыслах.

  • Сообщение это общий термин: полезная нагрузка плюс метаданные, которую вы отправляете через брокер.
  • Команда это просьба выполнить действие.
  • - пример: ChargePayment - важное свойство: у команды есть ожидаемый обработчик
  • Событие это факт, который уже произошёл.
  • - пример: PaymentSucceeded, OrderPaid - важное свойство: у события может быть много подписчиков, и издатель не должен знать кто они

    Это напрямую связано с границами модулей из Clean Architecture:

  • use case формирует команду или событие
  • инфраструктура доставляет сообщение (брокер, ретраи, DLQ)
  • потребитель сообщения должен быть тонким адаптером, который вызывает use case
  • Полезные справки по брокерам:

  • RabbitMQ Documentation
  • Apache Kafka Documentation
  • NATS Documentation
  • Amazon SQS Developer Guide
  • Семантика доставки: почему «ровно один раз» почти миф

    На уровне сети и брокеров у вас почти всегда будет одна из практических семантик.

  • At-most-once (не более одного раза)
  • - сообщение может потеряться - зато дублей почти нет
  • At-least-once (как минимум один раз)
  • - сообщение будет доставлено, но возможны дубли - это самая распространённая модель в продакшене
  • Exactly-once (ровно один раз)
  • - возможно только при жёстких условиях и специальных механизмах, и всё равно часто сводится к идемпотентности на стороне обработки

    Архитектурный вывод:

  • проектируйте систему так, будто доставка at-least-once
  • значит, обработчики обязаны быть идемпотентными
  • Идемпотентность: как переживать повторы без двойных эффектов

    Идемпотентная операция это операция, которую можно повторить, и итоговый эффект не изменится.

    Повторы возникают постоянно:

  • клиент повторил запрос из-за таймаута
  • балансировщик повторил запрос
  • consumer перезапустился и прочитал сообщение ещё раз
  • брокер доставил сообщение повторно
  • Где нужна идемпотентность

  • на уровне HTTP API для операций «создать», «оплатить», «списать»
  • на уровне consumer-обработчиков очереди
  • на уровне публикации событий (чтобы событие не «потерялось» между БД и брокером)
  • Типовые техники идемпотентности

  • Idempotency-Key от клиента
  • таблица дедупликации или уникальный индекс
  • идемпотентный потребитель: «если уже обработали messageId, то выходим без побочных эффектов»
  • проектирование команд как отдельных сущностей со статусом (команда имеет commandId)
  • Минимальный шаблон идемпотентного consumer

    Идея: перед выполнением побочных эффектов вы пытаетесь записать messageId в хранилище с уникальностью. Если запись не прошла, значит сообщение уже обработано.

    Ключевые моменты:

  • проверка и фиксация messageId должны быть в транзакции с критическими изменениями
  • дедупликация это инфраструктурная гарантия, но логика сценария остаётся в use case
  • Проблема «БД обновили, событие не отправили»: Transactional Outbox

    Одна из самых дорогих ошибок в интеграциях:

  • вы в транзакции обновили БД (например, заказ стал PAID)
  • после коммита попытались опубликовать событие в брокер
  • процесс упал между пунктами 2 и 3
  • в БД изменения есть, а подписчики никогда не узнают
  • Решение: Transactional Outbox.

    Идея:

  • в той же транзакции, где вы меняете бизнес-данные, вы записываете событие в таблицу outbox
  • отдельный процесс (паблишер) читает outbox и публикует в брокер
  • после успешной публикации помечает запись как отправленную
  • Описание паттерна:

  • Pattern: Transactional Outbox
  • Пример структуры outbox

    Use case пишет бизнес-данные и outbox в одной транзакции

    Паблишер outbox публикует и отмечает отправку

    Архитектурные последствия:

  • публикация становится повторяемой и переживает падения
  • подписчики должны быть готовы к дублям, потому что возможны повторные публикации
  • Очереди и топики: как выбрать форму коммуникации

    Практически вы будете использовать один из двух стилей.

  • Очередь (queue)
  • - сообщение должен обработать один consumer из группы - хорошо для фоновых задач и команд
  • Топик (pub/sub)
  • - одно событие получают многие подписчики - хорошо для доменных событий и интеграций

    Важно понимать: многие системы поддерживают оба подхода, но с разной терминологией.

  • RabbitMQ: очереди, exchanges, routing keys
  • Kafka: топики, consumer groups, partitions
  • SQS: очереди, для pub/sub обычно подключают SNS
  • Ретраи, backoff и DLQ: как делать обработку устойчивой

    Если consumer упал на сообщении, вы почти всегда хотите повтор.

    Но «повторить сразу и бесконечно» создаёт проблемы:

  • вы увеличиваете нагрузку на сломанную внешнюю систему
  • вы зацикливаете обработку на одном «ядовитом» сообщении
  • Типовая стратегия:

  • ограниченное число ретраев
  • задержка между попытками (обычно exponential backoff)
  • после исчерпания попыток сообщение уходит в DLQ (dead-letter queue)
  • отдельный процесс/панель для диагностики и переобработки DLQ
  • Проверочный список для ретраев:

  • какие ошибки ретраим (таймауты, 503) и какие нет (валидация, нарушение инварианта)
  • сколько попыток
  • какая задержка
  • как наблюдать DLQ и что делать дальше
  • Саги: как связывать шаги между модулями без распределённой транзакции

    Когда один бизнес-сценарий затрагивает несколько контекстов, возникает вопрос: как сохранить согласованность.

    Пример сценария «оформить заказ» может включать:

  • Orders: создать заказ
  • Payments: списать деньги
  • Inventory: зарезервировать товары
  • Delivery: создать доставку
  • Сделать «одну транзакцию на всё» невозможно, если это разные БД или разные сервисы. Сага это подход, где вы строите процесс как последовательность шагов с обработкой частичных успехов.

    Хорошее введение:

  • Saga (distributed transactions pattern)
  • Два стиля саг

  • Хореография
  • - каждый модуль реагирует на события других - нет центрального координатора - плюсы: слабая связность, проще масштабировать команды - минусы: сложнее понимать поток целиком, риск «спагетти из событий»

  • Оркестрация
  • - есть координатор (process manager), который отправляет команды и ждёт ответы - плюсы: поток явно описан в одном месте - минусы: появляется центральный компонент, его нужно хорошо проектировать

    Компенсации: что делать при частичном успехе

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

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

    Минимальный пример оркестрации на событиях

    Упрощённая логика процесса:

  • Orders публикует OrderCreated
  • Saga Coordinator отправляет команду ReserveInventory
  • Inventory отвечает событием InventoryReserved или InventoryReserveFailed
  • Если InventoryReserved, координатор отправляет ChargePayment
  • Payments отвечает PaymentSucceeded или PaymentFailed
  • Если PaymentFailed, координатор отправляет ReleaseInventory
  • Если PaymentSucceeded, координатор отправляет MarkOrderPaid
  • Архитектурные правила для саг:

  • шаги должны быть идемпотентны
  • сообщения должны иметь корреляцию: sagaId или orderId
  • состояние саги хранится явно (таблица состояния process manager)
  • таймауты и повторы проектируются так же тщательно, как и happy path
  • !Схема оркестрации саги с компенсацией

    Как это укладывается в Clean Architecture и модульность

    Чтобы асинхронность не разрушила границы, держите дисциплину слоёв.

  • consumer очереди это адаптер, как и HTTP-контроллер
  • адаптер делает минимум: распарсить сообщение, валидация формата, вызвать use case
  • use case открывает транзакцию, обеспечивает идемпотентность, пишет outbox
  • инфраструктура содержит конкретный брокер, ретраи, DLQ, планировщик outbox publisher
  • Практичная структура в терминах модульного монолита:

    Типичные ошибки при интеграциях

  • «Событие отправили напрямую из use case в брокер» без outbox и без повторяемости
  • «Consumer просто делает действие» без идемпотентности и дедупликации
  • «Ретраим всё подряд» включая ошибки доменных правил
  • «Сага без состояния» где вы не можете восстановить процесс после рестарта
  • «События как RPC» когда вы ждёте синхронный ответ через брокер вместо проектирования статусов
  • Что дальше по курсу

    Следующий шаг после интеграций это эксплуатационные и высоконагруженные аспекты:

  • кэширование и масштабирование чтения
  • rate limiting и защита от перегрузки
  • деградация и circuit breaker
  • наблюдаемость: метрики, трассировка, корреляция запросов
  • Но фундамент уже здесь:

  • если вы приняли модель at-least-once, сделали идемпотентность и outbox, вы резко повышаете надёжность системы
  • если вы умеете строить саги, вы можете проектировать сложные сценарии между модулями без опасных «распределённых транзакций»
  • 6. Производительность и масштабирование: кеши, шардирование, rate limit

    Производительность и масштабирование: кеши, шардирование, rate limit

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

    В прошлых статьях курса мы построили фундамент:

  • требования, доменная модель, use cases и инварианты
  • слои и границы (Clean Architecture, модульность)
  • API как контракт
  • транзакции, конкурентность и миграции
  • асинхронные интеграции, идемпотентность, outbox и саги
  • Теперь логичный следующий шаг: как сделать систему быстрой, устойчивой и способной расти. В этой статье разберём три ключевых инструмента, которые используются почти в любом серьёзном backend:

  • кеширование
  • масштабирование данных через партиционирование и шардирование
  • rate limiting и защита от перегрузки
  • !Общая карта, где именно в архитектуре обычно живут кеши, реплики, шардирование и rate limit

    Как думать о производительности, чтобы не оптимизировать вслепую

    Производительность почти всегда упирается в одно из трёх:

  • латентность (время ответа)
  • пропускная способность (сколько запросов в секунду вы выдерживаете)
  • насыщение ресурсов (CPU, память, соединения с БД, диски, сеть)
  • Практическое правило архитектуры:

  • оптимизировать стоит то, что является узким местом при целевом сценарии нагрузки
  • Чтобы это было не гадание, вам нужны измерения.

    Минимальный набор метрик

  • Latency по перцентилям (например, p50, p95, p99)
  • RPS и количество активных запросов
  • Error rate
  • Saturation
  • Для Node.js полезно отдельно смотреть лаг event loop
  • Для стандартизации наблюдаемости часто используют OpenTelemetry: OpenTelemetry

    Без метрик кеширование и шардирование легко превращаются в усложнение без эффекта.

    Кеширование

    Кеширование почти всегда даёт самый дешёвый выигрыш по производительности, потому что:

  • сокращает обращения к БД и внешним сервисам
  • уменьшает среднюю и хвостовую латентность
  • повышает устойчивость при пиках
  • Но кеш ломает консистентность, если его проектировать как «поставим Redis и станет быстрее».

    Где кеш живёт в слоях архитектуры

    С точки зрения Clean Architecture:

  • домен не должен знать о кешах
  • use case может принимать решения когда кеш использовать (например, читать через read model)
  • конкретная реализация кеша почти всегда находится в infrastructure
  • HTTP-адаптер может использовать HTTP-кеширование как часть контракта
  • Виды кешей

    | Вид кеша | Где находится | Когда полезен | Риски | |---|---|---|---| | In-memory кеш процесса | внутри Node.js инстанса | микрооптимизации, очень горячие данные | не общий между инстансами, сбрасывается при рестарте | | Distributed кеш (Redis) | отдельный сервис | общий кеш для всех инстансов, rate limit, сессии | сетевые задержки, консистентность, «эффект стада» | | HTTP кеш (браузер, CDN) | на границе системы | статические и редко меняющиеся ответы | сложность инвалидации |

    Про HTTP-кеширование как стандартную механику протокола см. RFC 9111: HTTP Caching.

    Базовые паттерны кеширования

    #### Cache-aside

    Приложение само управляет кешом:

  • прочитать из кеша
  • если промах, прочитать из БД
  • положить в кеш
  • Плюсы:

  • простой
  • кеш можно отключить без переписывания логики
  • Минусы:

  • нужно решить вопросы инвалидации
  • #### Read-through

    Кеш выступает как «умный слой», который сам загружает данные при промахе. На практике в Node.js чаще реализуют как обёртку вокруг репозитория.

    #### Write-through

    Запись идёт сразу и в БД, и в кеш.

    Плюсы:

  • меньше устаревших данных
  • Минусы:

  • сложнее обеспечить корректность при ошибках (что делать, если БД записалась, а кеш нет)
  • #### Write-behind

    Сначала пишем в кеш, потом асинхронно сбрасываем в БД.

    Плюсы:

  • высокая скорость записи
  • Минусы:

  • повышенные риски потери данных
  • Для бизнес-критичных систем write-behind обычно применяют только при очень осознанном дизайне.

    Инвалидация: главный источник багов

    Кеширование чтений «ломает реальность» так: вы обновили данные в БД, но клиент продолжает видеть старое значение.

    Есть три типовых стратегии.

  • TTL
  • Invalidate on write
  • Event-driven инвалидация
  • #### TTL

    Вы кладёте значение в кеш с временем жизни.

    Плюсы:

  • проще всего
  • Минусы:

  • данные могут быть устаревшими до истечения TTL
  • #### Invalidate on write

    После изменения данных вы удаляете или обновляете соответствующие кеш-ключи.

    Плюсы:

  • более свежие данные
  • Минусы:

  • нужно точно знать, какие ключи зависят от каких данных
  • #### Event-driven инвалидация

    Вы публикуете событие изменения (часто через outbox из прошлой статьи), а отдельные компоненты сбрасывают нужные кеши.

    Плюсы:

  • хорошо ложится на модульность и интеграции
  • Минусы:

  • нужна дисциплина событий и обработка дублей
  • Эффект стада: cache stampede

    Проблема:

  • горячий ключ истёк
  • тысяча запросов одновременно промахнулась
  • все пошли в БД
  • Типовые меры защиты:

  • TTL jitter
  • single flight или лок на ключ
  • stale-while-revalidate
  • TTL jitter это когда вы добавляете случайность к TTL, чтобы ключи не истекали одновременно.

    Single flight это когда только один запрос делает «дорогую загрузку», а остальные ждут.

    Stale-while-revalidate это когда вы временно отдаёте чуть устаревшее значение, параллельно обновляя кеш.

    Пример cache-aside с Redis в Node.js

    Пример показывает идею на уровне инфраструктуры. В реальном проекте это обычно обёртка вокруг read-репозитория.

    Архитектурные замечания:

  • Это должно быть максимально дешёвой операцией, иначе rate limit сам станет узким местом
  • Если вы используете Redis, убедитесь, что операции атомарны для вашего алгоритма
  • Для сложных алгоритмов (token bucket) часто используют Lua-скрипты в Redis
  • Rate limit не решает всё: нужна деградация

    Если зависимость деградирует (например, внешняя платёжка отвечает 10 секунд), то даже при rate limit вы можете «забить» пул соединений и воркеры.

    Две практики, которые хорошо сочетаются с темами курса:

  • Таймауты и ограничение параллелизма для внешних вызовов
  • Асинхронные команды через очередь, когда операция может быть долгой
  • Это напрямую связано со статьёй про асинхронность и саги: многие high-load операции лучше превращать в «создать команду и отслеживать статус», чем держать HTTP-соединение.

    Горизонтальное масштабирование Node.js: важные ограничения

    Node.js по умолчанию исполняет JavaScript в одном потоке на процесс. Поэтому под CPU-нагрузкой чаще всего масштабируются так:

  • несколько процессов (например, cluster) или несколько контейнеров/подов
  • балансировщик распределяет трафик
  • приложение остаётся stateless
  • Про кластеризацию Node.js: Node.js: Cluster

    Stateless означает:

  • не хранить состояние сессии в памяти процесса
  • хранить сессии/токены/кеши в внешних хранилищах (Redis, БД)
  • Иначе при добавлении инстансов вы получите непредсказуемое поведение.

    Практический план внедрения: что делать в каком порядке

  • Ввести метрики и трассировку для ключевых сценариев
  • Найти топ-эндпоинты по нагрузке и стоимости
  • Добавить защиту от перегрузки
  • Добавить кеширование чтений там, где допустима небольшая устарелость
  • Вынести тяжёлые сценарии в асинхронную обработку, если они зависят от внешних систем
  • Использовать read replicas и read-модели для масштабирования чтений
  • Рассматривать партиционирование как шаг до шардирования
  • Переходить к шардированию только при ясной необходимости и готовности принять ограничения на транзакции
  • Смысл этой статьи в одном принципе:

  • производительность в high-load backend это не «ускорить код», а правильно спроектировать границы, данные и защиту системы под нагрузкой
  • 7. Надежность в проде: безопасность, тесты, наблюдаемость и деплой

    Надежность в проде: безопасность, тесты, наблюдаемость и деплой

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

    В предыдущих статьях курса мы построили основу архитектуры:

  • требования → use cases → доменная модель → инварианты
  • слои и границы (Clean Architecture, DDD, модульность)
  • контракты API и версии
  • транзакции, конкурентность, миграции
  • очереди, события, outbox, саги, идемпотентность
  • кеши, масштабирование, rate limit
  • Теперь добавляем производственную дисциплину: как сделать так, чтобы архитектура выживала в эксплуатации.

    !Четыре опоры надёжности вокруг бизнес-логики

    Как мыслить о надёжности

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

  • Корректность: инварианты не нарушаются даже при повторах, сбоях и конкуренции.
  • Устойчивость: система деградирует контролируемо, а не падает каскадом.
  • Наблюдаемость: вы быстро понимаете, что сломалось и почему.
  • Безопасные изменения: вы можете деплоить часто, не боясь продакшена.
  • Практическая связка с тем, что уже было в курсе:

  • Инварианты и транзакции дают корректность.
  • Идемпотентность, outbox и ретраи дают устойчивость к сбоям.
  • Кеши, лимиты и асинхронность помогают переживать нагрузку.
  • А безопасность, тесты, наблюдаемость и деплой — делают всё это управляемым в реальной эксплуатации.
  • Безопасность: защищаем границы и данные

    Безопасность в backend — это не только про аутентификацию. Это про контроль границ: кто может сделать что, с какими данными, и что будет зафиксировано в системе.

    Модель угроз: минимум, который стоит сделать всегда

    Модель угроз — это короткий список того, что может пойти не так, и какие меры вы принимаете.

  • Какие данные считаются чувствительными: пароли, токены, платежные идентификаторы, персональные данные.
  • Какие роли и права есть: пользователь, админ, сервисный аккаунт.
  • Какие внешние поверхности атаки: публичное API, вебхуки, админка, очереди, файлы.
  • Какие типовые атаки применимы: перебор, инъекции, SSRF, утечки секретов.
  • Хороший ориентир для списка типовых классов уязвимостей: OWASP Top 10.

    Аутентификация и авторизация

    Определения, чтобы не путаться:

  • Аутентификация: кто ты? (проверка личности, например JWT).
  • Авторизация: что тебе можно? (проверка прав на действие).
  • Архитектурное правило из логики слоёв:

  • В API-адаптере вы проверяете наличие токена, формат, срок действия.
  • В use case вы проверяете права на сценарий, потому что права зависят от доменных правил.
  • Пример ошибки архитектуры:

  • Если вы «размажете» правила доступа по контроллерам, вы почти неизбежно получите разные проверки для разных интерфейсов (HTTP, consumer очереди, админские джобы).
  • Валидация входа: формат и смысл

    Из статьи про API важно помнить разделение:

  • Валидация формата на границе: типы, обязательные поля, длины.
  • Валидация инвариантов в домене: «нельзя оплатить оплаченный заказ».
  • Для формата используйте JSON Schema или валидаторы DTO, но не смешивайте это с доменными правилами.

    Инъекции: SQL, NoSQL и не только

    Базовые принципы защиты:

  • Всегда используйте параметризованные запросы в SQL.
  • Не собирайте запросы строковой конкатенацией.
  • Проверяйте и ограничивайте входные фильтры и сортировки в списках.
  • Полезный раздел: OWASP SQL Injection.

    SSRF и опасные интеграции

    SSRF — это атака, когда злоумышленник заставляет ваш backend делать HTTP-запросы туда, куда нельзя, например во внутреннюю сеть.

    Меры:

  • Не принимайте произвольные URL от клиента.
  • Если нужно, используйте allowlist доменов и проверку DNS.
  • Явно задавайте таймауты и лимиты редиректов для исходящих запросов.
  • Ориентир: OWASP SSRF.

    Секреты и конфигурация

    Секреты — это не только пароли, но и:

  • ключи подписи JWT
  • ключи API внешних провайдеров
  • строки подключения к БД
  • Практические правила:

  • Не храните секреты в репозитории.
  • Передавайте секреты через секрет-хранилище или переменные окружения.
  • Разделяйте конфигурацию по средам.
  • Хорошая база по конфигурации в проде: The Twelve-Factor App.

    Безопасность зависимостей

    Node.js экосистема быстро меняется, и зависимости — частый источник риска.

    Минимальный набор мер:

  • регулярные обновления зависимостей
  • автоматические проверки уязвимостей в CI
  • запрет на «лишние» зависимости в критических модулях
  • Практические источники:

  • npm audit
  • OSV
  • Логи и персональные данные

    Логи — часть наблюдаемости, но это также и зона риска.

  • Не логируйте пароли, токены, содержимое платежных данных.
  • Маскируйте чувствительные поля.
  • Разделяйте бизнес-аудит (кто что сделал) и технические логи (ошибки, производительность).
  • Тестирование: страхуем архитектуру от регрессий

    Тесты в backend-архитектуре — это не «побольше тестов», а правильные тесты в правильных местах, которые поддерживают слои и границы.

    Полезная статья-ориентир: The Practical Test Pyramid.

    Уровни тестов и что они проверяют

    | Уровень | Что тестируем | Что НЕ тестируем | Ценность | |---|---|---|---| | Unit (домен) | инварианты, методы сущностей, value objects | БД, HTTP, очереди | максимальная скорость и точность | | Unit (use case) | оркестрация сценария, ветки ошибок, идемпотентность | реальные сети и базы | ловит архитектурные регрессии | | Integration | репозитории, транзакции, outbox, миграции | внешний мир целиком | ловит ошибки границ инфраструктуры | | Contract | соответствие API контракту, стабильность ошибок | бизнес-логику глубоко | защищает клиентов | | E2E | основной пользовательский поток | мелкие детали домена | уверенность на уровне продукта |

    Архитектурное правило:

  • чем ближе тест к домену и use cases, тем он должен быть дешевле и чаще запускаться
  • Как тесты поддерживают Clean Architecture

    Если границы проведены правильно, вы получаете естественную тестируемость:

  • домен тестируется без БД
  • use case тестируется с подменными портами
  • инфраструктура тестируется отдельным слоем интеграционных тестов
  • Пример подхода для use case теста: фейковый репозиторий и фейковый платежный шлюз.

    Контрактные тесты для API

    Контрактные тесты помогают не ломать клиентов. Они особенно важны, если:

  • есть мобильные клиенты
  • есть внешние партнёры
  • много команд работают параллельно
  • Практика:

  • держать OpenAPI рядом с кодом
  • в CI проверять, что ответы соответствуют контракту по формату и кодам ошибок
  • Интеграционные тесты для критичных инфраструктурных гарантий

    То, что часто ломается только на реальной БД:

  • транзакционные границы
  • блокировки и уровни изоляции
  • UNIQUE ограничения для идемпотентности
  • outbox: запись в одной транзакции
  • Интеграционный тест ценен, когда он проверяет инварианты на стыке приложения и БД.

    Наблюдаемость: логи, метрики, трассировка

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

    Обычно выделяют три опоры:

  • логи: события и контекст
  • метрики: числа во времени
  • трейсы: путь запроса через компоненты
  • Стандарт индустрии для телеметрии: OpenTelemetry.

    !Как один запрос превращается в логи, метрики и трейс

    Корреляция: один идентификатор на весь запрос

    Критическая практика для продакшена: correlation id.

  • Генерируете requestId на входе.
  • Прокидываете его во все логи.
  • Если есть асинхронность, прокидываете requestId или traceId в сообщения.
  • Пример простого middleware для Express:

    Структурированные логи вместо текста

    Почему это важно:

  • в проде вы фильтруете и агрегируете логи машиной
  • вам нужно быстро находить все ошибки по orderId, userId, requestId
  • Практика:

  • логировать JSON
  • всегда добавлять контекст
  • не логировать чувствительные данные
  • Метрики, которые нужны почти всем

    Минимальный набор для HTTP API:

  • количество запросов
  • распределение латентности по перцентилям
  • доля ошибок по кодам
  • насыщение: CPU, память, пул соединений к БД
  • Для Node.js полезно отдельно наблюдать:

  • задержку event loop
  • количество активных соединений
  • Трейсы: когда без них почти невозможно

    Распределённые трейсы особенно полезны, когда:

  • запрос проходит через несколько модулей
  • есть внешние интеграции
  • есть очереди и воркеры
  • Трейс помогает ответить на вопрос: где именно ушло время.

    Health checks и готовность к трафику

    Для деплоя и автоскейлинга вам нужны проверки состояния.

    Определения:

  • Liveness: процесс жив или завис и его нужно перезапустить.
  • Readiness: процесс готов принимать трафик, потому что зависимости доступны.
  • Для Kubernetes это стандартные концепции: Kubernetes Probes.

    Деплой и эксплуатация: делаем изменения безопасно

    Если архитектура строится вокруг изменений, то деплой — это управляемый механизм доставки изменений.

    CI/CD пайплайн как часть архитектуры

    Типовой пайплайн для backend:

  • линт и статические проверки
  • unit тесты домена и use cases
  • интеграционные тесты репозиториев и миграций
  • сборка артефакта
  • скан уязвимостей зависимостей
  • деплой
  • Важно:

  • чем раньше вы ловите проблему, тем дешевле её исправить
  • Миграции без даунтайма

    Из статьи про данные и консистентность ключевая практика для прода:

  • expand/contract миграции
  • Связь с деплоем:

  • при rolling deploy у вас одновременно работают несколько версий приложения
  • значит, схема БД должна быть совместима на период раскатки
  • Стратегии деплоя

    | Стратегия | Идея | Плюсы | Минусы | |---|---|---|---| | Rolling update | постепенная замена инстансов | просто, стандартно | сложнее быстрый откат | | Blue/Green | две среды, переключение трафика | быстрый откат | дороже по ресурсам | | Canary | малый процент трафика на новую версию | снижает риск | сложнее наблюдение и маршрутизация |

    Выбор зависит от зрелости инфраструктуры, но общий принцип один:

  • деплой должен быть обратимым
  • Graceful shutdown в Node.js

    При деплое и автоскейлинге инстанс могут остановить. Если вы оборвёте процесс резко:

  • запросы оборвутся
  • возможны частичные эффекты
  • consumer может оставить сообщение «в обработке»
  • Базовый шаблон:

    Архитектурная связь с асинхронностью:

  • воркеры должны корректно завершать обработку сообщения
  • повторная доставка неизбежна, поэтому идемпотентность остаётся обязательной
  • Feature flags и безопасные релизы

    Feature flag — переключатель, который позволяет включать функциональность без полного отката версии.

    Зачем это нужно:

  • вы снижаете риск релиза
  • вы можете включать функциональность по группам пользователей
  • вы можете быстро отключить проблемный путь
  • Но важно:

  • feature flags не должны ломать инварианты и консистентность
  • Как собрать всё в один практический чеклист

    Ниже минимальный набор, который стоит внедрять в реальном проекте, даже небольшом.

  • Единый формат ошибок и стабильные code для доменных нарушений.
  • Валидация формата входа на границе и инварианты в домене.
  • Идемпотентность для команд и consumer-обработчиков.
  • Транзакции в application layer и ограничения в БД как вторая линия защиты.
  • Структурные логи с requestId и аккуратное обращение с PII.
  • Метрики латентности и ошибок, алерты на деградации.
  • Трейсы для сложных цепочек и внешних интеграций.
  • Миграции expand/contract и деплой без даунтайма.
  • Graceful shutdown для API и воркеров.
  • Что дальше по курсу

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

    Дальше обычно идут углубления под конкретные цели:

  • детальная безопасность: threat modeling, аудит, криптография, управление ключами
  • наблюдаемость на уровне SLO и инцидентов
  • продвинутые практики деплоя и релизов в Kubernetes
  • оптимизация БД и архитектурные изменения под экстремальную нагрузку