21. System Design. База

«Хорошая архитектура признает несовершенство реального мира и проектируется с расчетом на неизбежные сбои».
— Мартин Клеппман


Архитектурный компромисс: CAP-теорема на практике

В любой распределенной системе при возникновении сетевого разделения (Network Partition, P) архитектор обязан выбрать между строгой согласованностью (Consistency, C) и непрерывной доступностью (Availability, A):

flowchart TD
    classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc

    CAP["Сетевое разделение неизбежно (Partition Tolerance — факт)"]:::entry
    CAP --> CP["Выбор CP: Consistency + Partition Tolerance"]:::accent
    CAP --> AP["Выбор AP: Availability + Partition Tolerance"]:::warning

    subgraph CPSystems["CP Архитектура (Консенсус Raft/Paxos)"]
        CPDesc["Отказ отвечать на запросы изолированного меньшинства узлов"]:::process
        CPEx["Примеры: etcd, Consul, ZooKeeper, CockroachDB"]:::success
    end

    subgraph APSystems["AP Архитектура (Eventual Consistency)"]
        APDesc["Все узлы принимают чтения/записи, данные синхронизируются позже"]:::process
        APEx["Примеры: Apache Cassandra, AWS DynamoDB, ScyllaDB"]:::success
    end

    CP --> CPSystems
    AP --> APSystems
  

Консистентное хеширование (Consistent Hashing с Virtual Nodes)

Консистентное хеширование минимизирует перемещение ключей при добавлении или падении узлов кэша/хранилища (перемещается лишь $1/N$ часть ключей, а не 100%):

flowchart TD
    classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc

    Key["Входящий ключ: hash(userID)"]:::entry --> Ring["Кольцо хешей 0 .. 2^64-1"]:::accent
    Ring --> Node1["Virtual Node: Node A (v1, v2, v3)"]:::process
    Ring --> Node2["Virtual Node: Node B (v1, v2, v3)"]:::process
    Ring --> Node3["Virtual Node: Node C (v1, v2, v3)"]:::process
    Node1 --> Target["Попадание в ближайший узел по часовой стрелке"]:::success
  

Архитектуры алгоритмов Rate Limiting

flowchart LR
    classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc

    subgraph TB["Token Bucket (golang.org/x/time/rate)"]
        T1["Пополнение ведра токенами с постоянной скоростью R"]:::process
        T2["Разрешает кратковременные пиковые всплески до Burst емкости"]:::success
    end

    subgraph LB["Leaky Bucket"]
        L1["Очередь запросов фиксированного размера FIFO"]:::process
        L2["Обработка строго с равномерной скоростью без всплесков"]:::accent
    end

    subgraph SW["Sliding Window Counter"]
        S1["Скользящее окно времени в Redis (ZADD + ZREMRANGEBYSCORE)"]:::process
        S2["Абсолютная точность лимита без граничных скачков"]:::warning
    end
  

  1. Что такое масштабирование?
  2. Вертикальное vs горизонтальное масштабирование?
  3. Что такое балансировщик нагрузки (load balancer)?
  4. Что такое кэш (cache)?
  5. Что такое CDN?
  6. Что такое микросервисы vs монолит?
  7. Что такое REST API?
  8. Что такое gRPC?
  9. Что такое GraphQL?
  10. Как проектировать URL для REST API?
  11. Что такое API Gateway?
  12. Что такое Service Discovery?
  13. Что такое Circuit Breaker?
  14. Что такое Rate Limiter?
  15. Какие алгоритмы Rate Limiting существуют (Token Bucket, Leaky Bucket)?
  16. Что такое идемпотентность?
  17. Как спроектировать идемпотентный API?
  18. Как обрабатывать распределенные транзакции?
  19. Что такое CQRS?
  20. Что такое Event Sourcing?
  21. Что такое Backend for Frontend (BFF)?
  22. Что такое Webhook?
  23. Что такое Long Polling vs WebSocket vs SSE?
  24. Как спроектировать систему уведомлений?
  25. Как спроектировать сервис сокращения URL (URL shortener)?
  26. Как спроектировать чат (chat)?
  27. Как спроектировать ленту новостей (news feed)?
  28. Как спроектировать систему лайков (like system)?
  29. Как спроектировать систему рейтингов (rating system)?
  30. Как спроектировать систему поиска (search)?
  31. Как спроектировать e-commerce платформу?
  32. Как спроектировать платежную систему?
  33. Что такое "non-functional requirements" в System Design?
  34. Как оценить нагрузку (QPS, Storage, Bandwidth)?
  35. Какие основные trade-off'ы при выборе БД?
  36. Как спроектировать "idempotency key" механизм?
  37. Как работает "consistent hashing" и где применяется?
  38. Что такое "gossip protocol"?
  39. Как работает "leader election"?
  40. Что такое "heartbeat" в распределенной системе?
  41. Как спроектировать систему "единого входа" (SSO)?
  42. Как работает OAuth 2.0 и OpenID Connect?
  43. Как спроектировать "content delivery network" (CDN)?
  44. Что такое "bloom filter" и как он применяется в кэшировании?
  45. Как спроектировать "key-value store"?
  46. Что такое "SLA", "SLO", "SLI"?
  47. Как работает "graceful degradation" и "fail-fast" на уровне системы?
  48. Как спроектировать "database per service" паттерн в микросервисах?

Что такое масштабирование?

Краткий ответ (30–60 секунд)

Масштабирование — это способность системы справляться с ростом нагрузки (запросов, данных, пользователей) без потери производительности и доступности. Оно достигается за счёт добавления ресурсов: либо наращиванием мощности одного узла (вертикальное), либо увеличением количества узлов (горизонтальное). Цель — сохранить или улучшить пропускную способность и время отклика под растущей нагрузкой.

Развёрнутый ответ

Масштабирование охватывает не только hardware, но и архитектуру ПО, базы данных, сеть. Система, которая масштабируется линейно, удваивает пропускную способность при удвоении ресурсов. На практике это недостижимый идеал из-за координационных накладных расходов (CAP-теорема, сериализация, сетевые задержки). Различают два фундаментальных направления:

  • Вертикальное (scale-up): увеличение CPU, RAM, дисков на одном сервере. Просто, но имеет физический предел и единую точку отказа.
  • Горизонтальное (scale-out): добавление большего количества машин. Требует распределённой архитектуры, балансировки нагрузки, партиционирования данных, управления состоянием.

Современные облачные системы строятся преимущественно на горизонтальном масштабировании, реализуемом через микросервисы, шардирование, репликацию и оркестрацию (Kubernetes). Ключевые метрики: масштабируемость по нагрузке, по данным, географическая масштабируемость.

Под капотом (internals)

Горизонтальное масштабирование в распределённых системах неизбежно сталкивается с проблемами консистентности и сетевыми разделениями. При шардировании БД используется consistent hashing (см. вопрос 37) для минимизации перемещения данных. В Go идиоматично создавать конкурентные воркеры с помощью горутин и каналов, а масштабирование сервиса часто сводится к запуску дополнительных экземпляров за балансировщиком. Планировщик Go (GMP-модель) уже обеспечивает эффективное использование многоядерных процессоров на одном узле, но для распределённого масштабирования необходимы внешние механизмы: service discovery, RPC, очереди сообщений.

Типичные ошибки кандидатов

  1. Путают масштабирование с производительностью
    Производительность — это время отклика при фиксированной нагрузке, масштабирование — поведение при росте нагрузки. Кандидат должен чётко разделять: система может быть быстрой, но не масштабируемой (например, единый lock в базе). Исправляют архитектуру, убирая централизованные узкие места.
  2. Предполагают идеальную линейность
    Забывают про закон Амдала и координационные издержки. Объясняя проект, нужно всегда указывать пределы масштабируемости выбранного решения (например, throughput Kafka против RPC).
  3. Игнорируют стоимость и сложность горизонтального масштабирования
    Распределённые системы сложнее в отладке, требуют мониторинга, транзакций, согласованности. Кандидат должен уметь обосновать, когда достаточно вертикального масштабирования.

Возможные дополнительные вопросы интервьюера

  • Как измерить масштабируемость системы?
    Используют коэффициент масштабирования λ = T(2n)/T(n), где T — пропускная способность при n ресурсах. Также анализируют latency distribution (p50, p99) под увеличивающейся нагрузкой.
  • Можно ли бесконечно масштабировать базу данных вертикально?
    Нет, упор в физические ограничения процессора и памяти, а также в стоимость; кроме того, единая точка отказа остаётся. На определённом этапе выполняют шардирование.
  • Как влияет CAP-теорема на масштабирование?
    Горизонтально масштабируемая система при сетевом разделении должна выбирать между консистентностью и доступностью, что определяет выбор AP или CP решений.

Вертикальное vs горизонтальное масштабирование?

Краткий ответ (30–60 секунд)

Вертикальное масштабирование (scale-up) — увеличение мощности существующего сервера (CPU, RAM). Горизонтальное (scale-out) — добавление новых серверов, работающих параллельно. Вертикальное проще в реализации, но имеет потолок и единую точку отказа. Горизонтальное практически не ограничено, но требует распределённой архитектуры, балансировки нагрузки и решения проблем согласованности.

Развёрнутый ответ

Вертикальный подход подходит для монолитных систем и баз данных, которые сложно распределить. Он требует меньше изменений в коде и позволяет быстрее получить прирост производительности. Однако рано или поздно упирается в физические лимиты железа и стоимость top-tier серверов растёт экспоненциально. Горизонтальный подход — основа облачных и high-load систем: stateless-сервисы размножаются за балансировщиком, stateful-сервисы шардируются. Выбор между ними часто диктуется характером нагрузки: CPU-bound задачи иногда выгоднее вертикально масштабировать на машинах с GPU/FPGA, тогда как I/O-bound или веб-сервера идеально ложатся на горизонтальное масштабирование. В современном Go-микросервисном мире стандарт де-факто — горизонтальный scale-out в контейнерах.

Под капотом (internals)

При вертикальном масштабировании ОС и runtime Go видят больше ядер и памяти; планировщик горутин автоматически использует дополнительные CPU через механизм GOMAXPROCS. При горизонтальном — между экземплярами сервиса нет общей памяти, поэтому в Go нужно явно управлять состоянием через внешнее хранилище (Redis, БД), а также реализовать балансировку и обнаружение сервисов. Балансировщики работают на уровнях L4 (TCP) или L7 (HTTP), распределяя запросы по алгоритмам round-robin, least connections, consistent hashing.

Типичные ошибки кандидатов

  1. Считать, что вертикальное масштабирование всегда плохо
    Для многих бизнес-кейсов и стартапов достаточно вертикального масштабирования на ранних этапах; его дешевизна и простота недооценены. Ошибка — сразу проектировать распределённый монстр, не оценив реальную нагрузку.
  2. Не учитывать stateful-сервисы при горизонтальном масштабировании
    Просто запустить несколько реплик базы данных нельзя — требуется шардирование или репликация с разделением на чтение/запись. Кандидат должен предложить конкретную стратегию (master-slave, multi-master, partitioning).
  3. Игнорировать проблему sticky sessions
    При горизонтальном масштабировании веб-приложений, хранящих сессию локально, без sticky sessions на балансировщике юзер потеряет состояние. Исправляется выносом сессии в Redis или использованием JWT.

Возможные дополнительные вопросы интервьюера

  • Почему закон Амдала важен при вертикальном масштабировании?
    Он показывает теоретический предел ускорения при добавлении ядер, если часть кода остаётся последовательной; масштабирование упирается в последовательную долю.
  • Когда горизонтальное масштабирование может снизить производительность?
    При чрезмерном дроблении, когда накладные расходы на сетевые вызовы между сервисами превышают выгоду от параллелизма (chatty interfaces).
  • Как Kubernetes обеспечивает горизонтальное масштабирование?
    Horizontal Pod Autoscaler по метрикам CPU/memory/кастомным метрикам динамически изменяет количество реплик Deployment.

Что такое балансировщик нагрузки (load balancer)?

Краткий ответ (30–60 секунд)

Балансировщик нагрузки (LB) — это устройство или ПО, распределяющее входящий сетевой трафик между группой бэкенд-серверов. Он повышает доступность, отказоустойчивость и горизонтальную масштабируемость, скрывая за единым IP/VIP внутреннюю топологию. LB выполняет health-check'и и автоматически исключает упавшие узлы.

Развёрнутый ответ

Балансировщики работают на уровнях L4 (TCP/UDP, быстрее, но без анализа HTTP) или L7 (HTTP/HTTPS, могут маршрутизировать по URL, заголовкам, кукам). Основные алгоритмы: round-robin, least connections, least response time, IP-hash. В облачных средах LB часто предоставляется как услуга (AWS ALB/NLB, GCP Load Balancer), внутри кластеров Kubernetes — Ingress Controller и Service типа LoadBalancer. Программные реализации: HAProxy, Nginx, Envoy. В Go легко написать примитивный L7-балансировщик с использованием net/http/httputil.ReverseProxy, но в продакшене опираются на battle-tested решения.

Под капотом (internals)

L4 LB оперирует на уровне сокетов, модифицируя заголовки пакетов (NAT/DSR). L7 LB терминирует TLS, парсит HTTP-запрос, выбирает бэкенд и устанавливает новое TCP-соединение. Envoy, часто используемый в service mesh, реализован на C++ с асинхронной моделью; в мире Go существуют балансировщики вроде Traefik, которые используют net/http. Для минимальных задержек важна пул соединений и keep-alive. Некоторые балансировщики при health-check отсылают HTTP-запрос на /health с интервалом, используя настраиваемые таймауты и пороги.

Типичные ошибки кандидатов

  1. Путают балансировку с очередью
    LB не сглаживает пики за счёт буферизации — он сразу проксирует запросы. Для сглаживания пиков нужна очередь сообщений или rate limiter.
  2. Забывают про асимметричные бэкенды
    Если серверы имеют разную мощность, round-robin перегрузит слабые узлы; нужно least connections или взвешенные алгоритмы.
  3. Не настраивают health-check правильно
    Слишком частый health-check создаёт дополнительную нагрузку; недостаточный — долго не замечает падение. Нужно сочетать активные и пассивные проверки (circuit breaker на стороне LB).

Возможные дополнительные вопросы интервьюера

  • Как балансировщик влияет на сквозную задержку?
    Добавляет задержку на обработку (менее 1 мс для L4, до нескольких мс для L7 с TLS-termination). При большом RPS это превращается в существенные накладные расходы.
  • Что такое stickiness и когда оно нужно?
    Привязка сессии пользователя к одному бэкенду по cookie или IP; требуется, если состояние хранится локально. В распределённых системах это антипаттерн, лучше выносить состояние.
  • Как организовать балансировку gRPC?
    gRPC использует HTTP/2 с долгоживущими соединениями, поэтому L4 балансировка плохо распределяет нагрузку. Нужен L7-балансировщик с пониманием gRPC или client-side балансировка через сервис-дискавери.

Что такое кэш (cache)?

Краткий ответ (30–60 секунд)

Кэш — это временное высокоскоростное хранилище данных, к которым часто обращаются. Он размещается между источником данных (БД, диск) и потребителем, чтобы снизить задержки и нагрузку на источник. Ценой становится потенциальная несогласованность (stale data) и затраты памяти. Работает по принципу локальности ссылок: временной и пространственной.

Развёрнутый ответ

Кэши классифицируются по месту расположения: клиентский (браузер, HTTP-кэш), серверный (in-memory типа Redis/Memcached, локальный как sync.Map), проксирующий (CDN). Политика вытеснения: LRU, LFU, FIFO, TTL-based. В микросервисной архитектуре часто применяются cache-aside (приложение само управляет кэшем), read-through/write-through, write-behind. Важно решить проблему cache stampede (одновременная загрузка одного ключа) с помощью блокировок или вероятностного досрочного обновления. В Go для локального кэша существуют библиотеки go-cache, ristretto, freecache, учитывающие GC-давление.

Под капотом (internals)

Redis — однопоточный event-loop на C, структуры данных хранятся в памяти, для персистентности — RDB/AOF. Внутренне использует хеш-таблицы и skip-листы. In-memory кэши в Go обычно опираются на map с mutex или sync.Map для high-read сцены. ristretto реализует оптимальный LFU TinyLFU с политикой вытеснения для минимизации промахов, используя счётчики и окно. Кэш процессора — L1/L2/L3 — аппаратный пример, но принципы те же: cache line, ассоциативность, что находит отражение в оптимизации высоконагруженных Go-структур (выравнивание полей).

Типичные ошибки кандидатов

  1. Игнорирование инвалидации
    «Самый сложный момент в кэшировании — это инвалидация». Кандидат должен предложить стратегии: TTL, событийная инвалидация, версионирование.
  2. Кэширование всего подряд
    Бесконтрольный рост кэша приводит к вытеснению действительно нужных данных и давлению на GC. Нужно следить за hit ratio и размером.
  3. Отсутствие защиты от лавинного перезапроса (cache avalanche/stampede)
    Когда истекает срок популярного ключа, сотни горутин одновременно идут в БД. Исправляется блокировкой загрузки (singleflight в Go) или вероятностным обновлением.
  4. Не учитывают согласованность при write-behind
    Если пишем в кэш и асинхронно в БД, сбой вызовет потерю данных. Применяют только для некритичных данных или с журналом.

Возможные дополнительные вопросы интервьюера

  • Чем Redis отличается от Memcached?
    Redis — более богатые структуры, персистентность, кластеризация, Lua-скрипты. Memcached — простой key-value, многопоточный, без персистентности. Выбор под задачу.
  • Как работает singleflight в Go?
    Подавляет дублирующиеся вызовы, объединяя одновременные запросы к одному ключу в один фактический вызов, результат которого расшаривается всем ожидающим; решает stampede.
  • Что такое cache penetration и как с ним бороться?
    Запросы ключей, которых заведомо нет в источнике, приводят к промахам и нагрузке на БД. Решение — кэшировать факт отсутствия (negative cache) с коротким TTL или bloom-фильтр.

Что такое CDN?

Краткий ответ (30–60 секунд)

Content Delivery Network (CDN) — географически распределённая сеть прокси-серверов (PoP), которые кэшируют статический и динамический контент ближе к пользователю. Цели: снижение задержки, уменьшение нагрузки на origin-сервер, повышение доступности и защита от DDoS. CDN обслуживают веб-страницы, изображения, видео, API-ответы.

Развёрнутый ответ

CDN работает через DNS-перенаправление (anycast или GeoDNS), направляя пользователя в ближайшую точку присутствия. Edge-сервер кэширует контент согласно HTTP-заголовкам кэширования (Cache-Control, ETag). При промахе запрос идёт к origin, ответ кэшируется. Современные CDN (Cloudflare, Akamai, Fastly) также предоставляют edge-вычисления (WebAssembly, Cloudflare Workers), A/B-тестирование, защиту от ботов. Для динамического контента применяют «CDN acceleration» через оптимизацию маршрутов и постоянные соединения с origin. При проектировании системы важно решить, какой контент отдавать через CDN, и предусмотреть инвалидацию (purge) по API.

