Модуль 1. Когда монолиту пора: границы, а не мода
Перед началом: курс «Мессенджер: P2P, группы и продакшен» — монолит задеплоен, релизный конвейер работает (стадия 1). Инструменты: репозиторий трека (boilerplate), docker. Трудоёмкость: ~4 ч.
🔥 Инцидент: «релиз стикеров уронил чат всем»
Пятница, выкатываем стикеры: новая фича тянет за собой тяжёлый JSON-каталог и заметно прожорливее остального API. Через минуту после релиза дежурный видит две картины сразу: p99 REST-эндпоинтов вырос, а график WS-соединений пошёл пилой — тысячи клиентов переподключаются. Причина проста: WS-хаб живёт в том же процессе, что и API; горутины кодирования стикеров съели CPU, хаб не успевал читать сокеты, health-проверка начала флапать, оркестратор рестартовал поды — и каждый рестарт рвал все соединения всех пользователей. Откатили стикеры — чат ожил, хотя стикеры к чату отношения не имеют.
Это и есть технический шов: у realtime-доставки другой профиль нагрузки — десятки тысяч долгоживущих соединений, чувствительность к паузам GC, свой график релизов. А у бизнес-фич — свой. Пока они в одном процессе, любой релиз любой фичи — это релиз чата. Первый этап переезда снимет именно эту боль; но прежде чем что-то резать, нужно понять, где проходят границы.
📖 1.1 Что стадия 1 уже подготовила
Переезд начинается не с Kubernetes, а с устройства монолита. В boilerplate курса 1 уже лежат четыре решения, которые сделают вынос дешёвым:
- Интерфейс брокера. Все подписки идут через
events.Broker— сегодня за ним in-memory pub/sub, завтра будет Redis Streams. Ни один юзкейс не знает, как устроена доставка. - Outbox-конверт. То, что монолит кладёт в outbox-таблицу, — уже событие с типом и payload. Это будущий конверт шины, дословно.
- WS-протокол с
seq. Клиент синхронизируется по номерам сообщений, а не по факту «соединение живо». Потеря транспорта — не потеря данных. - JWT. Токены уже не привязаны к сессии в памяти — их можно будет валидировать без похода в чужую БД (останется только сменить подпись).
Проверьте это на своей копии: найдите в internal/ интерфейс брокера и
структуру outbox-события. Если бы shов не было, первым модулем пришлось бы
делать именно их — а это уже другой курс.
📖 1.2 Bounded contexts: карта домена
Границы сервисов — это границы языков, а не папок. В домене мессенджера их четыре:
| Контекст | Тип | Язык | Сервис |
|---|---|---|---|
| Identity & Access | supporting | аккаунт, токен, профиль, контакт, блокировка | user-service |
| Messaging | core | диалог, сообщение, seq, участие, роль, прочитано | messenger-service |
| Realtime Delivery | generic | соединение, подписка, fan-out, presence, конверт | event-service ×N |
| Notifications | supporting | push-подписка, «кого будить», агрегация | notification-worker |
Типы поддоменов подсказывают, куда вкладываться: core — то, чем продукт отличается (Messaging), туда идут лучшие люди и самое внимательное проектирование; supporting — нужно, но не различает (учётки, пуши); generic — решённая задача (доставка realtime): берём готовые паттерны и не изобретаем свою шину.
Отношения между контекстами — тоже контракт: user-publishes user.updated,
остальные ведут каталоги карточек; messenger и event связаны только
версионированным конвертом (Published Language). И правило карты:
события назад не текут — presence и typing живут в Realtime Delivery и не
становятся доменными событиями Messaging, их потеря — норма.
📖 1.3 Тест границы: почему p2p и группы — один сервис
Популярное предложение — развести messenger-p2p-service и
messenger-group-service. Проверим тестом контекста: «меняется ли значение
слов?» «Сообщение» в личке и «сообщение» в группе — одно слово с одними
инвариантами: лента с seq, курсорная пагинация, непрочитанные,
дедупликация по client_msg_id, outbox-конвейер. Механизмы совпадают
целиком:
| Механизм | direct | group | Общий? |
|---|---|---|---|
Лента, seq, курсоры | да | да | ✅ один код, одна таблица |
| Outbox → шина → доставка | да | да | ✅ один конвейер |
| Read-позиции, непрочитанные | да | да | ✅ одна формула |
| Участники | ровно 2, без ролей | N, роли | ⚠️ capability, не язык |
| Название | из собеседника | title | ⚠️ отображение |
| Приглашения, модерация | нет | да | ⚠️ только у групп |
Различия — capability и отображение, а не язык. Это два агрегата одного контекста. Резать их — получить distributed monolith: единый список диалогов придётся собирать на клиенте из двух сервисов, конвейер сообщений и read-состояние — дублировать двумя одинаковыми миграциями, а релизы «p2p» и «group» всё равно ходят парами (контракт, доставка, UI списка — общие).
Честные причины таких предложений решаются дешевле:
- «группы сложнее, нужна отдельная команда» → модуль
membershipвнутри messenger-service и владение пакетом; - «большие группы дают нагрузку на fan-out» → это нагрузка доставки — решается в event-service (стратегии fan-out, модуль 5), а не разрезом хранилища сообщений;
- «хочется масштабировать хранение» → шардирование по
conversation_idтем же кодом: ось — данные, а не тип диалога.
Membership — единственный реальный кандидат на выход в будущем, и для него
есть точный критерий: свой язык (ban, appeal, trust level), свои потребители
(модерация, антиабьюз) и свой темп релизов — одновременно. До тех пор —
модуль; вынос позже дешев, потому что membership уже публикует
member.changed и не участвует в транзакции записи сообщения.
📖 1.4 Что НЕ выделяем
- ❌ presence-service — присутствие неразрывно от соединений: это event-service.
- ❌ read-service / write-service — CQRS-читалка уже живёт на клиенте
(sync по
seq); отдельный read-сервис появится вместе с поиском. - ❌ api-gateway-сервис — роутинг по путям делает nginx; gateway понадобится, когда придут внешние клиенты с другими экранами (BFF).
- ❌ сервис реакций — реакция не существует без сообщения: тот же язык, та же граница изменений; это таблица и модуль в messenger-service.
Общий приём: когда просят «выделить сервис X», проведите тест границы. Если язык общий и релизы ходят вместе — это модуль, и это хорошая новость: модуль дешевле сервиса на порядок.
📖 1.5 Компромиссы
- Монолит vs сервисы. Монолит даёт транзакционность и нулевую сетевую цену; сервисы — независимые деплои и профили масштабирования. Переезд оправдан, когда боль конкретна (как WS-хаб в инциденте), а не «на вырост».
- Доменная граница vs технический шов. user и messenger режем по языку домена; event-service — по техническому шву (stateful-соединения). Оба разреза законны, но путать их нельзя: шов не создаёт языка.
- Готовность vs поспешность. Каждый вынос — цена эксплуатации ×N (мониторинг, деплой, дежурство). Правило курса: один этап — одна боль, подтверждённая метрикой.
💻 Практика
- Найдите швы (30 мин). В своей копии boilerplate найдите
events.Brokerи структуру outbox-события. Выпишите, какие типы событий уже существуют и у кого подписки. Это карта готовности к переезду. - Поймайте боль (30 мин). Рестартните монолит при 50 открытых WS-соединениях (свой скрипт из курса 1). Запишите: сколько соединений потеряно, за сколько клиенты переподключились, что увидел пользователь. Это ваш «до» для модуля 5.
- Нарисуйте карту (45 мин). Возьмите свой рабочий проект (не мессенджер) и разложите его по контекстам: язык, тип поддомена (core/supporting/ generic), владелец. Отметьте, где сейчас граница проходит через транзакцию — это будущая боль.
- Тест границы (30 мин). Проведите тест для трёх предложений: «вынести реакции в сервис», «вынести группы в сервис», «вынести push в воркер». Два ответа — «модуль», один — «воркер». Объясните каждый ответ языком границ, а не вкусами.
🧩 Quiz
✅ Итоги модуля
- Назвать четыре контекста домена и тип каждого (core/supporting/generic).
- Провести тест границы по языку и линиям изменений на любом предложении «вынести сервис».
- Объяснить, почему p2p и группы — один сервис, и чем это грозит при разрезе.
- Показать в boilerplate швы, которые подготовил курс 1: брокер, outbox, seq, JWT.
- Записать измеримую боль монолита («до») для первого этапа переезда.
🔗 Мостик к Модулю 2
Границы нарисованы, боли зафиксированы. Теперь соберём из контекстов целевую топологию: кто чем владеет, кто кого вызывает, где стоит nginx и почему на пути клиента не появится ни одной распределённой транзакции — и увидим, что из четырёх сервисов три появились почти бесплатно.