Монолит не умер. Вы просто плохо его написали
Почему микросервисы по умолчанию – плохая идея. Modular monolith паттерн, критерии перехода.
Один из наших клиентов — финтех-платформа в СНГ с ~200K активных пользователей и командой из 15 инженеров — обратился к нам с классической проблемой роста: система перестала масштабироваться вместе с бизнесом. Не технически — технически она справлялась. Масштабирование сломалось организационно: четыре команды работали в одном репозитории и постоянно блокировали друг друга.
Ниже — честный разбор того, что мы нашли, что сделали и что получилось. Без приукрашивания и без умолчания о том, где было сложно.
Система — монолитное Django-приложение, ~450K строк кода, которому на момент начала работы было около 6 лет. За эти годы в него складывали всё: платежи, KYC-верификацию, уведомления, аналитику, административные инструменты, интеграции с внешними провайдерами. Единая база данных PostgreSQL — 800 таблиц, часть из которых потеряла актуальную документацию ещё несколько лет назад.
Деплой происходил раз в две недели и каждый раз превращался в событие. Не потому что процесс был плохой — CI/CD был настроен, тесты писались. Проблема была в том, что любой деплой касался всего: одна ошибка в миграции платёжного модуля могла заблокировать выкатку независимой фичи в KYC-процессах. Время от коммита до прода — 14 дней. Для финтеха, который конкурирует с банковскими суперапами, это было критично.
Проблема была не в монолите. Проблема была в том, что монолит не имел никаких границ внутри себя — это был Big Ball of Mud с хорошим CI/CD.
Команда выросла с 8 до 15 инженеров за год — и вместо ускорения разработки получили замедление. Классический симптом: чем больше людей работает в одном пространстве без явных границ, тем выше координационные издержки.
Первые две недели мы провели в диагностике — не предлагая решений. Это принципиальный момент: нельзя проектировать границы системы, не понимая, как реально работает бизнес-домен.
Мы провели серию сессий event storming с командой. Результат: три чётко выраженных предметных контура с минимальными пересечениями по данным и бизнес-логике.
Четвёртый контекст — User Management — решили оставить в монолите на первом этапе: он был относительно стабилен, и выигрыш от его выделения не оправдывал риск.
Анализ зависимостей показал ожидаемую картину: 60% межмодульных вызовов шли через прямые импорты и прямые SQL-запросы к чужим таблицам. Это означало, что «просто вырезать» сервис невозможно — нужно сначала разорвать зависимости внутри монолита, а уже потом выносить физически.
Мы сознательно отказались от подхода «большого взрыва» — полного переписывания с нуля. Для работающего финтеха с реальными пользователями это неприемлемо: слишком высокий риск, слишком длинный период, когда вы сидите с двумя системами без гарантий успеха.
Выбрали strangler fig pattern — постепенное замещение. Идея: новый сервис разворачивается рядом со старым монолитом, трафик перенаправляется на него через API gateway, старый код постепенно удаляется. В какой-то момент «фига» поглощает дерево.
Для коммуникации между сервисами — event-driven architecture через Apache Kafka. Выбор был неочевидным: команда предлагала REST. Мы настояли на событийной модели по нескольким причинам: финансовые операции требуют audit trail по природе, асинхронность критична для notification-сервиса, и при event sourcing проще реализовать eventual consistency без распределённых транзакций.
Принцип shared nothing — каждый сервис владеет своей базой данных. Единственная точка соприкосновения — event bus. Это фундаментальное требование для независимых деплоев.
Самая сложная часть — не архитектура и не код новых сервисов. Самая сложная часть — миграция данных без даунтайма для финансовой системы.
Мы использовали dual-write с верификацией. Схема: на переходный период все записи идут одновременно в старую и новую системы. Читать продолжают из старой. Параллельно фоновый процесс верифицирует консистентность — сравнивает записи между источниками. Как только расхождения падают до нуля и держатся стабильно — переключаем чтение на новый сервис, затем отключаем запись в старый.
Это схема занимала от 3 до 6 недель для каждого домена. Payments — самый сложный и самый долгий. KYC прошёл быстрее: данные более статичны.
# Пример схемы события в Kafka (Apache Avro)
# payment_events.avsc
{
"namespace": "fin.payments.v1",
"type": "record",
"name": "PaymentCompleted",
"fields": [
{"name": "event_id", "type": "string", "doc": "UUID v4"},
{"name": "payment_id", "type": "string"},
{"name": "user_id", "type": "string"},
{"name": "amount", "type": "long", "doc": "в тиынах / копейках"},
{"name": "currency", "type": "string", "default": "KZT"},
{"name": "provider", "type": "string"},
{"name": "status", "type": {
"type": "enum",
"name": "PaymentStatus",
"symbols": ["COMPLETED", "FAILED", "REVERSED"]
}},
{"name": "occurred_at", "type": "long", "doc": "unix timestamp ms"},
{"name": "idempotency_key", "type": ["null", "string"], "default": null}
]
}
Версионирование схем через Confluent Schema Registry стало обязательным с первого дня. При финансовых операциях нет права на несовместимые изменения без миграции потребителей.
# Strangler fig: routing layer в API Gateway (Python/FastAPI)
# Постепенное переключение трафика по feature flag
from app.config import settings
from app.clients import legacy_client, payments_client
async def route_payment_request(payload: PaymentRequest) -> PaymentResponse:
"""
Переключение трафика с монолита на payments-service.
Управляется через feature flag в конфиге (0.0 → 1.0).
"""
rollout = settings.PAYMENTS_SERVICE_ROLLOUT # 0.0 → 1.0
if rollout >= 1.0 or (rollout > 0 and _should_route_to_new(payload.user_id, rollout)):
try:
return await payments_client.process(payload)
except PaymentsServiceError:
# Circuit breaker: fallback на монолит при ошибках нового сервиса
if settings.PAYMENTS_FALLBACK_ENABLED:
return await legacy_client.process_payment(payload)
raise
else:
return await legacy_client.process_payment(payload)
def _should_route_to_new(user_id: str, rollout: float) -> bool:
"""Детерминированное распределение по user_id (не случайное — для консистентности)."""
import hashlib
bucket = int(hashlib.md5(user_id.encode()).hexdigest()[:8], 16) % 100
return bucket < (rollout * 100)
Feature flag с детерминированным распределением по user_id — критичная деталь. Случайный роутинг создаёт ситуацию, когда один пользователь в рамках одной сессии попадает то в старую, то в новую систему. Для финансовых данных это неприемлемо.
Итоговая архитектура к концу проекта (5 месяцев активной работы + 2 месяца стабилизации):
Каждый сервис — отдельный репозиторий, отдельный CI/CD pipeline, отдельное расписание релизов. Команды больше не согласовывают деплой-окна между собой.
Через восемь месяцев после начала проекта — цифры, которые мы можем зафиксировать:
Главный результат — не технический. Команда вернула контроль над скоростью разработки. Бизнес снова может планировать фичи на недели, не на кварталы.
Честность требует назвать трудности. Их было три.
Distributed tracing. Отследить запрос, который прошёл через API Gateway, payments-service и вернулся через Kafka в notifications-service — нетривиально. Мы потратили три недели на настройку OpenTelemetry + Jaeger и до сих пор считаем, что сделали это поздно. Трейсинг нужно закладывать в архитектуру с первого дня.
Схемы событий. Первые три версии EventSchema были несовместимы между собой — мы недооценили, как быстро будут меняться требования. Schema Registry + строгий backward compatibility policy спасли ситуацию, но это потребовало времени на внедрение дисциплины в команде.
Тестирование интеграций. Юнит-тесты для изолированных сервисов — просто. Contract tests между сервисами через Pact — значительно сложнее организационно. Часть команды воспринимала это как overhead, пока не случился первый инцидент из-за несовместимого изменения API.
Декомпозиция монолита — дорогое и длинное предприятие. Это не то, что нужно делать ради архитектурной красоты. Признаки того, что время пришло: деплой занимает больше недели, команды постоянно блокируют друг друга, время онбординга нового инженера превышает два месяца, масштабирование одного компонента требует масштабирования всего приложения.
Если вы узнаёте в этом описании свою ситуацию — мы готовы провести диагностику. Первый шаг всегда один: понять, где реальные границы вашего домена, прежде чем рисовать архитектурные схемы.