Под капотом (internals)

Архитектура CDN включает иерархию кэшей: edge -> regional -> origin. Используются алгоритмы склейки потоков, сегментация видео (HLS/DASH). Для динамического роутинга измеряют RTT и пропускную способность между edge и пользователем. В Go написать простейший edge-прокси можно с помощью net/http/httputil.ReverseProxy, кэшируя ответы в памяти. Массштабные CDN реализованы на C/C++ для максимальной производительности. Для инвалидации используется механизм «purge by tag» или «surrogate keys».

Типичные ошибки кандидатов

  1. Предполагать, что CDN решает все проблемы
    CDN ускоряет доставку контента, но не снижает нагрузку на базу данных динамического origin. Нужно по-прежнему кэшировать на сервере.
  2. Кэшировать некешируемое
    По умолчанию CDN может кэшировать API-ответы с сессионными данными, если не настроены правильные Cache-Control заголовки. Приватные данные утекут. Исправляется заголовками private, no-cache.
  3. Не учитывать время инвалидации
    Обновлённый контент может не отображаться часами из-за долгого TTL. Включают версионирование URL или своевременный purge.

Возможные дополнительные вопросы интервьюера

  • Как спроектировать CDN для загрузки пользовательского контента (UGC)?
    Использовать двухуровневое хранилище: origin (облачное object storage), CDN с cache on demand; добавить антивирусную проверку до распространения.
  • Что такое CDN push vs pull?
    Push — контент загружается в CDN заранее. Pull — CDN забирает с origin при первом запросе. Pull проще, push даёт гарантированную доступность.
  • Как CDN защищает от DDoS?
    Распределённая ёмкость поглощает трафик; используется rate limiting, фильтрация на уровне сетевых пакетов, JavaScript-челленджи (Cloudflare I’m Under Attack).

Что такое микросервисы vs монолит?

Краткий ответ (30–60 секунд)

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

Развёрнутый ответ

Модульный монолит — промежуточная стадия, где код разделён по модулям, но деплоится как единое целое. Микросервисная архитектура требует решения вопросов межсервисного взаимодействия (синхронное REST/gRPC, асинхронное через брокеры сообщений), отказоустойчивости (circuit breaker, retry), распределённой трассировки и мониторинга. Основные принципы: single responsibility, автономность, децентрализованное управление данными (database per service). Контейнеризация (Docker) и оркестрация (Kubernetes) стали катализатором микросервисов. В Go микросервисы пишутся как небольшие программы, запущенные каждая в своей горутине/процессе, с gRPC или HTTP.

Под капотом (internals)

С точки зрения Go, монолит может просто использовать in-process вызовы функций, микросервисы — сетевое взаимодействие. При переходе с монолита на микросервисы возникает проблема «распределённого монолита», когда высокая связность порождает каскадные сбои. Для предотвращения используются контрактное программирование (protobuf), API Gateway. Service Mesh (Istio/Linkerd) внедряет sidecar-прокси (Envoy), которые берут на себя балансировку, circuit breaking, mTLS. Это накладывает издержки: добавление ~1-3 мс к вызову, но обеспечивает наблюдаемость.

Типичные ошибки кандидатов

  1. Считать микросервисы серебряной пулей
    Без должной инженерной культуры (CI/CD, мониторинг, автономные команды) микросервисы приведут к хаосу. Начинать с монолита и выносить сервисы по мере необходимости — здравая стратегия.
  2. Дробить слишком мелко
    Нано-сервисы на каждую функцию — переусложнение, сетевые издержки съедают выгоду. Границы сервисов определяют по бизнес-доменам (Domain-Driven Design).
  3. Игнорировать распределённые транзакции
    В монолите транзакция проста, в микросервисах требуется Sagas или event sourcing с компенсациями. Кандидат должен понимать последствия.

Возможные дополнительные вопросы интервьюера

  • Как определить границы микросервисов?
    Использовать bounded context из DDD: сервис должен иметь чёткую бизнес-ответственность, собственную модель данных и API, минимизировать совместные изменения.
  • Что такое shared kernel и чем он опасен?
    Общий модуль между сервисами; нарушает независимость деплоя, создаёт coupling. Лучше дублировать код или стабилизировать контракт.
  • Как мигрировать с монолита на микросервисы?
    Стратегия Strangler Fig: постепенно вырезать функциональность, перенаправляя трафик через фасад, пока монолит не исчезнет.

Что такое REST API?

Краткий ответ (30–60 секунд)

REST (Representational State Transfer) — архитектурный стиль для построения распределённых систем, использующий HTTP-протокол без состояния. API строится вокруг ресурсов, идентифицируемых URL, с операциями над ними через стандартные HTTP-методы (GET, POST, PUT, DELETE). Ключевые ограничения: client-server, stateless, кэшируемость, единообразие интерфейса, layered system, код по требованию (опционально).

Развёрнутый ответ

REST API проектируют, следуя принципам:

  • Ресурсы — существительные (users, orders), не глаголы.
  • Иерархия через вложенные URL: /users/{id}/orders.
  • Запросы без состояния: каждый запрос содержит всю информацию для обработки, сессия хранится на клиенте.
  • Использование кодов HTTP-статуса (200, 201, 400, 404, 500) и стандартных заголовков (Accept, Content-Type).
  • HATEOAS (Hypermedia as the Engine of Application State) — редко реализуется полностью, но даёт самодокументирование.
    Формат данных обычно JSON. Версионирование: через URL (/v1/users) или заголовок Accept. В Go популярны стандартный net/http, роутеры gorilla/mux, chi, фреймворки gin, echo. REST хорош для публичных API и межсервисного взаимодействия, где требуется простота и кэширование.

Под капотом (internals)

HTTP/1.1 — текстовый протокол с конвейеризацией и keep-alive, HTTP/2 — бинарный, мультиплексирует потоки, позволяет server push. REST API часто используют JSON, что приводит к накладным расходам на сериализацию; в Go пакет encoding/json использует рефлексию, что влияет на производительность. Альтернативы — jsoniter или генераторы кода. Стандартный net/http создаёт горутину на каждый запрос, что эффективно для конкурентных сервисов. Middleware-цепочки реализуются через обёртки http.Handler.

Типичные ошибки кандидатов

  1. Глаголы в URL
    /getUsers или /createOrder нарушают REST-стиль; правильнее GET /users, POST /orders. Ошибка возникает из-за RPC-мышления.
  2. Игнорирование статус-кодов
    Возвращают 200 OK с телом ошибки {"error": "..."}. Клиент не может различить успех/провал без разбора тела. Используют 4xx/5xx.
  3. Неверное использование POST и PUT
    POST создаёт ресурс (неидемпотентный), PUT — полностью заменяет (идемпотентный). Кандидаты часто используют PUT для частичных обновлений, для которых есть PATCH.
  4. Отсутствие пагинации
    Запрос списка без лимитов может вернуть миллионы записей, что уронит сервер. Добавляют query params ?offset=...&limit=... и возвращают ссылки на следующую страницу.

Возможные дополнительные вопросы интервьюера

  • Чем REST отличается от RPC?
    REST ориентирован на ресурсы и манипуляцию ими через стандартные методы, RPC — на вызов процедур с произвольными действиями (gRPC, JSON-RPC). REST проще кэшировать и интегрировать.
  • Что такое идемпотентные методы в HTTP?
    GET, HEAD, PUT, DELETE, OPTIONS должны быть идемпотентными (повторный запрос даёт тот же эффект). POST — неидемпотентный.
  • Как обеспечить безопасность REST API?
    Аутентификация через токены (JWT, OAuth2), авторизация на уровне ресурсов, HTTPS, валидация ввода, rate limiting, CORS-заголовки.

Что такое gRPC?

Краткий ответ (30–60 секунд)

gRPC — высокопроизводительный RPC-фреймворк от Google, использующий Protocol Buffers (protobuf) как язык описания интерфейса и бинарный формат сериализации. Построен на HTTP/2, поддерживает потоковую передачу (unary, server/client/bidirectional streaming), аутентификацию, балансировку нагрузки, таймауты. Идеален для межсервисного взаимодействия в микросервисах благодаря строгой типизации, кодогенерации и эффективности.

Развёрнутый ответ

Схема данных и сервисов описывается в .proto файлах, компилятор генерирует клиентский и серверный код на множестве языков (Go, Java, Python и т.д.). gRPC оперирует сообщениями protobuf, которые сериализуются в компактный бинарный вид. В Go реализация использует google.golang.org/grpc и protoc-gen-go. Сервер запускает gRPC сервер на определённом порту, клиент создаёт connection и вызывает методы как обычные функции. Поддерживаются перехватчики (interceptors) – аналог middleware. HTTP/2 позволяет мультиплексировать много вызовов в одном TCP-соединении, снижая накладные расходы. Streaming позволяет реализовать reactive patterns.

Под капотом (internals)

gRPC в Go использует транспорт на основе HTTP/2 библиотеки golang.org/x/net/http2. Каждый gRPC стрим — это отдельный HTTP/2 stream. Сериализация protobuf генерирует код, который напрямую читает поля структур без рефлексии, что даёт высокую скорость. Балансировка нагрузки на клиенте реализована через resolver и balancer интерфейсы; по умолчанию используется pick-first, но есть round-robin. gRPC предлагает встроенную поддержку deadline'ов, которые передаются от клиента к серверу и учитываются при обработке, что помогает реализовать fail-fast.

Типичные ошибки кандидатов

  1. Не обновлять прото-контракты с обратной совместимостью
    Изменение типа поля или удаление без резервирования номеров ломает десериализацию. Нужно следовать правилам protobuf: добавлять поля с новыми номерами, помечать удалённые как reserved.
  2. Использовать gRPC везде, включая браузер
    Браузеры плохо поддерживают gRPC напрямую (нужен gRPC-web прокси). Для внешнего API удобнее REST или GraphQL.
  3. Игнорирование deadline и retry
    Без deadline вызов может висеть бесконечно, переполняя соединения. В Go необходимо передавать контекст с таймаутом: ctx, cancel := context.WithTimeout(...), defer cancel().
  4. Пул соединений
    Создание нового клиента на каждый запрос — дорого (TLS handshake). Клиент gRPC должен быть долгоживущим, его безопасно переиспользовать из многих горутин.

Возможные дополнительные вопросы интервьюера

  • Сравнение gRPC и REST по производительности.
    gRPC с protobuf и HTTP/2 значительно быстрее из-за бинарного формата и мультиплексирования; REST с JSON требует парсинга и текстового HTTP/1.1, больше накладных расходов.
  • Как дебажить gRPC?
    Используют grpcurl, gRPC reflection для сервиса, interceptor-логирование, трассировку через OpenTelemetry, инструменты вроде BloomRPC.
  • Что такое gRPC-стриминг и зачем?
    Server streaming — сервер шлёт много ответов (логи, события); client streaming — клиент отправляет поток данных (загрузка файла); bidirectional — чат, интерактивные игры. Эффективно для сценариев с большим количеством сообщений.

Что такое GraphQL?

Краткий ответ (30–60 секунд)

GraphQL — язык запросов для API и среда выполнения, разработанная Facebook. Позволяет клиенту запрашивать ровно те данные, которые нужны, в одной конечной точке, избегая over-fetching/under-fetching. Сервер публикует строго типизированную схему, а клиенты описывают структуру ответа. Подписки реализуют real-time обновления через WebSocket.

Развёрнутый ответ

GraphQL-сервер имеет единственный endpoint (обычно /graphql), принимает POST-запросы с телом JSON, содержащим query и variables. Резолверы (resolver functions) связывают поля схемы с бизнес-логикой. Проблема N+1 решается с помощью DataLoader, который пакетирует запросы. Подход хорош для frontend-тяжёлых приложений, где множество клиентов с разными потребностями (mobile, web). В Go популярны библиотеки graphql-go/graphql и 99designs/gqlgen, генерирующие код на основе schema-first подхода. Однако GraphQL сложен для кэширования (POST-запросы, динамические ответы) и может привести к дорогим запросам без ограничений глубины.

Под капотом (internals)

Выполнение запроса проходит стадии: парсинг, валидация, выполнение. Движок строит дерево резолверов, вызывая их конкурентно. gqlgen генерирует типизированный unsafe-код, который минимизирует аллокации. Для подписок используется транспорт через WebSocket/SSE, управление событиями через каналы в pub/sub. DataLoader в Go реализует batch-функции с дедупликацией и кэшированием в рамках одного запроса, используя wait-группу или sync.Once с таймером.

Типичные ошибки кандидатов

  1. Бесконтрольная сложность запросов
    Злоумышленник может отправить глубоко вложенный запрос, что исчерпает ресурсы сервера. Обязательно ограничивают глубину, сложность и таймауты.
  2. N+1 проблема
    Резолвер для каждого элемента списка делает отдельный вызов к БД. Используют DataLoader или оптимизируют запросы с JOIN.
  3. Игнорирование авторизации на уровне полей
    Недостаточно защитить endpoint, нужно проверять права на каждое поле в резолвере, иначе возможна утечка данных.
  4. Использование GraphQL там, где достаточно REST
    Простые CRUD API становятся избыточными с GraphQL, добавляя сложность. Выбор должен быть обоснован вариативностью клиентов.

Возможные дополнительные вопросы интервьюера

  • Как GraphQL стыкуется с кэшированием HTTP?
    Традиционное кэширование не работает из-за единого POST-эндпоинта. Используют персистентные запросы (persisted queries) с GET и CDN, или кастомный кэш на основе сериализованного запроса.
  • Как организовать пагинацию в GraphQL?
    Relay-спецификация: курсорная пагинация с edges, node, pageInfo. Поддерживается first, after.
  • Что такое Apollo Federation?
    Способ объединения нескольких GraphQL-сервисов в единый граф через шлюз, реализуя принцип федерации без репликации кода.

Как проектировать URL для REST API?

Краткий ответ (30–60 секунд)

Хороший REST API оперирует иерархическими URL, представляющими ресурсы (существительные, множественное число). Используют вложенность для выражения отношений: /users/{id}/orders. Действия задаются HTTP-методами, не глаголами в URL. Query-параметры применяют для фильтрации, сортировки, пагинации. Версионирование — префикс /v1/. Документирование — OpenAPI (Swagger).

Развёрнутый ответ

Правила дизайна:

  • Ресурсы коллекций: /users, /orders.
  • Конкретный ресурс: /users/123.
  • Отношения: /users/123/orders (чьи заказы), /orders/456/items.
  • Избегать глубокой вложенности (>2 уровней), т.к. URL становится громоздким; можно предоставить короткий путь /order-items?orderId=456.
  • Не включать в URL глаголы: /createUser → POST /users.
  • Использовать camelCase в query-параметрах по возможности, но snake_case тоже ок, главное — консистентность.
  • Фильтрация: GET /users?status=active&role=admin.
  • Пагинация: GET /users?offset=0&limit=20 или курсорная ?cursor=....
  • Версионирование: /v1/users. Не разносить по разным доменам.

В Go роутеры (chi, gorilla/mux) позволяют легко извлекать параметры URL: /{userID} → chi.URLParam(r, "userID"). Middleware можно привязать к группам маршрутов.

Под капотом (internals)

URL — это унифицированный идентификатор, парсинг стандартной библиотекой net/url. Для извлечения параметров пути используются радикс-деревья (radix tree) в chi и HTTP-роутерах, обеспечивающие O(log n) или O(1) поиск. В REST API важно разделять transfer-состояние и прикладное состояние; URL должен идентифицировать ресурс, а HTTP-заголовки (Accept, If-None-Match) — управлять представлением. HATEOAS ссылки в ответах строятся динамически, для этого нужна генерация абсолютных URL на основе *http.Request.

Типичные ошибки кандидатов

  1. Использование глаголов в URL
    /getUser, /deleteOrder — нарушение REST, смесь RPC. Исправить: GET /users/{id}, DELETE /orders/{id}.
  2. Несогласованное именование
    Часть коллекций в единственном числе, часть во множественном (/user/, /orders/). Выбрать одно правило (обычно множественное) и придерживаться.
  3. Излишняя вложенность
    /a/{id}/b/{id}/c/{id}/d — нечитаемо и негибко. Лучше /d?cId=... или выделить корневой ресурс.
  4. Игнорирование HATEOAS и ссылок
    Клиенты вынуждены конструировать URL вручную, что ведёт к хрупкости. Стоит включать ссылки на связанные ресурсы в ответе: {"orders": [...], "_links": {"self": ...}}.

Возможные дополнительные вопросы интервьюера

  • Как передать сложный поиск?
    Используют query-параметры с префиксами: ?filter[status]=active&sort=-created_at, либо POST на /search с телом, если параметры не умещаются в URL.
  • Как обрабатывать версии, когда изменения ломают обратную совместимость?
    Увеличивают мажорную версию: /v2/users. Старую версию поддерживают в течение переходного периода с депрекацией.
  • Что такое "красивые" URL и почему это важно?
    Читаемые URL повышают UX и SEO, облегчают отладку. Используют транслитерацию, избегают ID, где возможно (slug).

Что такое API Gateway?

Краткий ответ (30–60 секунд)

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

Развёрнутый ответ

Гейтвей реализует паттерн Gateway Aggregation и Backend for Frontend (BFF). Он принимает запросы от внешних приложений и направляет их в нужные сервисы, часто выполняя композицию данных (scatter-gather). Может преобразовывать протоколы (например, HTTP в gRPC). Популярные решения: Kong, Tyk, AWS API Gateway, Envoy, Traefik; также собственные реализации на Go. API Gateway не должен содержать бизнес-логику, иначе превращается в монолит. Важно обеспечить его отказоустойчивость и горизонтальное масштабирование. В Go можно построить Gateway, используя обратный прокси httputil.ReverseProxy с кастомным Director, который модифицирует URL и заголовки.

Под капотом (internals)

Типичный API Gateway — это L7 reverse proxy. Он терминирует TLS, проверяет подпись JWT, делает запрос к rate limiter (In-memory или Redis). Маршрутизация строится на правилах (путь, метод, заголовки). Для каждого маршрута может быть цепочка плагинов (middleware), реализованных как цепочка обработчиков. В Go net/http не предоставляет готового Gateway, но любое кастомное решение использует http.ServeMux или кастомный роутер, а для проксирования применяет http.Transport с пулом соединений. Высокопроизводительные гейтвеи (Kong на OpenResty/Lua, Envoy на C++) используют асинхронный event loop, что даёт преимущество в latency tail.

