← все статьи
8 мин

Кейс: Миграция монолита в микросервисы для fintech-платформы

Один из наших клиентов — финтех-платформа в СНГ с ~200K активных пользователей и командой из 15 инженеров — обратился к нам с классической проблемой роста: система перестала масштабироваться вместе с бизнесом. Не технически — технически она справлялась. Масштабирование сломалось организационно: четыре команды работали в одном репозитории и постоянно блокировали друг друга.

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

// Контекст: что было

Система — монолитное Django-приложение, ~450K строк кода, которому на момент начала работы было около 6 лет. За эти годы в него складывали всё: платежи, KYC-верификацию, уведомления, аналитику, административные инструменты, интеграции с внешними провайдерами. Единая база данных PostgreSQL — 800 таблиц, часть из которых потеряла актуальную документацию ещё несколько лет назад.

Деплой происходил раз в две недели и каждый раз превращался в событие. Не потому что процесс был плохой — CI/CD был настроен, тесты писались. Проблема была в том, что любой деплой касался всего: одна ошибка в миграции платёжного модуля могла заблокировать выкатку независимой фичи в KYC-процессах. Время от коммита до прода — 14 дней. Для финтеха, который конкурирует с банковскими суперапами, это было критично.

Проблема была не в монолите. Проблема была в том, что монолит не имел никаких границ внутри себя — это был Big Ball of Mud с хорошим CI/CD.

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

// Диагностика: что мы нашли

Первые две недели мы провели в диагностике — не предлагая решений. Это принципиальный момент: нельзя проектировать границы системы, не понимая, как реально работает бизнес-домен.

Мы провели серию event storming-сессий с командой. Результат: три чётко выраженных bounded context с минимальными пересечениями по данным и бизнес-логике.

  • Payments — всё, что связано с движением денег: транзакции, балансы, провайдеры, reconciliation.
  • KYC — верификация пользователей, документы, статусы, интеграции с госреестрами.
  • Notifications — доставка сообщений: push, SMS, email, in-app; шаблоны, предпочтения, статусы доставки.

Четвёртый контекст — 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 месяца стабилизации):

  • API Gateway — FastAPI, единая точка входа, аутентификация, rate limiting, routing.
  • payments-service — Go, PostgreSQL (отдельная БД), Kafka producer/consumer. Go выбрали за предсказуемую latency для финансовых операций.
  • kyc-service — Python/FastAPI, PostgreSQL, S3 для документов. Python — из-за богатой экосистемы ML-библиотек для верификации документов.
  • notifications-service — Python/FastAPI, Redis для очередей доставки, PostgreSQL для истории. Kafka consumer для реакции на события других доменов.
  • Монолит (Django) — продолжает работать для User Management и аналитических функций. Будет декомпозирован позже.

Каждый сервис — отдельный репозиторий, отдельный CI/CD pipeline, отдельное расписание релизов. Команды больше не согласовывают деплой-окна между собой.

// Результаты

Через восемь месяцев после начала проекта — цифры, которые мы можем зафиксировать:

  • Цикл деплоя: 14 дней → 2 дня в среднем по всем командам. Payments-команда деплоит независимо 3–4 раза в неделю.
  • Три команды деплоят параллельно без координации. Количество «блокирующих» задач упало практически до нуля.
  • Инциденты при деплое: −73%. Основная причина: изолированные изменения имеют значительно меньший радиус взрыва. Ошибка в notifications не роняет payments.
  • MTTR (среднее время устранения инцидента): −60%. Когда сервисов три вместо одного монолита, локализация проблемы занимает минуты, не часы.
  • Команда выросла с 15 до 25 инженеров без роста координационного трения. Новые инженеры онбордятся в один сервис, а не в 450K строк.

Главный результат — не технический. Команда вернула контроль над скоростью разработки. Бизнес снова может планировать фичи на недели, не на кварталы.

// Что было сложнее всего

Честность требует назвать трудности. Их было три.

Distributed tracing. Отследить запрос, который прошёл через API Gateway, payments-service и вернулся через Kafka в notifications-service — нетривиально. Мы потратили три недели на настройку OpenTelemetry + Jaeger и до сих пор считаем, что сделали это поздно. Трейсинг нужно закладывать в архитектуру с первого дня.

Схемы событий. Первые три версии EventSchema были несовместимы между собой — мы недооценили, как быстро будут меняться требования. Schema Registry + строгий backward compatibility policy спасли ситуацию, но это потребовало времени на внедрение дисциплины в команде.

Тестирование интеграций. Юнит-тесты для изолированных сервисов — просто. Contract tests между сервисами через Pact — значительно сложнее организационно. Часть команды воспринимала это как overhead, пока не случился первый инцидент из-за несовместимого изменения API.

// Для кого этот подход подходит

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

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


← все статьи Следующая →Когда техдолг убивает раунд: 5 красных флагов для инвестора