Системный дизайн в Go — асинхронность не бывает бесплатной
Пока ваш сервис вызывает функции внутри одного процесса, всё просто — вызов либо вернул результат, либо упал, и компилятор следит за типами. Системный дизайн начинается там, где этой простоты больше нет: компоненты общаются через брокер, данные лежат в разных базах, а между записью в БД и публикацией события успевает случиться падение процесса. Здесь интервьюер проверяет не знание модного слова, а способность рассуждать о согласованности под асинхронностью.
Главная ловушка темы — переносить интуицию синхронного вызова на распределённую систему. В монолите запись и публикация события атомарны, потому что это один вызов. Стоит разнести их по двум системам — и атомарности больше нет: появляется dual-write, появляется частичный сбой, появляется лишь итоговая согласованность. Вторая ловушка — вера в «бесплатные» гарантии: что брокер сам обеспечит exactly-once, что одна ACID-транзакция охватит две базы, что повтор доставки можно просто игнорировать. Эта тема разбирает системный дизайн по слоям — от стиля общения компонентов до того, как именно сообщение доходит ровно один раз по эффекту.
Карта темы
- Событийно-ориентированная архитектура — компоненты общаются через брокер событиями, а не прямыми вызовами; что это даёт и чем расплачивается.
- Слоистая архитектура — разделение на транспорт, домен и хранилище и правило зависимостей, направленных внутрь.
- Паттерн adapter — стабильный внутренний интерфейс, за которым прячется изменчивый внешний API или формат.
- DTO против доменной сущности — транспортный контейнер данных против носителя бизнес-состояния, инвариантов и поведения.
- Паттерн outbox — как одна локальная транзакция решает проблему dual-write, и почему saga нужна, когда баз несколько.
- Гарантии доставки сообщений — at-most-once, at-least-once и effectively-once, и почему идемпотентность обязательна при повторах.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Считать событийно-ориентированную систему сильно согласованной | Ложные ожидания — по природе она лишь итогово согласована |
| Думать, что события — это просто логирование | Упустить, что событие и есть реальный канал общения компонентов |
| Направлять зависимости наружу — домен импортирует драйвер БД | Бизнес-правила привязаны к хранилищу, край не заменить |
| Путать слои с отдельно развёртываемыми сервисами | Неверная модель — слои живут внутри одного процесса |
Переиспользовать одну struct как DTO и доменную сущность | JSON-теги и форма API текут в бизнес-логику |
| Считать, что relay из outbox публикует внутри той же транзакции | Неверная модель — relay работает отдельно, после коммита |
| Верить, что outbox требует распределённый 2PC | Упустить главное — его смысл обойтись одной локальной транзакцией |
| Менять местами at-least-once и at-most-once | at-least-once задваивает, at-most-once теряет — не наоборот |
| Считать, что брокер даёт сквозной exactly-once бесплатно | Дубликаты дойдут до потребителя без его дедупликации |
Слать ACK до обработки сообщения | Падение посреди обработчика теряет сообщение — брокер не повторит |
| Считать, что одна ACID-транзакция охватит две базы данных | Изоляция кончается на границе БД — нужна saga или 2PC |
Значение для собеседований
Системный дизайн — обязательная тема на senior-уровне Go-интервью, и спрашивают не схему на доске, а понимание согласованности. Интервьюер проверяет, переносите ли вы вслепую интуицию монолита на распределённую систему.
Что обычно проверяют:
- Чем событийно-ориентированная архитектура платит за слабую связанность — асинхронность, сложная отладка, лишь итоговая согласованность.
- Правило слоистой архитектуры — зависимости направлены внутрь, доменный слой не импортирует ни транспорт, ни БД.
- Зачем нужен adapter — поглотить изменение внешнего API в одном месте за стабильным интерфейсом.
- Почему DTO и доменная сущность — разные типы, и что протекает, если их объединить.
- Что такое проблема dual-write и как паттерн outbox решает её одной локальной транзакцией.
- Чем at-least-once отличается от at-most-once и почему at-least-once безопасен лишь с идемпотентным обработчиком.
- Как поддерживать согласованность двух сервисов с разными базами — saga и компенсирующие транзакции, а не один ACID.
Типичный неверный ответ: «брокер сам гарантирует exactly-once, потребителю ничего делать не надо». Это запускает разбор того, что сквозной exactly-once в распределённой системе практически недостижим и приближается как effectively-once — at-least-once плюс дедупликация по idempotency key на стороне потребителя.