Типичные ошибки кандидатов

  1. Загрузка бизнес-логики в гейтвей
    Желание «починить» данные в гейтвее ведёт к раздуванию и спагетти. Всю логику оставляют в сервисах.
  2. Отсутствие обработки ошибок при агрегации
    Если один из вызываемых сервисов упал, гейтвей должен уметь возвращать partial response или грациозно деградировать, а не отдавать 500.
  3. Проблема единой точки отказа
    API Gateway становится критичным звеном. Нужно запускать несколько экземпляров, настроить health checks и circuit breaker перед сервисами.
  4. Слишком жёсткая привязка к внутренним изменениям
    Каждый новый сервис требует изменения конфигурации гейтвея и перезапуска. Используют discovery-сервис и динамическую конфигурацию.

Возможные дополнительные вопросы интервьюера

  • Чем API Gateway отличается от Service Mesh?
    Gateway управляет север-юг трафиком (извне), mesh — восток-запад (между сервисами внутри). Часто mesh работает на уровне sidecar-прокси.
  • Как обеспечить zero-downtime деплой API Gateway?
    Graceful shutdown (остановка приёма новых запросов, завершение текущих), health check в оркестраторе, rolling update.
  • Нужен ли API Gateway при использовании gRPC?
    Да, может конвертировать REST в gRPC для внешних клиентов, обеспечивать аутентификацию, rate limiting на уровне gRPC вызовов через interceptors.

Что такое Service Discovery?

Краткий ответ (30–60 секунд)

Service Discovery — механизм, с помощью которого сервисы динамически находят сетевые адреса друг друга. В динамической среде (контейнеры, облака) IP постоянно меняются, поэтому нужен реестр, где сервисы регистрируют себя и запрашивают адреса зависимостей. Подходы: client-side discovery (клиент сам выбирает экземпляр из реестра) и server-side discovery (запрос через балансировщик).

Развёрнутый ответ

Компоненты:

  • Реестр (Consul, etcd, Zookeeper, Kubernetes DNS + Service).
  • Сервис-провайдер регистрируется при старте и удаляется при остановке (с heartbeat).
  • Сервис-потребитель получает список экземпляров и кэширует его.
    В client-side discovery потребитель самостоятельно балансирует, например, с помощью library gRPC resolver + round-robin. В server-side — используется внешний балансировщик (AWS ELB, Kubernetes Service с ClusterIP), который знает о бэкендах. Kubernetes изначально предоставляет server-side discovery через Service object и DNS: my-service.namespace.svc.cluster.local. В Go-микросервисах часто используют Consul с agent, либо встраивают библиотеку для DNS-резолва.

Под капотом (internals)

Consul использует протокол gossip для обнаружения участников и Raft для консенсуса состояния. etcd — Raft и gRPC. Kubernetes DNS (CoreDNS) мониторит API-сервер и создаёт DNS записи для Service. Клиентская балансировка в gRPC-Go реализуется через интерфейс resolver.Builder, который периодически опрашивает discovery и обновляет адреса. Balancer выбирает sub-connection. Для HTTP-сервисов в Go можно использовать библиотеку hashiCorp/go-discover или периодически опрашивать Consul API, обновляя пул транспортов.

Типичные ошибки кандидатов

  1. Жёсткое прописывание адресов (hardcoding)
    Не масштабируется и ломается при изменении окружения. Следует использовать service discovery с первых дней.
  2. Отсутствие health check при регистрации
    Мёртвый сервис остаётся в реестре, что ведёт к ошибкам подключения. Нужны периодические проверки и автоматическое исключение.
  3. Игнорирование задержки распространения
    Реестр может отдавать устаревшие данные; клиент должен иметь retry, circuit breaker, и graceful fallback на закэшированный список.
  4. Выбор server-side discovery там, где нужна клиентская гибкость
    Server-side добавляет лишний сетевой хоп, но упрощает клиента. Выбор должен быть осознанным.

Возможные дополнительные вопросы интервьюера

  • Как реализовать service discovery в среде без Kubernetes?
    Запустить Consul/etcd кластер, интегрировать регистрацию в процесс запуска сервиса через библиотеки.
  • Что такое self-registration vs third-party registration?
    Self — сервис сам отправляет запрос на регистрацию; third-party — агент/ orchestrator (Kubernetes) делает это за сервис. Второй вариант надёжнее для сервисов, не умеющих регистрироваться.
  • Как избежать разрыва соединений при обновлении реестра?
    Использовать client-side балансировщик, который при отказе экземпляра переключается на другой, и плавно обновлять пул.

Что такое Circuit Breaker?

Краткий ответ (30–60 секунд)

Circuit Breaker — паттерн устойчивости, предотвращающий каскадные отказы. Он отслеживает вызовы к внешнему сервису и при превышении порога ошибок «размыкает цепь», мгновенно возвращая ошибку без попытки реального вызова. Через таймаут переходит в полуоткрытое состояние, проверяя восстановление сервиса. Аналогичен электрическому автомату.

Развёрнутый ответ

Основные состояния:

  • Closed: нормальная работа, вызовы проходят; счётчик ошибок инкрементируется.
  • Open: цепь разомкнута, вызовы немедленно фейлятся (fail-fast); длится заданное время.
  • Half-Open: разрешён пробный ограниченный вызов; если успешен — переход в Closed, иначе — обратно в Open.

Параметры: порог ошибок, окно оценки, таймаут размыкания. Может работать на уровне одного экземпляра или распределённо. В Go реализуется с помощью пакетов gobreaker или hystrix-go. Обёртывает вызов функции. Важно засекать не только ошибки сети, но и таймауты, а также медленные ответы (slow calls). Circuit breaker защищает и клиента, и сервер от перегрузки.

Под капотом (internals)

gobreaker использует счётчики в памяти, атомарные операции для подсчёта успехов/неудач. Состояние хранится в uint32 и изменяется через CAS. Типичная реализация: при вызове проверяется состояние, если Open — сразу возвращается ErrOpenState. В Closed состоянии после каждого ответа обновляется счётчик в пределах скользящего окна (например, кольцевой буфер с временными корзинами). Если за окно набралось достаточно ошибок, переключается в Open и запускает таймер. В распределённых системах состояние можно хранить в Redis, чтобы делить между экземплярами.

Типичные ошибки кандидатов

  1. Не настраивать пороги под характер сервиса
    Слишком низкий порог вызывает ложные размыкания при кратковременных всплесках. Нужно анализировать реальные метрики.
  2. Забывать о полуоткрытом состоянии
    Без него система никогда не восстановится автоматически. Некоторые реализации позволяют задать кол-во пробных запросов.
  3. Использовать circuit breaker для бизнес-ошибок
    Логические ошибки (404, 422) не должны размыкать цепь, т.к. это ожидаемое поведение. Circuit breaker реагирует на технические сбои (таймаут, connection refused).
  4. Отсутствие fallback-логики
    Разомкнутый circuit должен предоставлять запасной вариант (дефолтное значение, кэш, ответ с degraded quality), а не просто 500.

Возможные дополнительные вопросы интервьюера

  • Чем Circuit Breaker отличается от Retry?
    Retry увеличивает нагрузку на падающий сервис, circuit breaker прерывает вызовы, давая ему восстановиться. Обычно используются вместе: retry с отсечкой через circuit breaker.
  • Как мониторить состояние circuit breaker?
    Экспортировать метрики (состояние, счётчики, количество размыканий) в Prometheus, настраивать алерты.
  • Можно ли использовать circuit breaker для in-process вызовов?
    Технически да, если какой-то компонент может отказать (deadlock?), но обычно применяется для удалённых взаимодействий.

Что такое Rate Limiter?

Краткий ответ (30–60 секунд)

Rate Limiter ограничивает частоту запросов, которые клиент может отправить к API за определённый промежуток времени. Защищает от перегрузки, DDoS, гарантирует fair use. Реализуется на уровне API Gateway, reverse proxy или middleware. Нарушителям возвращается HTTP 429 Too Many Requests.

Развёрнутый ответ

Rate limiting бывает глобальным (общий лимит на сервис) или на основе ключа (IP, user ID, API token). Алгоритмы описаны в следующем вопросе. Ограничения могут быть жёсткими (hard) или мягкими (dynamic). Для распределённых систем состояние счётчиков необходимо хранить централизованно (Redis) или использовать алгоритмы с локальным состоянием и синхронизацией (например, DRL). В архитектуре микросервисов rate limiter обычно размещается на API Gateway. Go-реализации: golang.org/x/time/rate (in-process Token Bucket), ulule/limiter (с Redis/In-memory), didip/tollbooth. Важно учитывать скользящую точность и атомарность.

Под капотом (internals)

x/time/rate.Limiter реализует Token Bucket с дополнительной способностью ждать (Wait). Хранит количество токенов, последнее время обновления, лимит. Операции атомарны. Для распределённого rate limiting часто используют Lua-скрипты в Redis для атомарной проверки и инкремента. Сортированные наборы (Sorted Sets) позволяют реализовать Sliding Window Log. Envoy интегрирует rate limiter через внешний сервис, запрашивая разрешение на каждый запрос. Клиент rate limiter кэширует квоту на короткое время, чтобы снизить нагрузку.

Типичные ошибки кандидатов

  1. Проверка лимита после обработки запроса
    Ресурсы уже потрачены; нужно отклонять запрос как можно раньше в цепочке middleware.
  2. Использование наивного счётчика с фиксированным окном
    На границах окна возможен двойной всплеск (burst). Лучше скользящее окно или алгоритмы token/leaky bucket.
  3. Отсутствие информирования клиента
    Не возвращаются заголовки X-RateLimit-Remaining, Retry-After. Клиенты не могут адаптироваться и повторяют запросы.
  4. Ограничение по IP в эпоху NAT и IPv6
    Много пользователей за одним IP; нужно использовать API ключи или JWT claims.

Возможные дополнительные вопросы интервьюера

  • Как реализовать rate limiter для WebSocket?
    По количеству сообщений в минуту на соединение или по кадру. Эффективнее проверять частоту на сервере после получения сообщения.
  • Что делать, если Redis с состоянием rate limiter упал?
    Graceful degradation: разрешить запросы (риск перегрузки) или временно запретить (консервативно). Можно fallback к локальному лимитеру.
  • Чем rate limiting отличается от throttling?
    Rate limiting жёстко обрезает запросы сверх лимита, throttling — замедляет, ставит в очередь. Иногда термины смешивают.

Какие алгоритмы Rate Limiting существуют (Token Bucket, Leaky Bucket)?

Краткий ответ (30–60 секунд)

Основные алгоритмы: Token Bucket – разрешает кратковременные всплески; Leaky Bucket – сглаживает поток до постоянной скорости; Fixed Window – простой счётчик за временной интервал, но страдает от «эффекта границы»; Sliding Window Log/Сounter – даёт точное ограничение без всплесков; Sliding Window с приближением – компромисс точности и памяти. Token Bucket – самый популярный в API-шлюзах.

Развёрнутый ответ

  • Token Bucket: есть ведро с максимальной вместимостью токенов. Токены добавляются с постоянной скоростью. Запрос забирает один токен; если их нет – отказ. Позволяет накопить токены до максимума (burst). В Go x/time/rate реализует этот алгоритм.
  • Leaky Bucket: очередь фиксированного размера. Запросы поступают в очередь (протекают как вода), а обрабатываются с постоянной скоростью. При переполнении очереди запрос отбрасывается. Обеспечивает строгое сглаживание. Может быть реализован через каналы: горутина-воркер считывает с фиксированной частотой, очередь — буферизированный канал.
  • Fixed Window Counter: инкремент счётчика за окно (1 минута). После истечения окна сбрасывается. Недостаток: если лимит 100/мин, в первые 30 секунд приходит 100 запросов, а в следующие 30 сек ещё 100, то за 1 минуту пройдёт 200, но счётчик в каждом окне будет ≤100.
  • Sliding Window Log: храним timestamp каждого запроса в Redis sorted set, удаляем устаревшие за пределами окна. Точный, но память O(количество запросов в окне).
  • Sliding Window Counter: комбинация: храним счётчик предыдущего окна и текущего с весом по времени. Меньше памяти, хорошая точность.

Под капотом (internals)

Token Bucket в Go: limiter.Reserve() атомарно вычисляет, можно ли осуществить действие сейчас или позже, использует time.Time для расчёта пополнения. Wait блокирует горутину до доступности токена, используя таймер. Leaky Bucket через канал chan struct{}: производитель посылает запрос в канал, если канал полон — отказ; потребитель забирает с фиксированным интервалом (тикер). В распределённых системах часто применяют Redis с Lua для атомарности: Token Bucket хранит tokens, last_refill_time. При каждом запросе вычисляют число токенов к моменту вызова, обновляя.

Типичные ошибки кандидатов

  1. Путают Token Bucket и Leaky Bucket
    Token Bucket управляет входной скоростью, допуская всплески, Leaky Bucket управляет выходной скоростью. Кандидат должен различать use-case: API rate limiting – token bucket, шейпинг трафика – leaky bucket.
  2. Игнорирование burst
    Установка burst=1 при разумном лимите делает систему негибкой. Нужно разрешать небольшой burst (например, 10% от rate).
  3. Использование Fixed Window без понимания проблемы
    Думают, что защита надёжна, а двойной скокс может пройти. Рекомендовать Sliding Window.
  4. Неверная реализация распределённого Token Bucket без Lua
    Операции check-and-set неатомарны, что приводит к race condition. Используют EVAL скрипты или подходы на основе Lua.

Возможные дополнительные вопросы интервьюера

  • Как выбрать между Token Bucket и Fixed Window?
    Если клиенты могут кэшировать и нужен burst — Token Bucket; для простого API с одним ресурсом — Fixed Window дешевле.
  • Как достичь глобального rate limiting с несколькими экземплярами шлюза?
    Централизованное хранилище (Redis) + локальный кэш квоты с коротким TTL, либо eventual consistency алгоритм типа DRL.
  • Как протестировать rate limiter?
    Послать поток запросов чуть выше лимита, проверить, что возвращаются 429, заголовки, правильное количество успешных.

Что такое идемпотентность?

Краткий ответ (30–60 секунд)

Идемпотентность — свойство операции, при котором многократное выполнение даёт тот же результат, что и однократное. В HTTP: GET, PUT, DELETE — идемпотентны, POST — нет. В API это критично для платёжных и мутационных операций, когда из-за сетевых сбоев клиент повторяет запрос, но операция не должна выполниться дважды (списание денег).

Развёрнутый ответ

Идемпотентный API гарантирует, что независимо от количества идентичных запросов состояние системы изменится только один раз. Реализуется через идемпотентные ключи (idempotency key), которые клиент генерирует и передаёт в заголовке (например, Idempotency-Key). Сервер сохраняет ключ и результат первого успешного выполнения; последующие запросы с тем же ключом возвращают сохранённый ответ, не выполняя операцию. Это требует атомарного хранения ключа и состояния. В Go логика строится вокруг сначала проверки ключа в БД (Redis/SQL), затем выполнения бизнес-логики и сохранения ответа. Неидемпотентные операции в случае сбоя могут быть повторены, но если операция частично выполнена, может возникнуть дубликат.

Под капотом (internals)

При реализации идемпотентного API в Go можно использовать транзакции БД: INSERT INTO idempotency_keys (key, response, status, created_at). Если вставка не удалась из-за нарушения уникальности, значит ключ уже обработан — читаем сохранённый ответ. Чтобы избежать гонок между параллельными запросами с одинаковым ключом, применяют блокировки (SELECT FOR UPDATE) или upsert с conditional. В Redis используют SETNX с TTL. Для платёжных систем идемпотентность гарантируется на уровне платёжного шлюза (Stripe idempotency). В Go нужно следить за таймаутами; если первый запрос завершился таймаутом, клиент повторяет с тем же ключом, а сервер должен корректно вернуть ответ, когда операция завершится — для этого используют блокировку и ожидание.

Типичные ошибки кандидатов

  1. Считать, что PUT всегда идемпотентен
    PUT заменяет ресурс целиком. Многократный вызов с одними данными — идемпотентен. Но если реализация добавляет запись в лог или инкрементирует счётчик, то нет. Идемпотентность в HTTP — это про семантику.
  2. Идемпотентность без ключа
    Рассчитывать на естественную идемпотентность операции опасно. Нужен явный ключ для контроля.
  3. Неправильное хранение ключей
    Удаление ключа после первого использования (из-за короткого TTL) делает операцию опасной для повторного запроса после сбоя. Ключ надо хранить достаточно долго (минимум 24h).
  4. Отсутствие обработки параллельных запросов с одинаковым ключом
    Оба запроса могут увидеть отсутствие ключа и выполнить операцию дважды. Используют атомарную проверку+вставку и блокировку.

Возможные дополнительные вопросы интервьюера

  • Как идемпотентность связана с безопасностью повторных атак (replay attack)?
    Идемпотентные запросы без ограничения по времени могут быть повторно использованы злоумышленником. Добавляют временную метку и ограничивают срок действия ключа.
  • Можно ли сделать POST идемпотентным?
    С помощью идемпотентного ключа и логики сервера, да. Но в стандартном понимании POST неидемпотентен, потому что создаёт новый ресурс.
  • Как обработать случай, когда первая операция ещё не завершена, а пришёл повторный запрос?
    Возвращать 409 Conflict с сообщением «request in progress» либо ожидать завершения (с таймаутом) и затем вернуть результат.

Как спроектировать идемпотентный API?

Краткий ответ (30–60 секунд)

Требовать от клиента передавать уникальный Idempotency-Key в заголовке на каждую мутирующую операцию. Сервер атомарно проверяет ключ в хранилище: если ключ новый — выполняет операцию, сохраняет ответ; если уже существует — возвращает сохранённый ответ без выполнения. Ключ должен быть уникальным для каждой попытки смены состояния, обычно UUID. Хранилище обеспечивает уникальность ключа и транзакционность.

Развёрнутый ответ

Проект включает:

  1. Контракт: клиент включает заголовок Idempotency-Key: <uuid> в POST/PUT/PATCH/DELETE.
  2. Middleware/обёртка в Go: извлекает ключ, если отсутствует — возвращает 400.
  3. Логика:
    • Попытка атомарно вставить ключ со статусом processing (например, Redis SET NX с TTL).
    • Если вставка успешна — выполняем бизнес-операцию в транзакции, сохраняем результат (тело + статус-код) и обновляем ключ.
    • Если ключ уже существует — читаем статус: если processing (другой запрос в процессе) — либо возвращаем 409 Conflict, либо ждём с polling и таймаутом. Если completed — возвращаем сохранённый ответ.
  4. Хранилище: Redis для быстрой проверки с атомарностью Lua-скриптов, или PostgreSQL с INSERT ... ON CONFLICT DO NOTHING и последующим SELECT.
  5. Очистка: старые ключи удаляются по TTL или отдельной задачей, чтобы не бесконечно расти.
    В Go можно использовать singleflight.Group для обработки конкурентных запросов с одним ключом в рамках одного процесса, но это не заменит внешнее хранилище.

Под капотом (internals)

Атомарность в Redis: Lua-скрипт, который проверяет GET idempotency_key, если nil — SET с телом и TTL, возвращает nil и выполняем операцию; иначе возвращает сохранённый ответ. В SQL: BEGIN;, попытка вставки, если успех — выполнение операции, апдейт, коммит; если нарушение уникальности — откат, селект ответа. Для ждущего режима можно использовать SELECT ... FOR UPDATE NOWAIT или pg_advisory_lock. В Go database/sql с уровнем изоляции Serializable может дать ошибку сериализации, которую нужно ретраить.

Типичные ошибки кандидатов

  1. Хранить ключ только в памяти одного экземпляра
    Нарушает горизонтальное масштабирование. Нужно общее хранилище.
  2. Не защищать от race condition двух одновременных запросов
    Два запроса могут прочитать отсутствие ключа, оба выполнят операцию. Решение – INSERT ... ON CONFLICT или Redis SETNX.
  3. Забывать про идемпотентность асинхронных операций
    Если операция ставится в очередь, отправка дубликата поставит второе сообщение. Нужен механизм дедупликации на уровне consumer.
  4. Не учитывать различный ответ при повторе после успеха
    Повторный запрос должен вернуть точно такой же HTTP-статус и тело, как и оригинальный успешный, включая заголовки (Location созданного ресурса). Сохранять ответ полностью.

Возможные дополнительные вопросы интервьюера

  • Как быть с идемпотентностью при обновлении ресурса (PATCH)?
    Идемпотентный ключ гарантирует, что операция применится один раз. Если частичное обновление меняет только часть полей, повторный запрос с теми же данными не должен ничего менять, но ответ должен быть одинаковым.
  • Можно ли обойтись без явного ключа, если операция естественно идемпотентна?
    Только если вы абсолютно уверены, но лучше добавить ключ для безопасности, особенно в распределённой среде.
  • Как тестировать идемпотентность в интеграционных тестах?
    Посылать два запроса с одинаковым ключом, проверять, что побочный эффект возник один раз (например, один заказ в БД) и ответы идентичны.

Как обрабатывать распределенные транзакции?

Краткий ответ (30–60 секунд)

Распределённые транзакции координируют изменения данных в нескольких независимых сервисах/базах. Два основных подхода: двухфазный коммит (2PC) для строгой консистентности, но с блокировками и задержками; и Саги (Sagas) — последовательность локальных транзакций с компенсационными шагами при откатах. В микросервисах Саги с оркестрацией или хореографией являются прагматичным выбором.

Развёрнутый ответ

2PC (XA протокол) требует координатора, который готовит участников (prepare), затем коммитит. Бывает медленным и блокирует ресурсы, плохо сочетается с автономными микросервисами. Саги – длинные бизнес-транзакции, каждая часть — локальная ACID транзакция, публикующая событие. Виды Саг:

  • Оркестрированная: центральный Saga orchestrator вызывает участников последовательно, при ошибке вызывает компенсации.
  • Хореографическая: сервисы обмениваются событиями через брокер, каждый знает, когда и как откатить.
    Также применяется Eventual Consistency и Event Sourcing. В Go для оркестрации можно использовать библиотеки типа temporal.io (Temporal) — workflow as code, который автоматически обрабатывает ретраи, таймауты, компенсации, сохраняя состояние.

Под капотом (internals)

Temporal реализует event-driven workflow engine: решения записываются в журнал событий, воркеры выполняют activity. Этот подход гарантирует прогресс даже при отказе воркера. В Go Temporal SDK интегрирует контекст, каналы и селекты для управления workflow. При использовании вручную: Saga orchestrator сохраняет текущий шаг в БД и слушает ответы. Для компенсации нужно иметь идемпотентные компенсационные операции. Без помощи движка сложно гарантировать выполнение компенсации при падении оркестратора; используют persistence состояния и механизмы повторного запуска.

Типичные ошибки кандидатов

  1. Предполагать, что 2PC всегда плох
    Для некоторых low-latency внутренних систем, где все участники под контролем одной команды, 2PC допустим. Но он создаёт сильную связность.
  2. Забывать про компенсации
    Разработчик саги может пропустить определение компенсационных шагов, оставляя систему в несогласованном состоянии при сбое.
  3. Игнорировать идемпотентность операций и повторную обработку событий
    Если retry приводит к дубликату события, без идемпотентной обработки возникнут дубликаты списаний. Нужны идемпотентные потребители.
  4. Проектировать хореографию без централизованного мониторинга
    Трудно понять, на каком шаге застряла сага. Нужна трассировка и мониторинг состояния саги.

Возможные дополнительные вопросы интервьюера

  • Как реализовать гарантию выполнения компенсации при падении оркестратора?
    Сохранять состояние саги в durable storage и после восстановления продолжать с того же шага; либо использовать надёжный движок (Temporal).
  • Что такое Saga Execution Coordinator?
    Компонент, хранящий конечный автомат саги, обрабатывающий таймауты, ретраи, компенсации, обеспечивающий атомарность обновления состояния.
  • Чем отличается событийный коллаборативный подход от хореографии?
    Хореография подразумевает непосредственную реакцию на событие без центра, коллаборация может использовать единый лог событий.

Что такое CQRS?

Краткий ответ (30–60 секунд)

CQRS (Command Query Responsibility Segregation) — паттерн, разделяющий модели обработки команд (изменение состояния) и запросов (чтение данных). Команды работают с write-моделью, оптимизированной для бизнес-инвариантов; запросы — с read-моделью, денормализованной под конкретные нужды UI/API. Это позволяет независимо масштабировать чтение и запись и упрощает сложную доменную логику.

Развёрнутый ответ

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

  • Command side: получает команды, валидирует, применяет бизнес-правила, сохраняет события (или агрегаты) в event store или реляционную БД.
  • Query side: read-only реплики, денормализованные проекции (materialized views), обновляемые асинхронно через события.
    Связь между ними обычно через события (Event-Driven). Применяется в Event Sourcing, но может использоваться и без него. В Go реализация: два отдельных сервиса или два пакета в одном процессе, разные структуры БД. Синхронизация может выполняться через публикацию событий изменения в NATS/Kafka, а проектор строит read-модель в MongoDB или Elasticsearch.

Под капотом (internals)

Команды обрабатываются внутри транзакции. Изменения сохраняются в event store как неизменяемые события. Проектор (projection engine) подписывается на поток событий и обновляет денормализованные таблицы. Может быть отставание (eventual consistency). В Go это выглядит как: команда → commandHandler → aggregate.HandleCommand → события EventStore.Save. Другой горутин или сервис читает из канала событий и обновляет read-модель. Для снижения задержки можно формировать read-модель синхронно после коммита, но тогда ослабляется разделение.

Типичные ошибки кандидатов

  1. Применение CQRS ко всей системе без нужды
    CQRS добавляет сложности; используется только там, где есть асимметрия нагрузки на чтение/запись или сложные domain rules.
  2. Игнорирование eventual consistency
    Клиент после команды может увидеть устаревшие данные. Нужно либо сообщать об этом, либо сделать жёсткую консистентность для отдельных операций.
  3. Проектировать команды как CRUD
    Команды должны быть намерениями (PlaceOrder, CancelOrder) с высокоуровневыми полями, а не просто заменой PUT.
  4. Отсутствие уникальности идемпотентности обработки событий проектором
    Проектор должен уметь обрабатывать повторные доставки событий.

Возможные дополнительные вопросы интервьюера

  • Можно ли реализовать CQRS без Event Sourcing?
    Да, можно разделить модели на уровне сервисов, используя реляционные БД и синхронизацию через CDC (Change Data Capture) или асинхронное обновление.
  • Как спроектировать API Gateway для CQRS?
    Маршрутизация команд и запросов на разные микросервисы, возможно, разные эндпоинты.
  • Как отлаживать расхождение read-модели?
    Вести журнал событий с версиями, периодически перестраивать проекцию, применять reconciliation-скрипты.

Что такое Event Sourcing?

Краткий ответ (30–60 секунд)

Event Sourcing — способ хранения состояния, при котором все изменения фиксируются как последовательность неизменяемых событий. Текущее состояние агрегата восстанавливается путём последовательного применения событий. В отличие от традиционного сохранения текущего состояния, история не теряется, что даёт полный аудит, временные запросы, и удобство для CQRS.

Развёрнутый ответ

Вместо таблицы orders с колонкой status мы храним события: OrderPlaced, OrderShipped, OrderDelivered. Агрегат в памяти восстанавливается из потока событий. Новые события добавляются в event store. Снапшоты (snapshots) периодически сохраняются для ускорения восстановления. События становятся источником истины (single source of truth). Это упрощает отладку, интеграцию с другими сервисами, и построение произвольных проекций. В Go реализация часто использует интерфейс EventStore, где SaveEvents(streamID, expectedVersion, events) атомарно сохраняет с оптимистичным контролем версий. Библиотеки: Eventsourcing или самописный слой над PostgreSQL/EventStoreDB.

Под капотом (internals)

Event Store должен гарантировать строго упорядоченное добавление событий в рамках одного потока (aggregate). Оптимистичный контроль версий: каждое событие имеет номер версии; при сохранении проверяется, что ожидаемая версия совпадает с последней, иначе — конфликт, и команда должна повториться с пересчётом. В Go это делается через UPDATE ... WHERE version = expected_version. Для повышения производительности используют асинхронную публикацию событий в шину после коммита. Проекции могут вестись в асинхронной манере, обновляя кэш, БД. Снапшот — сериализованное состояние агрегата + версия последнего события; при восстановлении сначала загружается снапшот, затем применяются последующие события.

Типичные ошибки кандидатов

  1. Проектировать события как мелкие CRUD-обновления
    Событие должно выражать бизнес-факт (OrderShipped), а не «поле статус стало 5». Это повышает выразительность.
  2. Не учитывать эволюцию событий
    С изменением схемы событий старые события нечитаемы. Нужна стратегия upcasting (преобразование при чтении) или версионирование с миграцией.
  3. Игнорировать производительность при большом количестве событий
    Восстановление агрегата из тысяч событий дорого; обязательны снапшоты.
  4. Переиспользовать Entity модели для событий
    События должны быть неизменяемыми структурами данных, с простыми типами, без ссылок на агрегаты.

Возможные дополнительные вопросы интервьюера

  • Как обеспечить консистентность между event store и публикацией событий?
    Использовать транзакционный outbox: события сохраняются в outbox таблицу в той же транзакции, а затем отдельный процесс публикует их в брокер.
  • Что делать, если проекция событий содержит ошибку и её нужно исправить?
    Добавить корректирующее событие, перестроить проекцию из полного потока событий, или запустить миграцию данных.
  • Как обрабатывать удаление данных (GDPR) в event sourcing?
    Сложно: можно использовать криптографическое стирание, забывание данных в снапшотах, но события как история сохраняются. Требуется legal hold.

Что такое Backend for Frontend (BFF)?

Краткий ответ (30–60 секунд)

Backend for Frontend — паттерн, при котором для каждого типа клиента (web, mobile, IoT) создаётся отдельный бэкенд-сервис, адаптирующий общие API микросервисов под нужды конкретного фронтенда. Он агрегирует данные, трансформирует форматы, оптимизирует количество запросов и улучшает UX. Уменьшает сложность фронтенда и изолирует изменения.

Развёрнутый ответ

В микросервисной архитектуре один универсальный API часто требует от клиента множества вызовов и неэффективен по сети. BFF решает это, становясь шлюзом с логикой, специфичной для канала: мобильный BFF может обрезать тяжелые поля, агрегировать несколько вызовов в один, поддерживать офлайн-сценарии. Основные правила: BFF не содержит бизнес-логики, только адаптацию и композицию. В Go BFF строится как сервис, использующий gRPC/HTTP к внутренним сервисам, и предоставляющий REST/GraphQL фронтенду. Часто реализуется через API Gateway с кастомными плагинами или независимым сервисом. Если фронтендов несколько, у каждого свой BFF.

Под капотом (internals)

BFF технически похож на API Gateway, но с прицелом на конкретный фронтенд. Использует scatter-gather: параллельные запросы к нескольким сервисам, ожидание ответов с таймаутами, сборка. В Go используются горутины и каналы для параллельных вызовов; errgroup для контроля ошибок. Важно применять circuit breaker и timeout. Паттерн «Stale while revalidate» может задействоваться для кэширования. Проксирование лучше делать через httputil.ReverseProxy с модификацией ответов. Версионирование BFF должно быть привязано к версиям фронтенда.

Типичные ошибки кандидатов

  1. Смешивание BFF и общего API Gateway
    BFF — это специализация, Gateway — общая точка входа. Неправильно заставлять Gateway выполнять задачи BFF, т.к. это ведёт к разбуханию.
  2. Дублирование бизнес-логики в BFF
    Бизнес-правила должны оставаться в доменных сервисах; BFF только адаптирует. Иначе логика размазывается.
  3. Недооценка накладных расходов
    Каждый BFF — это еще один сервис для поддержки. Если фронтенды похожи, может быть проще один универсальный API с параметрами.
  4. Отсутствие защиты от медленных бэкендов
    BFF должен устанавливать агрессивные таймауты и деградировать, иначе медленный сервис заблокирует все запросы фронтенда.

Возможные дополнительные вопросы интервьюера

  • Чем BFF отличается от API Gateway?
    Gateway маршрутизирует запросы как есть, часто без трансформации; BFF глубоко кастомизирует ответ для конкретного UI.
  • Может ли BFF напрямую обращаться к базе данных?
    Нет, это нарушит инкапсуляцию и приведёт к связанности с моделью данных.
  • Как управлять версиями BFF при обновлении мобильного приложения?
    Поддерживать несколько версий BFF одновременно или использовать механизм feature flags для обратной совместимости.

Что такое Webhook?

Краткий ответ (30–60 секунд)

Webhook — это механизм, при котором сервер-источник инициирует HTTP callback на URL, предоставленный потребителем, при наступлении определённого события. В отличие от polling, где клиент периодически опрашивает сервер, webhook толкает данные сразу. Используется для интеграции сторонних сервисов: уведомления о платежах, коммитах, изменениях заказа.

Развёрнутый ответ

Поставщик webhook регистрирует URL приёмника и события, на которые нужно подписываться. При возникновении события поставщик отправляет POST-запрос с телом JSON (или XML) на этот URL. Приёмник обрабатывает запрос и возвращает 2xx. Важные аспекты: безопасность (HMAC подпись для проверки подлинности), повторные попытки при неудаче (с exponential backoff), идемпотентность обработки. Webhook противоположен API polling, он эффективнее по ресурсам. В Go реализация ресивера — обычный HTTP handler, где проверяется подпись и запускается асинхронная обработка. Отправка webhook — это HTTP клиент с retry и таймаутами. Иногда используют очередь для гарантированной доставки.

Под капотом (internals)

Подпись часто реализуется как HMAC-SHA256 секретного ключа с телом запроса; в заголовке X-Hub-Signature-256 передаётся хеш. Приёмник вычисляет хеш и сравнивает. При обработке паники важно восстанавливаться (recover). Для обеспечения доставки при отказе приёмника поставщик должен хранить историю отправок и повторять попытки с увеличивающимися интервалами. В высоконагруженных системах поставщик может использовать worker pool для отправки. В Go можно легко сделать отправителя, запуская горутину на каждое событие, но нужно контролировать параллелизм через семафор.

Типичные ошибки кандидатов

  1. Не проверять подпись или не хранить секрет безопасно
    Любой может послать поддельный webhook и изменить состояние. Всегда проверять HMAC.
  2. Долгая синхронная обработка в обработчике
    Поставщик может иметь маленький таймаут, ответ нужно возвращать быстро. Следует асинхронно ставить задачу в очередь.
  3. Не обрабатывать повторные доставки
    Поставщик может доставить одно событие несколько раз; без идемпотентности возможны дубликаты операций.
  4. Игнорирование порядка событий
    Если события зависят от порядка (например, created/updated), при параллельной обработке может нарушиться логика. Проверять версию или использовать FIFO очередь.

Возможные дополнительные вопросы интервьюера

  • Как обеспечить надежность доставки webhook при долгом даунтайме приёмника?
    Хранить сообщения в БД со статусом, экспоненциально увеличивать паузу между ретраями, ограничить общее время попыток (например, 3 дня), затем пометить как failed.
  • Как отлаживать webhook?
    Использовать сервисы типа RequestBin, ngrok для локальной разработки, логировать все входящие запросы с телом и заголовками.
  • Чем webhook лучше polling?
    Ниже задержка, меньше бесполезных запросов, снижается нагрузка на сервер-источник. Но сложнее в отладке и требует публичного endpoint.

Что такое Long Polling vs WebSocket vs SSE?

Краткий ответ (30–60 секунд)

Три техники реального времени:

  • Long Polling: клиент запрашивает сервер и удерживает соединение, пока сервер не ответит с данными или не истечёт таймаут, после чего клиент немедленно открывает новый запрос. Псевдо-реалтайм.
  • WebSocket: полнодуплексный протокол, постоянное TCP-соединение, позволяющий обмениваться сообщениями в обе стороны с низкой задержкой.
  • Server-Sent Events (SSE): однонаправленный поток от сервера к клиенту поверх HTTP, клиент автоматически переподключается при разрыве. Проще WebSocket, но только сервер → клиент.

Развёрнутый ответ

Выбор зависит от требований:

  • Long Polling прост в реализации, не требует специальной инфраструктуры, но создаёт много соединений и overhead заголовков.
  • WebSocket идеален для чатов, игр, где нужен низкий latency и двусторонняя связь. Требует прокси, которые понимают Upgrade: websocket, может страдать от проблем с балансировкой (sticky sessions). В Go пакет gorilla/websocket или стандартный nhooyr.io/websocket.
  • SSE подходит для ленты новостей, уведомлений, где поток от сервера. Работает через обычный HTTP, автоматически восстанавливается (EventSource API), поддерживает переподключение и last-event-id.

Под капотом (internals)

WebSocket начинается с HTTP Upgrade handshake, затем TCP-соединение остаётся открытым, данные передаются в виде фреймов. В Go горутины читают и пишут в websocket.Conn. Пинги/понг для keep-alive. SSE — HTTP-соединение с Content-Type: text/event-stream, сервер через http.ResponseWriter пишет строки data: ... и периодический \n. Клиентский браузерный EventSource переподключается автоматически. Long Polling — сервер держит коннекшн, спит, при появлении данных пишет ответ и закрывает; клиент сразу делает новый запрос. Все три техники требуют учёта таймаутов прокси.

Типичные ошибки кандидатов

  1. Использовать WebSocket без необходимости
    Если нужна только серверная push-модель, SSE проще и не требует сложного прокси. WebSocket же оправдан при двунаправленном обмене.
  2. Long Polling без защиты от дубликатов
    При повторном соединении клиент может получить одни и те же данные. Нужно использовать last-event-id или маркер.
  3. Забывать про аутентификацию в вебсокете
    Handshake содержит HTTP-заголовки, но после установки соединения аутентификация по токену в query string при старте. Лучше использовать cookie или отправлять токен в первом сообщении.
  4. Проблемы с масштабированием вебсокетов
    Состояние соединения хранится на конкретном сервере; нужно sticky session или pub/sub брокер (Redis) для доставки сообщений нужному экземпляру.

Возможные дополнительные вопросы интервьюера

  • Как обрабатывать миллион одновременных WebSocket-соединений в Go?
    Использовать неблокирующий ввод-вывод, тюнинг ОС (file descriptors), эффективный epoll (к сожалению, стандартная библиотека Go для сети использует горутины, что создаёт по горутине на соединение; это возможно, но нужно контролировать память). Чаще используют специализированные библиотеки или C-решения.
  • Как SSE восстанавливается после разрыва?
    Браузерный EventSource автоматически переподключается и посылает заголовок Last-Event-ID, позволяя серверу продолжить с момента разрыва.
  • Почему SSE не подходит для отправки данных от клиента?
    SSE — однонаправленный поток; клиент может отправлять сообщения только через параллельные fetch-запросы или другой канал.

Как спроектировать систему уведомлений?

Краткий ответ (30–60 секунд)

Система уведомлений отправляет сообщения пользователям через различные каналы (push, email, SMS, in-app) на основе событий в системе. Нужен надёжный сбор событий, маршрутизация и рендеринг шаблонов, управление предпочтениями пользователей и троттлинг. Архитектура включает event bus, обработчики по типам каналов, шлюзы провайдеров и мониторинг доставки.

Развёрнутый ответ

Основные блоки:

  • Event Collector: получает события от бизнес-сервисов (заказ создан, сообщение в чате).
  • Notification Engine: применяет фильтры (подписан ли пользователь, не превышен ли лимит), выбирает шаблон, обогащает данными, формирует задачу для канала.
  • Channel Dispatchers: адаптеры к внешним провайдерам (FCM, APNs, Twilio, SMTP). Обеспечивают ретраи, rate limiting на стороне провайдера.
  • User Preferences и Opt-out: хранит настройки каналов и подписок.
  • Мониторинг доставки и аналитика: открытия, клики, жалобы.

В Go система может быть построена на асинхронных очередях (NATS, Kafka). Сервис-воркер читает событие, определяет список получателей, для каждого подготавливает сообщение и отправляет через соответствующий канал. Важно следить за идемпотентностью: одно событие может обработаться несколько раз. Используют БД для хранения уведомлений с состоянием (sent, delivered, failed).

Под капотом (internals)

Push-уведомления: HTTP/2 к APNs с аутентификацией JWT. FCM использует HTTP v1 API. В Go для отправки используют net/http с пулом соединений. Для массовых рассылок нужен worker pool, чтобы не исчерпать ресурсы соединений и соблюдать rate limit провайдера. Шаблоны рендерятся с помощью text/template или html/template. Ретраи с backoff при временных ошибках. В качестве хранилища предпочтений — быстрая key-value БД (Redis). События доставки (callback от провайдера) обрабатываются через webhook и обновляют состояние.

Типичные ошибки кандидатов

  1. Отсутствие троттлинга и дозирования
    Пользователь может получить 100 push за минуту и удалить приложение. Нужен лимит на пользователя в окне.
  2. Жёсткая синхронная отправка в цикле
    Задержка отправки одного push замедлит всю очередь. Всё должно быть асинхронно.
  3. Игнорирование локализации и часовых поясов
    Отправка уведомлений ночью бесит пользователей. Хранить предпочтения и местное время.
  4. Отсутствие обработки ошибок провайдера (invalid token)
    Если токен невалиден, его нужно удалить, иначе повторные попытки будут бесполезны и могут привести к блокировке провайдером.

Возможные дополнительные вопросы интервьюера

  • Как обеспечить последовательность доставки сообщений?
    Обычно не требуется; если нужна очередность, используют партиционирование по пользователю и строгий порядок в очереди.
  • Как следить за тем, чтобы уведомление не пропало при сбое?
    Хранить каждое уведомление как persistent запись со статусом, периодически переотправлять упавшие с увеличивающимся интервалом, в конце dead letter.
  • Чем отличается transactional email от marketing?
    Транзакционные — обязательные (сброс пароля), marketing — рекламные. Разные приоритеты и обработка отписок.

Как спроектировать сервис сокращения URL (URL shortener)?

Краткий ответ (30–60 секунд)

Сервис генерирует короткий уникальный ключ для длинного URL и редиректит на оригинал. Ключевые требования: высокая доступность, низкая задержка редиректа, уникальность ключа. Решение: хеширование с проверкой коллизий или распределённая генерация ID; хранение в key-value store; кэширование для горячих URL. Простой дизайн: REST API для создания/получения, 302 редирект.

Развёрнутый ответ

Алгоритмы генерации:

  • Случайный Base62 (a-zA-Z0-9) фиксированной длины (7-8 символов).
  • Хеш-функция (MD5, SHA-1) + взятие первых n бит, преобразование в Base62.
  • Распределённый генератор ID (Snowflake) с кодированием в Base62.
    Генерация должна быть идемпотентной, если для одного URL возвращается тот же ключ (custom alias может быть опциональным). API: POST /shorten с телом {"long_url": "..."} -> {"short_url": "..."}. GET /{key} -> 302 Redirect с Location. Для масштаба нужны: шардирование БД по ключу, кэш (Redis), репликация. В Go можно реализовать, используя net/http, генератор случайных строк crypto/rand. Отслеживание статистики кликов — отдельный поток, запись в аналитическую БД.

Под капотом (internals)

Кодирование Base62: цифры 0-9, буквы верхнего и нижнего регистра, исключая спецсимволы. В Go можно написать кодировщик с использованием big.Int и деления на 62. Для генерации уникального ключа часто применяют подход: сгенерировать случайный байтовый массив, закодировать в base62, проверить уникальность в БД через INSERT ... ON CONFLICT. Для высокой нагрузки используют предварительно сгенерированный пул ключей (key pool), загружая их в память и выдавая по мере надобности; синхронизируют маркер выдачи в Redis или БД. Редирект: достаточно 302, но можно 301 для SEO, если URL постоянен. Кэш: Redis с TTL = вечность для горячих.

Типичные ошибки кандидатов

  1. Использование только хеша от длинного URL без проверки коллизий
    MD5 даёт коллизии; урезанный хеш увеличивает вероятность. Нужно проверять уникальность в хранилище и при коллизии добавлять соль/ретрай.
  2. Забывать про валидацию URL
    Не проверять, что входящая строка — валидный URL с http/https, иначе возможны XSS, редиректы на внутренние ресурсы.
  3. Игнорировать идемпотентность
    Каждый новый запрос с тем же длинным URL создаёт новый ключ, раздувая БД. Лучше проверять существование и возвращать имеющийся (при условии, что пользователь не хочет кастомный).
  4. Неверный выбор статуса редиректа
    302 временный не кэшируется, 301 permanent кэшируется браузером; если нужна аналитика кликов, 302 предпочтительнее, чтобы запрос доходил до сервера.

Возможные дополнительные вопросы интервьюера

  • Как обеспечить уникальность в распределённой системе?
    Snowflake-like генератор на каждом экземпляре, либо централизованный сервис с Redis atomic increment.
  • Как предотвратить перебор коротких URL?
    Использовать длиннее ключи (8+ символов), rate limiting на создание и редирект.
  • Что делать, если ссылка удалена/заблокирована?
    Возвращать 410 Gone или 404, иметь флаг статуса в БД.

Как спроектировать чат (chat)?

Краткий ответ (30–60 секунд)

Чат — система обмена сообщениями между пользователями в реальном времени. Ключевые элементы: постоянное соединение (WebSocket), хранение истории, уведомления о новых сообщениях, статусы пользователей. Архитектура включает chat-сервер, message queue, БД для сообщений, push-уведомления для офлайн-пользователей. Для групповых чатов и медиафайлов добавляются соответствующие сервисы.

Развёрнутый ответ

Основные компоненты:

  • Chat Service: установка WebSocket соединения, аутентификация по токену, поддержка комнат (1-1 и групповые).
  • Message Broker (NATS/Redis pub-sub, Kafka): распределение сообщений между экземплярами сервиса. При получении сообщения сервер публикует его в канал комнаты; все подписанные экземпляры доставляют своим подключённым пользователям.
  • Message Store: реляционная БД (шардированная по chat_id) или NoSQL (Cassandra) для хранения истории с упорядочиванием по времени.
  • Notification Service: для пользователей, которые не в сети, отправляет push-уведомление через поставщиков.
  • Presence Service: отслеживает онлайн статус через heartbeat WebSocket и Redis TTL.
    В Go всё это реализуется горутинами: каждая WS-сессия обслуживается парой горутин (чтение/запись). Каналы для маршрутизации. Для масштабирования используется pub-sub Redis и шардирование.

Под капотом (internals)

WebSocket соединение сохраняется в map map[userID][]*websocket.Conn внутри экземпляра. При получении сообщения от клиента, сервер через Redis pub/sub опубликует сообщение; подписанные экземпляры прочитают и доставят в нужные соединения. Синхронизация карты: мьютекс или каналы. Для гарантии доставки при недоступности экземпляра нужно хранить сообщения в persistent БД, и при подключении клиента после разрыва доставлять пропущенные по last_message_id. Используется паттерн «mailbox» с курсором. Медиа сообщения загружаются через отдельный сервис, сохраняются в object storage, а в чат отправляется ссылка.

Типичные ошибки кандидатов

  1. Хранение состояния подключений в памяти без возможности горизонтального масштабирования
    Если пользователь подключён к серверу A, а сообщение приходит на B, без pub-sub доставить не сможем. Использовать брокер для кросс-серверной коммуникации.
  2. Отсутствие оффлайн-доставки
    Если получатель оффлайн, сообщение теряется. Нужно сохранять в БД и отправлять push.
  3. Непродуманная модель данных для истории
    Одна огромная таблица сообщений быстро становится узким местом. Шардировать по conversation_id, использовать временные партиции.
  4. Игнорирование порядка сообщений
    WebSocket гарантирует порядок в рамках одного TCP-соединения, но при переподключениях или нескольких отправителях в группе порядок может сбиться. Добавляют серверную метку времени/sequence number.

Возможные дополнительные вопросы интервьюера

  • Как реализовать «набирает сообщение» (typing indicator)?
    Отправлять событие по тому же WebSocket без сохранения в историю, через pub-sub.
  • Как организовать поиск по истории сообщений?
    Индексировать сообщения в Elasticsearch для полнотекстового поиска, асинхронно обновляя индекс.
  • Как спроектировать групповые чаты с тысячами участников?
    Не слать каждому индивидуально на уровне сервиса; использовать fan-out на уровне брокера или доставлять через long polling с подканалами.

Как спроектировать ленту новостей (news feed)?

Краткий ответ (30–60 секунд)

Лента новостей (как в Twitter/Facebook) — это персонализированный поток публикаций от подписок. Основные задачи: быстрое формирование ленты, низкая задержка доставки новых постов, масштабирование на миллионы пользователей. Решения: push-модель (fan-out on write) для активных пользователей, pull-модель (fan-out on read) для знаменитостей; кэширование сгенерированной ленты в Redis; использование очередей для асинхронной доставки.

Развёрнутый ответ

Архитектура:

  • Post Service: при публикации поста сохраняет его в БД, затем либо сразу рассылает (push) в ленты подписчиков, либо помечает для pull.
  • Feed Generation: для обычных пользователей — push: публикация добавляется в ранжированный список (Redis sorted set) каждого подписчика. Для знаменитостей (много подписчиков) — pull: при запросе ленты подписчика мерджится с постами знаменитостей из их таймлайнов.
  • Feed Service: читает кэшированную ленту, обогащает данными пользователей и медиа, пагинирует.
  • Асинхронная очередь: fan-out может быть медленным; ставится в очередь (Kafka), worker'ы обрабатывают.
    В Go реализация: post handler -> сохранить в БД -> отправить событие в NATS -> consumer обновляет feed cache (ZADD в Redis) для небольших списков подписчиков, либо помечает post id в отдельный сет для pull.

Под капотом (internals)

Push-модель: для каждого пользователя поддерживается Redis Sorted Set с ключом feed:<user_id>, элементы — post_id, score — временная метка. При добавлении нового поста, consumer воркер батчами выполняет ZADD feed:subscriber_id score post_id. Ограничивают размер ленты (обрезают старые). Pull: SQL запрос типа SELECT * FROM posts WHERE author_id IN (list of celebrities) ORDER BY created_at DESC. Комбинированная модель: горячие пользователи обрабатываются pull, обычные — push. Кэширование ленты: после генерации сохраняется сериализованный JSON в Redis с TTL, инвалидируется при новой публикации.

Типичные ошибки кандидатов

  1. Чистый push для всех, включая миллионников
    Запись в ленту миллиона подписчиков при каждом посте убьёт систему. Используют гибридный подход.
  2. Генерация ленты синхронно при запросе без кэша
    Латентность высокая, нагрузка на БД. Ленту должны формировать заранее или кэшироваться.
  3. Отсутствие пагинации и «бесконечный скролл»
    Нужно поддерживать курсорную пагинацию.
  4. Не учитывать удаление постов/апдейты
    При удалении у знаменитости нужно обновить кэш у подписчиков, что сложно. Используют мягкое удаление или асинхронную инвалидацию.

Возможные дополнительные вопросы интервьюера

  • Как ранжировать ленту (machine learning)?
    ML-модель присваивает score каждому посту, хранится рядом с post_id, в ленте используется ZRANGEBYSCORE.
  • Как обрабатывать частые обновления (лайки/комментарии) в ленте?
    Счётчики можно обновлять отдельным сервисом и не перестраивать всю ленту; клиент может дозапрашивать счётчики или получать push-обновления.
  • Как тестировать ленту с разнородными сценариями?
    Создавать синтетических «китов» и пользователей, измерять время доставки поста.

Как спроектировать систему лайков (like system)?

Краткий ответ (30–60 секунд)

Система лайков должна обрабатывать огромное количество быстрых мутаций (like/unlike) для множества объектов (посты, комментарии). Требуется высокая доступность и eventual consistency. Решение: хранить лайки в виде связки (user_id, object_id) в key-value хранилище (Redis, Cassandra), кэшировать счётчики, асинхронно обновлять агрегаты. API: POST /likes с телом объекта, DELETE для отмены.

Развёрнутый ответ

Для обеспечения производительности используют:

  • Redis Set/Hash для хранения факта лайка: SADD likes:object_id user_id — проверка уникальности, SREM — удаление. Счётчик — SCARD или отдельный ключ, инкрементируемый атомарно.
  • Асинхронная запись в основную БД (Cassandra: table likes with partition key object_id, clustering key user_id) через очередь изменений (change log), чтобы выдержать пиковые нагрузки (например, популярный пост).
  • Сервис лайков может быть тонким слоем перед Redis. При масштабировании шардирование Redis по object_id.
  • Клиенты отображают собственный статус лайка (лайкнул или нет) и общее количество. Количество может быть приблизительным.
    В Go API-хендлеры: проверить валидность, выполнить SADD, вернуть новое количество. При отмене — SREM. Если Redis недоступен, graceful degradation (показывать закэшированное значение).

Под капотом (internals)

Redis SADD выполняется за O(1), через хеш-таблицу, устойчив к race condition. При большом количестве лайков использование Set затратно по памяти; для каждого user_id можно хранить битую карту (Roaring Bitmap) для экономии. В Go при падении Redis можно локально кэшировать счётчики, но это приводит к дрейфу. Поэтому важно иметь персистентное хранилище (Cassandra), куда события попадают через Kafka: при like отправляется событие LikeEvent, которое consumer вставляет в БД и обновляет счётчик в отдельной таблице агрегатов. При этом счётчик в Redis остаётся ведущим для быстрого чтения.

Типичные ошибки кандидатов

  1. Использовать реляционную БД для хранения индивидуальных лайков с уникальным constraint
    На высоком RPS это создаст множество вставок, конфликтов, блокировок; нужна специализированная БД.
  2. Делать синхронный инкремент счётчика в одной транзакции с вставкой лайка
    Добавляет задержку; счётчик можно держать отдельно, допуская небольшую погрешность.
  3. Нет идемпотентности обработки событий
    При переобработке лайка счётчик увеличится дважды. Использовать upsert по user_id + object_id.
  4. Не обрабатывать «мгновенную отмену»
    Пользователь быстро лайкает и убирает; нужно корректно обрабатывать эти ситуации, возможно, используя состояние в кэше.

Возможные дополнительные вопросы интервьюера

  • Как спроектировать лайки с реакциями (like, love, haha)?
    Использовать хеш: HSET likes:object_id user_id reaction_type, счётчики по реакциям — HINCRBY reactions:object_id reaction_type 1.
  • Как реализовать топ постов по лайкам за последний час?
    Redis Sorted Set с временными метками, периодическая агрегация, или потоковая обработка (Kafka Streams).
  • Как проверять, лайкнул ли конкретный пользователь пост, при загрузке ленты?
    Client-side: запрос на batch проверку (SISMEMBER для нескольких object_id) или хранить в ленте флаг для текущего пользователя.

Как спроектировать систему рейтингов (rating system)?

Краткий ответ (30–60 секунд)

Система рейтингов собирает оценки пользователей (звёзды, баллы) для объектов (товары, фильмы) и вычисляет агрегированный рейтинг. Проблемы: честное усреднение, защита от накруток, задержка обновления. Подходы: простое среднее, байесовское среднее, алгоритм Wilson score. Хранилище: Redis для счётчиков, реляционная БД для сырых оценок. Асинхронная обработка через очереди.

Развёрнутый ответ

API: POST /ratings с параметрами object_id, score (1-5). Middleware проверяет, что пользователь имеет право оценить (например, купил товар). Оценка сохраняется в транзакционной БД с ограничением уникальности на user+object. Затем публикуется событие рейтинга. Агрегатор обновляет счётчики в Redis: INCRBY ratings:object_id:sum score, INCR ratings:object_id:count, и пересчитывает среднее. Для защиты от спама применяют rate limiting, анализ поведения. Для отображения часто используют взвешенные рейтинги. В Go cервис похож на систему лайков, но с числовым значением. Может использоваться sync/atomic для локальных кэшей агрегатов.

Под капотом (internals)

Байесовское среднее: WR = (v / (v+m)) * R + (m / (v+m)) * C, где v — количество оценок, m — порог, C — среднее по всем объектам. Расчёт можно делать при запросе рейтинга объекта в SQL запросе или pre-computed. В высоконагруженных системах оценочные данные записываются в log (Kafka), consumer обновляет агрегаты в Cassandra/Redis. Для борьбы с накрутками нужен outlier detection. В Go может быть реализован как pipeline обработки.

Типичные ошибки кандидатов

  1. Показывать простое среднее для объектов с малым числом оценок
    Объект с одной оценкой 5 будет выше, чем с 500 оценками 4.9. Используют взвешенное среднее.
  2. Позволять многократное голосование без аутентификации
    Нужно привязать к пользователю и обеспечить уникальность.
  3. Игнорировать временной фактор
    Старые оценки могут быть менее актуальны; можно использовать затухание по времени.
  4. Синхронная запись в БД при каждом голосовании без очереди
    При пиках рейтинговая система откажет. Лучше применить асинхронную обработку.

Возможные дополнительные вопросы интервьюера

  • Как пересчитать рейтинг, если пользователь изменил свою оценку?
    Атомарный апдейт: вычесть старую сумму, добавить новую. В Redis через Lua-скрипт.
  • Как агрегировать рейтинги по категориям для топ-листов?
    Использовать Sorted Set в Redis с ключом категории, обновлять ZADD при каждом изменении рейтинга.
  • Как предотвратить накрутку ботами?
    Капча, ограничение частоты, ML-модели, анализ паттернов.

Краткий ответ (30–60 секунд)

Поисковая система индексирует документы и отвечает на текстовые запросы с релевантными результатами. Ключевые компоненты: индексатор, обратный индекс, поисковый движок. Для большинства приложений используют готовые решения (Elasticsearch, Solr), а сервис поиска является обёрткой, обеспечивающей интеграцию, обогащение и ранжирование. Важно учесть масштабирование индекса, релевантность и обновления в реальном времени.

Развёрнутый ответ

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

  • Ingestion Pipeline: получает события изменений данных (через CDC или application events), преобразует документы, и отправляет в Elasticsearch для индексации.
  • Search API: принимает запросы пользователей с параметрами (текст, фильтры, сортировка, пагинация), строит ES-запрос, обогащает результаты (подгружает недостающие поля, права доступа), возвращает клиенту.
  • Ранжирование: может использовать встроенное BM25 или ML-ранжирование.
    В Go реализуют клиент к Elasticsearch (olivere/elastic, официальный elastic/go-elasticsearch). Сервис обычно stateless и масштабируется горизонтально. Индексация асинхронная, чтобы не задерживать основной поток. Для поиска по логам — отдельный кластер. Важны настройки: шардирование, репликация, near real-time поиск.

Под капотом (internals)

Elasticsearch основан на Apache Lucene; данные хранятся в сегментах, обратный индекс использует FST (Finite State Transducer). Индексация сначала попадает в буфер памяти, потом записывается в Lucene сегмент; поиск видит новые данные после refresh (по умолчанию 1 сек). В Go при построении запросов важно избегать инъекций, экранировать пользовательский ввод. Для снижения нагрузки на ES используют кэширование популярных запросов.

Типичные ошибки кандидатов

  1. Использовать ES как основную базу данных (source of truth)
    ES не транзакционна, возможна потеря данных; источник истины должен быть в ACID-хранилище, ES — только для поиска.
  2. Неоптимальный дизайн индекса
    Один индекс для всего, неправильные mapping’и, взрыв поля. Нужно проектировать mapping под поисковые запросы.
  3. Отсутствие защиты от тяжелых запросов
    Deep pagination, агрегации без ограничений, запросы с большим size могут убить кластер. Устанавливать лимиты.
  4. Игнорирование безопасности на уровне документов
    Если разные пользователи имеют права на разные документы, нужно фильтровать в запросе или использовать document-level security.

Возможные дополнительные вопросы интервьюера

  • Как реализовать поиск с опечатками (fuzzy)?
    Использовать нечёткий запрос (Levenshtein distance) или suggest API.
  • Как синхронизировать удаление данных?
    При удалении документа в источнике отправлять событие и удалять из ES по ID.
  • Как обрабатывать обновление индекса без даунтайма?
    Использовать алиасы и переиндексацию с нуля на новый индекс, затем переключить алиас.

Как спроектировать e-commerce платформу?

Краткий ответ (30–60 секунд)

E-commerce платформа включает каталог товаров, корзину, оформление заказа, платежи, инвентарь, доставку. Архитектура — микросервисная, с разделением по доменам. Высоконагруженные части: каталог (поиск, фильтры), страница товара, оформление заказа. Нужна масштабируемость, отказоустойчивость, консистентность критичных данных (платежи). Ключевые паттерны: CQRS, Event Sourcing, Sagas для заказа.

Развёрнутый ответ

Основные сервисы:

  • Product Catalog Service: хранение товаров, категорий, атрибутов. Индексация в Elasticsearch для поиска.
  • Shopping Cart Service: может быть реализован в Redis, привязан к сессии/пользователю, с TTL.
  • Order Service: оркестрирует процесс создания заказа: проверка наличия, резервирование, расчёт стоимости, создание заказа. Использует Saga для координации платежа и резервирования.
  • Payment Service: интеграция с платёжными шлюзами, идемпотентность.
  • Inventory Service: управление остатками, резервирование на время заказа.
  • Shipping Service: логистика.
  • Notification Service: email, sms.
    API Gateway для клиентов, BFF для web/mobile. В Go каждый сервис может использовать свой стек, коммуникация через gRPC/асинхронные события.

Под капотом (internals)

Корзина в Redis: HSET cart:{user_id} product_id json_data. При оформлении заказа выполняется Saga: начинается с заморозки товаров (inventory reserve), затем создаётся заказ в статусе Pending, затем платёж. При успехе — подтверждение резервирования, при ошибке — отмена резервирования и отмена платежа. Для предотвращения повторной оплаты используется идемпотентный ключ. Поиск товаров — ES. В Go сагу можно реализовать с использованием Temporal или ручного оркестратора с хранением состояния в БД.

Типичные ошибки кандидатов

  1. Недооценка сложности инвентаря и гонки за последний товар
    Без распределённых блокировок и optimistic locking возможна продажа одного и того же товара разным пользователям. Использовать версионирование.
  2. Синхронное обращение ко всем сервисам при проверке корзины
    Высокая задержка; применять асинхронность и eventual consistency, показывая пользователю подтверждение после успешного резерва.
  3. Отсутствие обработки ошибок платежа
    Если платёж прошёл, а заказ не создался из-за сбоя, пользователь теряет деньги. Нужно вести reconciliation и компенсацию.
  4. Неправильное проектирование каталога (медленные запросы)
    Использовать денормализацию и кэширование, CDN для статики.

Возможные дополнительные вопросы интервьюера

  • Как реализовать скидки и промокоды?
    Отдельный Pricing/Promotion сервис, который применяет правила к корзине при оформлении заказа, с транзакционной обработкой.
  • Как обрабатывать отмену заказа после отправки?
    Saga с компенсационными шагами: запрос на возврат, обновление статуса, возврат денег.
  • Как масштабировать сервис заказов?
    Шардировать по customer_id или order_id, использовать асинхронные очереди, CQRS.

Как спроектировать платежную систему?

Краткий ответ (30–60 секунд)

Платёжная система обрабатывает денежные транзакции между пользователями и продавцами, взаимодействуя с эквайерами, шлюзами и банками. Критичны: надёжность, идемпотентность, точность, безопасность и аудируемость. Архитектура обычно включает Payment Gateway, Payment Processor, Ledger (книга учёта), и риск-менеджмент. Система синхронно-асинхронная с гарантией согласованности.

Развёрнутый ответ

Компоненты:

  • Payment API: принимает запросы на проведение платежа (authorize + capture), refund, отмену. Каждый запрос идемпотентен с ключом.
  • Payment Gateway: абстрагирует взаимодействие с разными провайдерами (Stripe, Adyen), унифицирует интерфейс.
  • Payment Processor: маршрутизирует транзакции, управляет состоянием (конечный автомат: Authorized, Captured, Refunded...), хранит состояние в БД.
  • Ledger Service: журнал проводок, double-entry accounting, обеспечивающий баланс. Часто использует event sourcing.
  • Fraud Detection: анализирует в реальном времени.
    В Go для гарантий используем транзакции при смене состояния платежа и записи в леджер. Для масштабирования БД шардируется по merchant_id или payment_id.

Под капотом (internals)

Состояние платежа — конечный автомат. Переходы атомарны: UPDATE payments SET status='captured', version=version+1 WHERE id=? AND version=?. В случае ошибки шлюза повторяем с экспоненциальным backoff, но только для idempotent операций. Леджер — неизменяемый, записи только добавляются. Для отчётности CQRS. В Go используем database/sql с драйвером PostgreSQL; для обработки асинхронных ответов от шлюзов используем webhook с проверкой подписи, после чего обновляем состояние.

Типичные ошибки кандидатов

  1. Отсутствие double-entry и аудита
    Простое хранение баланса может привести к несоответствиям; нужен леджер с транзакциями.
  2. Ретраи без идемпотентности
    Повторные запросы на capture при timeout приведут к двойному списанию. Идемпотентный ключ обязателен.
  3. Игнорирование частичных захватов и возвратов
    Сумма может быть частично захвачена, нужна модель с суммами.
  4. Плохая обработка недоступности провайдера
    Транзакция должна зависнуть в статусе pending с возможностью ручного разрешения или автоматического реконсилиации.

Возможные дополнительные вопросы интервьюера

  • Как обеспечить баланс и сверку с платёжным провайдером?
    Ежедневная сверка (reconciliation) через загрузку отчётов провайдера и сравнение с ledger.
  • Что такое двухфазный захват (authorize + capture)?
    Authorize резервирует средства на карте, capture — фактическое списание. Может быть отложенным.
  • Как обрабатывать валюты и конвертацию?
    Использовать актуальные курсы от поставщика, хранить сумму в исходной валюте и в валюте расчёта, фиксировать курс при транзакции.

Что такое "non-functional requirements" в System Design?

Краткий ответ (30–60 секунд)

Non-functional requirements (NFR) описывают, как система должна работать, а не что она делает. Это качественные характеристики: производительность (latency, throughput), масштабируемость, доступность, надёжность, безопасность, maintainability, observability, совместимость. В system design интервью они ограничивают выбор архитектуры и технологий.

Развёрнутый ответ

Ключевые NFR:

  • Производительность: время отклика (p95, p99), пропускная способность (RPS).
  • Масштабируемость: способность расти с нагрузкой, горизонтальное vs вертикальное.
  • Доступность: uptime (99.9%, 99.99%), failover, disaster recovery.
  • Надёжность: корректная работа даже при сбоях (fault tolerance).
  • Безопасность: аутентификация, авторизация, шифрование, комплаенс.
  • Сопровождение (maintainability): логирование, мониторинг, лёгкость деплоя.
  • Observability: метрики, трассировка, логи.
  • ** Durability**: гарантия сохранности данных.
    На собеседовании кандидат должен сразу спрашивать про NFR, оценивать цифры и проектировать под них. Например, "лайки: 10k QPS, задержка < 100ms, доступность 99.9%".

Под капотом (internals)

Реализация NFR в Go: конкурентность и производительность за счёт горутин; безопасность — crypto/tls, securecookie; observability — экспорт метрик Prometheus, трассировка OpenTelemetry. Доступность обеспечивается graceful shutdown через signal.NotifyContext. Масштабируемость — stateless сервисы, кэши. Durability — репликация БД, WAL. В архитектуре это выливается в выбор конкретных паттернов (circuit breaker, bulkhead, etc.).

Типичные ошибки кандидатов

  1. Игнорировать NFR и сразу рисовать диаграмму
    Не спросив про нагрузку, доступность, кандидат может предложить неадекватное решение. Нужно начинать с NFR.
  2. Отсутствие конкретных цифр
    "Система должна быть быстрой" — не метрика. Всегда приводить численные ориентиры.
  3. Концентрация только на функциональности
    Вопросы про мониторинг, CI/CD, безопасность могут быть не менее важны. Показать широту мышления.
  4. Противоречие между NFR
    Например, strict consistency и высокая доступность (CAP). Надо уметь объяснять trade-off.

Возможные дополнительные вопросы интервьюера

  • Как обеспечить observability в микросервисной архитектуре?
    Стандартизированный формат логов (структурированные), distributed tracing (W3C Trace Context), единый metric endpoint.
  • Какие метрики важны для API?
    Request rate, errors rate, duration (RED). Также бизнес-метрики.
  • Что такое SLO и как его выбрать?
    Service Level Objective — цель по надёжности, например, 99.9% успешных запросов за месяц; выбирается на основе user happiness.

Как оценить нагрузку (QPS, Storage, Bandwidth)?

Краткий ответ (30–60 секунд)

Оценка нагрузки — back-of-the-envelope расчеты, используемые для выбора компонентов архитектуры. Основные метрики: Queries Per Second (QPS) — количество запросов в секунду; Storage — объём хранимых данных на определённый период; Bandwidth — пропускная способность сети. Подход: понять user actions → перевести в запросы → применить коэффициенты (пиковые нагрузки, рост за год) → рассчитать потребности в ресурсах.

Развёрнутый ответ

Пример: сервис сокращения URL.

  • Допустим, 100 миллионов новых URL в месяц, читают в 10 раз больше.
  • Запись: 100M/ (30*86400) ≈ 40 запросов/сек. Чтение: 400 запросов/сек.
  • Пик: можно утроить.
  • Storage: 100M * (короткая строка + длинная + метаданные) ~ 500 байт = 50 ГБ в месяц, ~600 ГБ в год. Учитываем репликацию.
  • Bandwidth: 400 rps * средний размер ответа ~ 200 байт = 80 КБ/с (мало).
    В Go можно провести тесты (benchmark) для уточнения. один запрос может порождать несколько внутренних. Использовать нотацию: 1 М = 10^6, 1 день = 86 400 сек (можно ~10^5 для упрощения). Показывать умение прикидывать быстро.

Под капотом (internals)

Расчёты опираются на понимание накладных расходов протоколов: TCP overhead, TLS, HTTP-заголовки. В Go net/http/pprof позволяет профилировать реальную пропускную способность. Знание возможностей железа: 1 Гбит/с сеть ≈ 125 МБ/с, SSD IOPS. Память: Redis 1 млн ключей ~ 100 МБ.

Типичные ошибки кандидатов

  1. Не учитывать пиковые нагрузки
    Средний QPS 100, а пик может быть 1000 из-за распродажи. Система ляжет. Дизайн под пик.
  2. Слепо применять средние значения вместо перцентилей
    Latency p99 важен, а не среднее. Игнорировать tail latency.
  3. Забывать про фактор репликации и бэкапов в storage
    Хранение 3-х копий утраивает потребность.
  4. Ошибаться в единицах измерения
    Путать байты и биты (bandwidth в битах), неверно переводить периоды.

Возможные дополнительные вопросы интервьюера

  • Сколько памяти нужно для хранения 1 млн пользовательских сессий?
    Сессия ~ 2 КБ, итого 2 ГБ, плюс overhead Redis — около 3-4 ГБ.
  • Как рассчитать необходимую пропускную способность для стриминга видео 4К?
    Битрейт ~ 25 Мбит/с на пользователя; для 1000 одновременных — 25 Гбит/с.
  • Что делать, если оценки показывают, что сервер не справится?
    Рассмотреть горизонтальное масштабирование, шардирование, CDN.

Какие основные trade-off'ы при выборе БД?

Краткий ответ (30–60 секунд)

Выбор между реляционной (SQL) и нереляционной (NoSQL) БД, а также между конкретными типами (key-value, документная, column-family, графовая) управляется trade-off'ами: консистентность vs доступность (CAP), нормализация vs денормализация, производительность чтения/записи, гибкость схемы, сложность запросов, опыт команды. SQL даёт ACID, сложные запросы; NoSQL — горизонтальное масштабирование и гибкость.

Развёрнутый ответ

Основные измерения:

  • Модель данных: реляционная для структурированных связей; документная (MongoDB) для гибких схем; key-value (Redis, DynamoDB) для простых lookups; column-family (Cassandra) для time-series; графовая (Neo4j) для связных данных.
  • Масштабирование: SQL вертикальное, труднее шардировать; большинство NoSQL спроектированы для горизонтального scale-out.
  • Транзакции: SQL — ACID, complex transactions; NoSQL — eventual consistency, ограниченные гарантии (ACID некоторые могут давать).
  • Консистентность: выбираем между строгой (CP) и eventual (AP).
  • Производительность: NoSQL могут обеспечить миллионы операций в секунду для простых паттернов.
  • Стоимость эксплуатации: поддержка кластера Cassandra сложнее, чем управляемый RDS.

При проектировании необходимо проанализировать паттерны доступа и требования к данным, после чего выбрать БД. В рамках одного проекта часто используют polyglot persistence.

Под капотом (internals)

PostgreSQL использует MVCC с версионированием строк; Cassandra — LSM-деревья и compaction; Redis — однопоточный event loop с асинхронной персистентностью. Эти механизмы влияют на поведение под нагрузкой. В Go драйверы для PostgreSQL (pgx) используют пул соединений и бинарный протокол. Для NoSQL — клиенты с connection pool.

Типичные ошибки кандидатов

  1. «NoSQL — серебряная пуля»
    Многие выбирают MongoDB, потому что она модная, но теряют консистентность и реляционные возможности. Нужно обосновывать.
  2. Игнорирование паттернов доступа
    Выбрали Cassandra для частых джойнов — будет мучительно медленно.
  3. Недооценка операционных сложностей
    Установить кластер Cassandra легко, управлять им — нет. Лучше managed service.
  4. Пренебрежение персистентностью в памяти
    Использование Redis как primary БД без сохранения на диск рискованно.

Возможные дополнительные вопросы интервьюера

  • Почему бы не использовать PostgreSQL для всего?
    PostgreSQL подходит для многих случаев, но при экстремальных нагрузках, требующих горизонтального масштабирования записи, может потребоваться NoSQL (например, Cassandra).
  • Когда нужна графовая БД?
    Глубокие связи (социальные графы, рекомендации) с запросами типа «друзья друзей» неэффективны в SQL; графовая БД делает это быстрее.
  • Что такое полиглотное персистентное хранилище?
    Использование разных БД для разных задач в одной системе.

Как спроектировать "idempotency key" механизм?

Краткий ответ (30–60 секунд)

Idempotency key — уникальный строковый ключ, отправляемый клиентом для обеспечения идемпотентности мутирующих операций. Сервер должен атомарно сохранить ключ с результатом выполнения. При первом поступлении ключа выполняется операция, ответ сохраняется. При повторе ключа операция не выполняется, а возвращается сохранённый ответ. Ключ хранится в быстром хранилище (Redis) или БД с уникальным constraint, с TTL.

Развёрнутый ответ

Механизм включает:

  • Клиент генерирует UUID (или другой уникальный идентификатор) для каждой уникальной операции и передаёт в заголовке Idempotency-Key.
  • Серверная middleware извлекает ключ, проверяет его наличие в хранилище:
    • Если ключ отсутствует: атомарно вставляем запись со статусом processing, выполняем бизнес-логику, по завершении обновляем запись результатом (status completed + ответ).
    • Если ключ существует и статус completed — возвращаем сохранённый ответ.
    • Если статус processing — ждём или возвращаем 409.
  • Атомарность вставки обеспечивается SETNX в Redis или INSERT ... ON CONFLICT DO NOTHING в SQL.
  • TTL для ключа должен быть достаточно длинным (24-48 часов), чтобы пережить ретраи, но не вечным, чтобы не переполнять хранилище.

В Go реализация: middleware -> проверка Redis -> вызов бизнес-обработчика -> сохранение ответа. Для сложных операций используют транзакции.

Под капотом (internals)

Redis: SET idempotency_key value NX EX ttl. При получении OK — ключ новый, при nil — уже существует. В этом случае нужно прочитать сохранённое значение. Использование Lua-скрипта для атомарного check-and-set: скрипт проверяет ключ, если nil — записывает processing и возвращает статус «продолжить»; если processing — возвращает «wait»; если completed — возвращает тело. В Go redis.Client может выполнить Eval. PostgreSQL: таблица idempotency(id UUID PK, state enum, response_body BYTEA, created_at). В транзакции: INSERT INTO idempotency VALUES ... ON CONFLICT DO NOTHING. Если INSERT вернул 0 строк — ключ уже есть.

Типичные ошибки кандидатов

  1. Хранение ключа только в памяти процесса
    При перезапуске сервера все гарантии теряются. Обязательно внешнее хранилище.
  2. TTL слишком короткий
    Клиент может повторить запрос после того, как ключ был удалён, что приведёт к повторному выполнению. Нужно время жизни, большее максимальной задержки ретрая.
  3. Отсутствие обработки параллельных запросов
    Два запроса с одним ключом одновременно прочитают отсутствие ключа и выполнят операцию дважды. Использовать атомарные операции.
  4. Неполное сохранение ответа
    Сохранять только тело и код, но не заголовки (например, Location при создании ресурса). Повторный ответ должен быть точной копией первого.

Возможные дополнительные вопросы интервьюера

  • Как быть, если бизнес-операция частично выполнена, а сервер упал до сохранения результата?
    Нужно делать операцию и сохранение ключа и результата в одной транзакции, если позволяет БД, либо использовать паттерн background reconciliation.
  • Можно ли использовать timestamp как idempotency key?
    Нет, т.к. при повторном вызове в ту же миллисекунду возможны коллизии или другой порядок. Лучше UUID.
  • Как клиенту передавать ключ при неидемпотентных операциях (POST)?
    Именно для таких операций ключ критичен. Клиент генерирует и включает в заголовок.

Как работает "consistent hashing" и где применяется?

Краткий ответ (30–60 секунд)

Consistent hashing — алгоритм распределения ключей по узлам в динамически изменяющемся наборе серверов. В отличие от простого modulo, при добавлении/удалении узла перераспределяется только ~1/N ключей, а не почти все. Узлы (и виртуальные узлы) располагаются на хеш-кольце. Ключ хешируется, ищется ближайший узел по кольцу (по часовой стрелке). Применяется в распределённых кэшах (Memcached), CDN, распределённых БД (Cassandra, DynamoDB).

Развёрнутый ответ

Идея: хеш-функция отображает и ключи, и идентификаторы узлов в одно кольцевое пространство (например, 0..2^32-1). Для поиска узла для ключа вычисляется h(key), ищется узел с ближайшим хешем, большим или равным h(key) (если такого нет, берётся первый узел). При добавлении узла он берёт на себя часть ключей от соседа. Для равномерного распределения используют виртуальные узлы: каждый физический узел представлен несколькими точками на кольце (обычно 100-200). При добавлении физического узла равномерно распределённые виртуальные узлы забирают ключи от многих других узлов.
В Go реализация: библиотеки hashring или самописный код с crc32/md5, sort.Search для поиска по кольцу. Используется в шардировании кэша Redis cluster, в gRPC балансировщике (ringhash).

Под капотом (internals)

Структура данных: отсортированный массив хешей виртуальных узлов с указанием на реальный узел. Добавление узла: генерируем N виртуальных узлов, вычисляем хеши, вставляем с сортировкой. Поиск — бинарный поиск hash в срезе, если выход за границы — берём первый. В Go для потокобезопасности используется sync.RWMutex. При удалении узла ключи, принадлежавшие ему, автоматически перейдут следующему узлу на кольце; но данные нужно физически перенести, если это кэш — они просто будут загружены заново.

Типичные ошибки кандидатов

  1. Не использовать виртуальные узлы
    Без них нагрузка распределяется неравномерно, и при добавлении узла «горячие» точки остаются.
  2. Применять weak hash-функцию
    Плохая функция (например, простая сумма байт) даёт несбалансированное распределение. Используют MD5/SHA-1 (не криптографически, но равномерно) или xxhash.
  3. Забывать, что данные не перемещаются автоматически
    Consistent hashing определяет только владельца; при изменении топологии данные нужно либо повторно закэшировать, либо мигрировать. Кандидат должен различать применение.
  4. Неправильный размер кольца (collision)
    Маленькое пространство хешей может привести к коллизиям. Размер должен быть достаточно большим.

Возможные дополнительные вопросы интервьюера

  • Как Cassandra использует consistent hashing?
    Cassandra назначает каждый узел токеном на кольце, использует virtual nodes (num_tokens) для балансировки.
  • Как добавить вес узлу (гетерогенные серверы)?
    Назначить больше виртуальных узлов более мощному серверу.
  • Что такое jump hash и когда он предпочтительнее?
    Jump consistent hash — эффективный алгоритм без хранения кольца, но не поддерживает произвольное удаление узлов; хорош для шардирования БД с фиксированным набором.

Что такое "gossip protocol"?

Краткий ответ (30–60 секунд)

Gossip (сплетни) — децентрализованный протокол распространения информации в кластере, вдохновлённый распространением слухов. Узлы периодически обмениваются данными с несколькими случайно выбранными соседями. Информация распространяется экспоненциально быстро (как virus) с высокой устойчивостью. Применяется для обнаружения членов (Serf, consul), распространения конфигурации, распределения метрик. Характеризуется eventual consistency и надёжностью.

Развёрнутый ответ

У каждого узла есть членский список (membership list) с версиями. Протокол обычно включает:

  • Ping/Ack: проверка живости.
  • Indirect Ping: через другие узлы для уменьшения нагрузки.
  • Gossip messages: периодически (каждые 1-2 с) узел выбирает несколько случайных узлов и отправляет сводку известных состояний (push), или запрашивает изменения (pull), или комбинированно (push-pull). При получении обновлений с большей версией обновляет своё состояние.
    Используется в Consul, Cassandra, Riak. В Go известная библиотека memberlist от HashiCorp (используется в Serf). Она поддерживает обнаружение, обработку dead-узлов, custom делегаты.

Под капотом (internals)

memberlist использует SWIM протокол (Scalable Weakly-consistent Infection-style process group Membership). SWIM разделяет failure detection и dissemination. Периодический ping случайного узла; если не ответил — косвенный ping через k других узлов, если всё равно нет — объявляется подозрительным, gossip распространяет. Gossip идёт по push-pull: выбирается несколько случайных узлов, передаётся полный state map с версиями; получатель мерджит, отправляет обратно свою версию (push-pull). В Go memberlist использует UDP с подтверждениями, горутины для периодического пробы. Состояние распространяется за O(log N) раундов.

Типичные ошибки кандидатов

  1. Считать gossip мгновенным
    Распространение занимает несколько раундов, что может привести к временному рассогласованию. Не для strong consistency.
  2. Игнорировать проблему split-brain
    Gossip сам по себе не решает проблему лидера и консенсуса; нужен дополнительный алгоритм (Raft, Paxos).
  3. Не настраивать частоту и размер gossip-пакетов
    Слишком частый gossip создаёт избыточную нагрузку, редкий — медленное обнаружение отказов.
  4. Путать gossip и broadcast
    Это вероятностный протокол, не гарантирующий доставку всем, но высокая вероятность.

Возможные дополнительные вопросы интервьюера

  • Как gossip протокол справляется с ложными срабатываниями?
    Используется подтверждение через косвенные пинги, таймауты и пороги.
  • Где применяется gossip в Kubernetes?
    Не напрямую, но etcd использует Raft; однако некоторые CNI плагины используют gossip.
  • Как оценочно время распространения?
    Логарифмически: O(log N) раундов, где каждый раунд ~ задержка сети * коэфф. Для 1000 узлов ~ 3-5 секунд.

Как работает "leader election"?

Краткий ответ (30–60 секунд)

Leader election — процесс выбора одного узла-координатора среди группы равноправных узлов. Нужен для избежания конфликтов (split-brain) при планировании задач, записи в shared ресурс. Реализуется через консенсус-алгоритмы (Raft, Paxos) или внешние координаторы (etcd, ZooKeeper, Consul). Лидер периодически посылает heartbeats; при сбое запускаются новые выборы.

Развёрнутый ответ

Сердце: алгоритм Raft (используется в etcd). Узлы находятся в состояниях: follower, candidate, leader. Follower ожидает heartbeats от лидера. Если таймаут выборов истекает, follower становится candidate, увеличивает терм, голосует за себя и просит голоса у других. Если получает большинство голосов — становится лидером и начинает рассылать heartbeats. Если голоса разделились — новая попытка с случайным таймаутом. В распределённых системах лидер используется для планировщика, записи в БД, блокировок. В Go проще всего использовать etcd client concurrency API: concurrency.NewElection(session, "/prefix"), который внутри реализует на основе lease и watch.

Под капотом (internals)

В etcd election API: каждый кандидат создаёт lease и кладёт свой ключ в prefix с WithLease. Затем пытается создать значение; ключ с наименьшим revision считается лидером. Все участники watch на prefix с меньшим revision. При удалении (lease expires) следующая в порядке становится лидером. Это оптимистичный lock-free election. В Go библиотека go.etcd.io/etcd/client/v3/concurrency предоставляет Election.Campaign() и Observe(). Для самодельных решений можно использовать Redis с Redlock, но это не так надёжно.

Типичные ошибки кандидатов

  1. Реализация лидера через shared DB без блокировок
    Может привести к двойному лидерству из-за race condition.
  2. Отсутствие обработки устаревших лидеров
    Старый лидер, не зная о своём свержении, может продолжать писать, вызывая повреждение данных. Нужен fencing token.
  3. Игнорирование задержек и таймаутов
    Слишком маленький election timeout вызывает бесконечные выборы, слишком большой — долгое время без лидера.
  4. Выбор единственного координатора для данных вместо горизонтального масштабирования
    Лидер становится узким местом; возможно, стоит использовать шардирование.

Возможные дополнительные вопросы интервьюера

  • Что такое fencing token?
    Монотонно возрастающее число (терм + индекс), которое лидер прикрепляет к каждой операции; хранилище отвергает операции с устаревшим токеном.
  • Как etcd гарантирует linearizable read?
    Read запросы обрабатываются лидером с подтверждением кворума.
  • Чем отличается leader election в Kafka от Raft?
    Kafka использует ZooKeeper для выбора контроллера, KRaft (новый) собственную реализацию Raft.

Что такое "heartbeat" в распределенной системе?

Краткий ответ (30–60 секунд)

Heartbeat — периодический сигнал от узла к монитору или другим узлам, подтверждающий его работоспособность. Используется для обнаружения отказов, membership, и поддержания lease (аренды). При отсутствии heartbeats в течение таймаута узел считается мёртвым, что запускает ребалансировку/перевыборы. Может быть push (узел шлёт) или pull (монитор опрашивает).

Развёрнутый ответ

Применение:

  • Lease: узел в etcd создаёт lease с TTL и периодически обновляет KeepAlive. Если keepalive прекращается, lease истекает, связанные ключи удаляются, что используется для выборов лидера.
  • Service Health Check: балансировщик пингует /health endpoint.
  • Gossip: узлы обмениваются heartbeats для обнаружения (SWIM ping).
    Обычно heartbeat сочетается с таймаутом и порогом (несколько пропущенных подряд). В Go реализуют периодическую горутину, выполняющую client.KeepAlive или time.Ticker для отправки пакетов. При реализации нужно избегать блокировок в отправке.

Под капотом (internals)

etcd keepalive: клиент запускает отдельную горутину, которая отправляет LeaseKeepAlive запрос по gRPC стриму. Сервер etcd продлевает lease и шлёт ответ. При разрыве соединения клиент реконнектится. В Go etcd clientv3 предоставляет Lease.KeepAlive канал. При реализации своего протокола (UDP heartbeat) сервер слушает, хранит map последнего времени получения от каждого узла; отдельная горутина проверяет устаревшие записи и помечает как dead. Используют sync.Map для конкурентного доступа.

Типичные ошибки кандидатов

  1. Heartbeat без таймаутов и порогов
    Одиночная потеря heartbeat не должна сразу убивать узел; нужен порог (например, 3 подряд).
  2. Блокирующий heartbeat
    Если запрос на отправку ждёт ответа долго, а сеть тормозит, может заблокировать основной поток и создать ложное срабатывание. Асинхронность обязательна.
  3. Использование системного времени без учёта дрейфа
    Сравнение времени на разных узлах требует учёта clock skew; лучше опираться на монотонные часы или lease, управляемый сервером.
  4. Забыть про graceful shutdown
    При остановке сервиса нужно явно отозвать lease или завершить heartbeat, чтобы быстрее освободить ресурсы.

Возможные дополнительные вопросы интервьюера

  • Чем lease отличается от простого heartbeat?
    Lease даёт гарантию, что ресурс принадлежит узлу до определённого срока, и отзыв происходит атомарно при истечении TTL, даже если узел пропал.
  • Как масштабировать heartbeat мониторинг на тысячи узлов?
    Использовать gossip (децентрализованно) или распределённый мониторинг (Consul).
  • Как реализовать двусторонний heartbeat?
    Оба узла отправляют друг другу; так оба узнают о доступности обеих сторон.

Как спроектировать систему "единого входа" (SSO)?

Краткий ответ (30–60 секунд)

Single Sign-On позволяет пользователю аутентифицироваться один раз и получать доступ к нескольким приложениям без повторного ввода учётных данных. Центральный Identity Provider (IdP) аутентифицирует пользователя и выдаёт токен (обычно JWT или SAML assertion), который доверенные сервисы (Service Providers) валидируют. Протоколы: OAuth2 + OpenID Connect, SAML. Архитектура включает сервер авторизации, клиентов, ресурс-серверы.

Развёрнутый ответ

При использовании OIDC:

  1. Пользователь пытается зайти в приложение (SP).
  2. SP перенаправляет на IdP (Login page).
  3. IdP аутентифицирует (пароль, MFA), выдаёт Authorization Code.
  4. SP обменивает code на ID Token (JWT) и Access Token.
  5. SP создаёт локальную сессию на основе ID Token.
  6. Для других приложений, если уже есть сессия на IdP, повторной аутентификации не требуется (единая сессия).
    Реализация IdP: Keycloak, Okta, самописный на Go с использованием библиотек fosite, ory/hydra. Важна безопасность: HTTPS, CSRF protection, PKCE. Сессия IdP обычно хранится в cookie на его домене. Logout инициирует завершение сессий на всех подключённых SP.

Под капотом (internals)

OpenID Connect — слой идентификации поверх OAuth2. ID Token — JWT с claims (sub, iss, aud, exp). Подписывается приватным ключом IdP. SP валидирует подпись, проверяет aud, срок действия. В Go можно использовать middleware, который проверяет JWT через github.com/lestrrat-go/jwx или golang-jwt. Для централизованного IdP можно создать свой сервер, реализующий OIDC flow, с БД пользователей. Keycloak написан на Java, но Go-решения (Hydra) тоже хороши.

Типичные ошибки кандидатов

  1. Реализация собственного SSO без использования стандартных протоколов
    Уязвимо и несовместимо. Нужно OIDC/SAML.
  2. Отсутствие PKCE для публичных клиентов
    Мобильные и SPA без client secret должны использовать PKCE, чтобы предотвратить перехват кода.
  3. Некорректное хранение сессии IdP
    Если хранить только в памяти, масштабирование теряется; нужно хранить в Redis с общей state.
  4. Не обрабатывать Single Logout (SLO)
    Выход только из одного приложения оставляет сессию в других. Требуется backchannel logout или frontchannel.

Возможные дополнительные вопросы интервьюера

  • Чем отличается OAuth2 от OIDC?
    OAuth2 — авторизация, OIDC — аутентификация (добавляет ID Token).
  • Как реализовать SSO для нативных мобильных приложений?
    Использовать браузер для аутентификации с PKCE.
  • Что такое backchannel logout?
    Механизм, при котором IdP отправляет сообщение напрямую (backchannel) SP о необходимости завершения сессии.

Как работает OAuth 2.0 и OpenID Connect?

Краткий ответ (30–60 секунд)

OAuth 2.0 — фреймворк авторизации, позволяющий приложениям получать ограниченный доступ к ресурсам пользователя без раскрытия пароля. Участники: Resource Owner (пользователь), Client, Authorization Server, Resource Server. Основные flow: Authorization Code (с PKCE), Implicit (устарел), Client Credentials. OpenID Connect (OIDC) — это тонкий слой поверх OAuth 2.0 для аутентификации, добавляющий ID Token (JWT) с информацией о пользователе.

Развёрнутый ответ

Authorization Code Flow:

  1. Client перенаправляет браузер на Authorization Server с параметрами (client_id, redirect_uri, scope, state).
  2. Пользователь аутентифицируется и даёт согласие.
  3. AS возвращает redirect с Authorization Code.
  4. Client обменивает code (вместе с client secret) на Access Token (и Refresh Token) через прямой запрос к token endpoint.
  5. Client использует Access Token для доступа к Resource Server.
    OIDC добавляет openid scope, при обмене кода возвращается ID Token (JWT), содержащий identity пользователя (sub, name, email). ID Token подписан, его можно проверить локально. В Go популярна библиотека fosite, реализующая OAuth2/OIDC provider; клиентская часть — golang.org/x/oauth2.

Под капотом (internals)

Access Token обычно opaque, проверяется через introspection endpoint или локально (если JWT). ID Token — JWT с подписью RS256 или HS256. В Go валидация — разбор JWT, проверка подписи через JWKS endpoint (набор ключей). Authorization Server по стандарту должен поддерживать динамическое получение метаданных (.well-known/openid-configuration). Для предотвращения CSRF используется state-параметр.

Типичные ошибки кандидатов

  1. Путать Access Token и ID Token
    Access Token — для доступа к API (не нужно клиенту понимать содержимое), ID Token — для идентификации пользователя. Нельзя отправлять ID Token как авторизационный заголовок.
  2. Игнорирование проверки подписи JWT
    Принимать JWT без проверки подписи — критическая уязвимость.
  3. Не использовать PKCE для публичных клиентов
    Даже в Authorization Code Flow без client_secret обязателен PKCE (code challenge/verifier).
  4. Не проверять aud (audience)
    Токен может быть предназначен другому приложению. Всегда проверять aud claim.

Возможные дополнительные вопросы интервьюера

  • Как обновить Access Token?
    Использовать Refresh Token, отправленный вместе с Access Token, на token endpoint для получения нового.
  • Что такое scope в OAuth?
    Ограничивает, к каким ресурсам и правам имеет доступ токен; например, read:profile, write:messages.
  • В чем разница между implicit и authorization code flow?
    Implicit flow возвращал токен напрямую (в URL фрагменте), что небезопасно; сейчас устарел, рекомендуется Authorization Code с PKCE даже для SPA.

Как спроектировать "content delivery network" (CDN)?

Краткий ответ (30–60 секунд)

CDN — распределённая сеть edge-серверов для доставки контента с низкой задержкой. Проектирование включает: выбор архитектуры (pull-based кэширование), маршрутизацию (GeoDNS, anycast), иерархию кэшей (edge, shield, origin), стратегию инвалидации, безопасность (TLS, DDoS защита), мониторинг. Ключевые компоненты: origin storage, edge-прокси, конфигурация DNS, purge API.

Развёрнутый ответ

Если мы создаём свой CDN (хотя обычно используют готовый), основные элементы:

  • Origin: object storage (S3) или кластер серверов с содержимым.
  • Edge Nodes: распределённые по географическим регионам, кэшируют контент на диске/в памяти.
  • Request Routing: DNS-сервер возвращает IP ближайшего edge, учитывая загрузку и доступность (anycast упрощает).
  • Cache Policies: на основе HTTP-заголовков Cache-Control, TTL, варианты (Vary). Поддержка conditional requests (ETag).
  • Invalidation: API для очистки кэша при обновлении контента; может быть мягким (удаление из кэша) или жёстким (purge).
  • TLS Termination: edge-узлы держат сертификаты.
  • Shielding: промежуточный кэш между edge и origin для снижения нагрузки.
  • Logging/Analytics: сбор логов для биллинга и мониторинга.
    В Go edge-узел можно реализовать на базе httputil.ReverseProxy с кэширующим middleware, хранящим данные в ristretto или boltdb.

Под капотом (internals)

Реализация кэширования: локальное хранилище (быстрый SSD) с ключом = cache key (URL + заголовки). Используются Bloom-фильтры для быстрого определения отсутствия объекта. При промахе запрос отправляется на origin (если shield available, сначала туда, потом origin). Управление дисковым кэшем: LRU вытеснение, фрагментация. Для TCP-оптимизации используют BBR congestion control. В Go для high-performance edge применяют fasthttp (не рекомендуется) или тонко настраивают стандартный net/http. Распределённый purge: при вызове purge API publisher отсылает сообщение в очередь, edge-серверы подписаны и удаляют ключи.

Типичные ошибки кандидатов

  1. Не учитывать стоимость инвалидации
    Purge всех edge может быть медленным; лучше tag-based инвалидация или CDN с малым TTL.
  2. Проксирование большого трафика через единственный origin без shield
    Origin упадёт под нагрузкой; shield-кэш (intermediate) спасает.
  3. Игнорирование безопасности edge
    Не фильтровать запросы, открытый proxy — угроза.
  4. Отсутствие мониторинга здоровья edge-узлов
    DNS должен исключать нерабочие узлы.

Возможные дополнительные вопросы интервьюера

  • Как CDN обрабатывает DDoS по запросам?
    Anycast распределяет трафик, rate limiting, scrubbing центры, challenge-response.
  • Как обеспечить консистентность между origin и edge?
    Контент считается stale после TTL; для динамики используют cache bypass или очень короткий TTL.
  • Зачем нужен CDN для API?
    Для ускорения ответов по всему миру, снижения задержки через оптимизацию TCP/TLS, кэширование неизменных ответов.

Что такое "bloom filter" и как он применяется в кэшировании?

Краткий ответ (30–60 секунд)

Bloom filter — вероятностная структура данных для проверки принадлежности элемента множеству. Позволяет быстро определить, что элемента точно нет в множестве, но может дать ложное срабатывание (элемент возможно есть). Занимает мало памяти. В кэшировании используется для уменьшения числа холостых запросов к источнику: если bloom filter говорит «нет», то ключа точно нет в БД/кэше, и запрос даже не выполняется (cache penetration protection).

Развёрнутый ответ

Bloom filter представляет собой битовый массив размера m и k хеш-функций. При добавлении элемента вычисляются k хешей, устанавливаются соответствующие биты в 1. При проверке: если все k битов == 1, элемент возможно в множестве; если хотя бы один 0 — точно отсутствует. Вероятность ложного срабатывания регулируется параметрами m, k и числом элементов. Не поддерживает удаление (есть counting bloom filter). Применение:

  • Защита кэша: перед запросом в Redis/БД проверяем bloom filter ключа; если отрицательно, сразу возвращаем, избегая ненужного запроса.
  • Базы данных (Cassandra) — для поиска в SSTable.
  • CDN — проверка наличия объекта в edge.
    В Go библиотека willf/bloom (in-memory) или redisbloom (Redis модуль).

Под капотом (internals)

Структура в Go: фильтр состоит из BitSet (массив uint64) и набора хеш-функций (обычно используют double hashing: h1, h1 + h2*i mod m). Добавление и проверка атомарны через мьютекс. При использовании распределённого bloom filter (Redis) применяют SETBIT. Для масштабируемых фильтров — Scalable Bloom Filter, добавляющий новые слои при заполнении. В кэшировании bloom filter периодически перестраивается на основе актуального набора ключей.

Типичные ошибки кандидатов

  1. Использовать bloom filter как точный инструмент
    Не учитывать false positive; в некоторых кейсах это допустимо, но важно понимать последствия.
  2. Не обновлять фильтр при удалении ключей из кэша
    Фильтр врёт, что ключ ещё есть; запрос уходит в пустоту, но это всего лишь потеря перформанса.
  3. Неправильный выбор параметров (m, k) для ожидаемого n
    Слишком маленький битовый массив даёт высокий FPR. Нужно рассчитывать.
  4. Хранение фильтра в памяти приложения и горизонтальное масштабирование
    Каждый экземпляр имеет своё состояние, FPR может различаться; лучше иметь централизованный фильтр.

Возможные дополнительные вопросы интервьюера

  • Как рассчитать оптимальные параметры bloom filter?
    k = (m/n) * ln(2); m = -(n * ln(p)) / (ln(2)^2), где p — желаемая вероятность ошибки.
  • Что такое counting bloom filter?
    Вместо битов — счётчики; позволяет удаление, но занимает больше памяти.
  • Как использовать bloom filter для защиты от D читателей?
    Фильтр ключей, которых гарантированно нет в БД (negative cache) — если запрос не проходит, сразу 404.

Как спроектировать "key-value store"?

Краткий ответ (30–60 секунд)

Key-value store — простая БД, хранящая пары ключ-значение с операциями Get, Put, Delete. Дизайн включает: in-memory index (хэш-таблица или B-дерево), персистентное хранение на диске (WAL + SSTable/LSM-дерево), стратегии компактификации, репликацию с консистентностью (CP/AP). Примеры: Redis, RocksDB, Dynamo. В Go можно реализовать хранилище с map + WAL, или использовать BadgerDB.

Развёрнутый ответ

Проект встроенного KV-хранилища:

  • Memory Index: sync.Map или хеш-таблица с сегментами для конкурентного доступа. Хранит ссылки на местоположение на диске.
  • Storage Engine: LSM-дерево — записи попадают в активный memtable, при его заполнении сбрасываются в SSTable на диске; чтение сначала memtable, потом SSTable (с bloom-фильтрами). Альтернатива — B+ дерево (BoltDB).
  • WAL (Write Ahead Log): каждая запись сначала в WAL для durability.
  • Compaction: слияние SSTable в фоне для освобождения места и удаления устаревших записей.
  • Replication: Raft для кластера.
    В Go можно использовать BadgerDB (реализация LSM-дерева с транзакциями ACID). Проектирование: API сервиса над таким движком, поддержка пагинации по префиксу, TTL.

Под капотом (internals)

BadgerDB (написана на Go) использует vaulted copy-on-write LSM-дерево, где value сохраняется отдельно (vlog) для снижения write amplification. Memtable — skiplist. Чтение использует bloom-фильтры для пропуска SST. WAL — это отдельный vlog, который циклически перезаписывается. В Go для конкурентного доступа применяют sync.RWMutex на операциях с memtable или lock-free подходы. При проектировании собственного KV можно вдохновляться bitcask (log-structured hash table): ключ-индекс в памяти, значения на диске в append-only логах.

Типичные ошибки кандидатов

  1. Игнорировать durability
    Думать, что in-memory map достаточно. При падении данные теряются. WAL обязателен.
  2. Забывать о конкурентности
    Обычная map с мьютексом на весь store становится узким местом. Нужно шардирование (сегментированный lock).
  3. Неправильное удаление: tombstone
    При удалении нельзя просто убрать из памяти; нужно записать tombstone, чтобы при восстановлении или в репликах ключ не появился снова.
  4. Выбор структуры без понимания trade-off
    LSM — быстрое письмо, медленное чтение (особенно при большом количестве SST); B+дерево — сбалансировано, но больше write amplification.

Возможные дополнительные вопросы интервьюера

  • Как реализовать транзакции в KV?
    MVCC, snapshot isolation, WAL с записями транзакций; в BadgerDB транзакции уже реализованы.
  • Как выполнять range-запросы (сканирование по префиксу)?
    Данные на диске сортированы, индекс поддерживает поиск по диапазону (skiplist/B-дерево).
  • Как спроектировать распределённый KV-store?
    Использовать consistent hashing для шардирования, репликацию с кворумом (R + W > N) для настройки консистентности.

Что такое "SLA", "SLO", "SLI"?

Краткий ответ (30–60 секунд)

Это три кило Service Reliability:

  • SLI (Service Level Indicator) — количественная метрика качества сервиса (latency, error rate, throughput). Например, latency p95 < 300ms.
  • SLO (Service Level Objective) — целевое значение SLI за период, с которым сервис должен работать. Например, 99.9% запросов в месяц должны иметь latency < 300ms.
  • SLA (Service Level Agreement) — договорное обязательство перед клиентами с последствиями за нарушение SLO (компенсации). Обычно SLO немного строже SLA.

Развёрнутый ответ

SLI выбираются так, чтобы отражать пользовательский опыт. SLO задаёт допустимый error budget: если SLO 99.9%, то бюджет на ошибки 0.1% времени ≈ 43 минуты в месяц. Это позволяет балансировать между надёжностью и скоростью разработки (feature velocity). SLA — внешний контракт, обычно публичный. Например, AWS S3 SLA обещает 99.9% доступности, ниже — кредиты. При проектировании системы мы ориентируемся на SLO: 99.9% или 99.99%, что определяет архитектурные решения (fault tolerance, репликация).

Под капотом (internals)

В Go SLI собираются через библиотеки expvar, Prometheus metrics. SLO можно вычислять с помощью Prometheus recording rules: avg_over_time(latency_seconds{quantile="0.95"}[30d]). Error budget автоматически отслеживается, при его исчерпании вводятся ограничения на деплой. В Google это описано в концепции Site Reliability Engineering (SRE). При дизайне системы нужно учитывать, как измерять SLI и какой трейдофф принять для достижения SLO.

Типичные ошибки кандидатов

  1. Путать SLO и SLA
    SLO — внутренняя цель; SLA — внешнее обязательство. Не должно быть SLO строже, чем SLA, иначе частые нарушения.
  2. Игнорировать error budget
    Говорить, что система должна быть «100% доступной», нереалистично. Нужно явно определить бюджет на ошибки.
  3. Выбор неправильного SLI
    Средняя задержка — плохой индикатор, tail latency важнее. Выбирают p95/p99.
  4. Несоответствие архитектуры и SLO
    Требуют 99.99%, но предлагают архитектуру без географической репликации и автоматического failover.

Возможные дополнительные вопросы интервьюера

  • Как error budget влияет на скорость разработки?
    Если бюджет израсходован, фриз деплоев; команды мотивированы повышать надёжность.
  • Какие SLI подходят для конвейера обработки данных?
    Свежесть данных (latency обработки), процент успешных обработок, пропускная способность.
  • Как устанавливать SLO?
    На основе пользовательских исследований, исторических данных, бизнес-ценности; начинать с реалистичных и ужесточать.

Как работает "graceful degradation" и "fail-fast" на уровне системы?

Краткий ответ (30–60 секунд)

Graceful degradation — способность системы продолжать предоставлять ограниченный функционал при отказе некоторых компонентов (например, отключить рекомендации, когда сервис рекомендаций упал). Fail-fast — принцип немедленно сообщать об ошибке и прекращать обработку, вместо того чтобы скрывать сбой или пытаться продолжать в некорректном состоянии. Оба повышают отказоустойчивость: fail-fast предотвращает каскадные сбои, а graceful degradation сохраняет полезность системы.

Развёрнутый ответ

В микросервисах пример fail-fast: если сервис A вызывает сервис B, у которого истёк deadline, A сразу возвращает ошибку клиенту, не накапливая очередь и не блокируя ресурсы. Реализуется через таймауты, circuit breaker. Graceful degradation: если сервис рейтингов недоступен, интернет-магазин может показывать товары без рейтинга, но не падать. Для этого архитектура предусматривает резервные варианты: fallback-данные, кэш, статические значения. В Go часто используют паттерн «fallback» с дефолтными ответами. Важно определять критический путь: без него сервис не может функционировать (проверка оплаты), его отказ фатален; некритические компоненты деградируют.

Под капотом (internals)

Реализация в Go: для каждого внешнего вызова задаём context.WithTimeout; при ошибке возвращаем err, но для fallback можно обернуть вызов: if err != nil { return defaultResponse, nil }. Circuit breaker может переключаться на fallback. Для graceful degradation часто используют bulkhead паттерн — изолируют пул ресурсов для критичных операций. В обработчике HTTP: recover от паники, чтобы не обрушить весь сервер.

Типичные ошибки кандидатов

  1. Отсутствие fallback вообще
    Реклама валится — весь сайт падает. Нужно всегда продумывать, как деградировать.
  2. Fail-fast без таймаутов
    Ожидание ответа от упавшего сервиса бесконечно — не fail-fast. Обязательно задавать дедлайны.
  3. Слишком агрессивная деградация
    Показывать устаревшие или пустые данные без информирования пользователя, что сервис не работает, может привести к путанице.
  4. Путать graceful degradation и retry storm
    При получении ошибки пытаться повторить запрос несколько раз без backoff — fail-slow, а не fail-fast.

Возможные дополнительные вопросы интервьюера

  • Как протестировать graceful degradation?
    Chaos engineering: отключать сервисы и проверять, что система продолжает работать.
  • Что делать, если fallback-данные устарели и вводят в заблуждение?
    Показывать баннер о том, что данные могут быть неактуальны, и ограничивать изменения.
  • Как выбрать, какие функции деградировать первыми?
    Определять приоритеты бизнеса: панель «рекомендации» может исчезнуть, корзина — нет.

Как спроектировать "database per service" паттерн в микросервисах?

Краткий ответ (30–60 секунд)

Database per service — паттерн, при котором каждый микросервис владеет своей собственной БД (логически отдельной, может быть отдельной таблицей, схемой или экземпляром СУБД). Никакой другой сервис не имеет прямого доступа к этим данным. Взаимодействие — только через API/события. Это обеспечивает изоляцию, независимое масштабирование и технологическое разнообразие, но требует решения проблем распределённых транзакций и крос-сервисных запросов.

Развёрнутый ответ

Реализация:

  • Каждый сервис деплоится с собственной БД (например, отдельная PostgreSQL schema/database или отдельный инстанс).
  • Сервис A не может напрямую читать БД сервиса B даже для отчетов; он должен либо вызывать API B, либо подписываться на события изменений, создавая собственную read-only копию (материализованное представление).
  • Схема данных свободна эволюционировать без влияния на другие сервисы.
  • Миграция: каждая БД управляется отдельными миграциями (golang-migrate, goose в Go) в рамках CI/CD сервиса.
    Проблемы: транзакционная целостность между сервисами (Saga), eventual consistency, необходимость в API Composition для join-like запросов. Для аналитики используют data lake, куда стекаются события из всех сервисов.

Под капотом (internals)

В Go типичный сервис: main подключается к БД, используя sql.Open с DSN из переменных окружения. Миграции выполняются при старте. Для изоляции используют разные учётные данные БД, ограничивающие доступ. События публикуются в Kafka после успешного коммита (outbox table). Для read-only копий сервисы слушают события и обновляют свои проекции. Для обеспечения уникальности в рамках распределённой системы используют UUID вместо автоинкремента.

Типичные ошибки кандидатов

  1. Нарушение изоляции: прямое подключение к чужой БД ради «быстрого отчёта»
    Создаёт сильную связность, ломает автономность. Используют API или репликацию данных.
  2. Совместная БД с разделением на схемы, но с общим доступом
    Технически возможно, но всё равно есть связность на уровне схемотехники и миграций. Лучше полностью отдельные БД.
  3. Игнорирование сложности распределённых запросов
    Поиск заказов по конкретному пользователю может потребовать обращения к нескольким сервисам и сборки данных на уровне API Gateway/BFF.
  4. Не продумано резервное копирование и восстановление
    Множество БД требуют централизованного управления бэкапами.

Возможные дополнительные вопросы интервьюера

  • Как обновлять данные, если нужно атомарное обновление в двух БД?
    Использовать Saga или Event Sourcing, обеспечивая eventual consistency.
  • Можно ли использовать одну и ту же технологию БД для всех сервисов?
    Да, главное — логическая изоляция; полиглотность — опция.
  • Как избежать дублирования данных и проблем с консистентностью read-проекций?
    Проекции — осознанное дублирование; необходимо следить за order и идемпотентностью.