разработка

Чистая архитектура и микросервисы: разбор реальных продакшен-проектов

Команда ZUB-AI разобрала два продакшен-проекта: 13 микросервисов на .NET и Django с нуля. Конкретика по слоям, паттернам и типичным ошибкам.

Чистая архитектура и микросервисы: разбор реальных продакшен-проектов

Чистая архитектура и микросервисы: разбор реальных продакшен-проектов

Мы изучили два материала суммарно около двух часов: первый — детальный обзор живой образовательной платформы из 13 микросервисов на .NET в production, второй — практическое объяснение чистой архитектуры на Django с разбором реального спагетти-кода на 800 тысяч строк. Аудитория обоих материалов — разработчики, которые уже пишут что-то рабочее, но пока не разобрались, как это правильно организовать.

Оба разбора объединяет одна мысль: язык и фреймворк — детали. Принципы разделения на слои, управления зависимостями и изоляции бизнес-логики одинаково работают на C#, Python, Go и чём угодно ещё. Это не абстрактная теория из учебника — оба проекта живут в production и решают реальные задачи.

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


Монолит, модульный монолит, микросервисы — где на самом деле граница

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

Монолит — это когда всё приложение живёт в одном процессе, использует одну базу данных, и модули незаметно расползаются друг в друга. Работает быстро на старте, превращается в проблему при росте. Классический признак — разработчик боится менять модуль А, потому что непонятно, как это отразится на модуле Б.

Модульный монолит — один процесс, но внутри жёсткое разделение на изолированные модули. Модули общаются только через публичные контракты-интерфейсы, не лезут к данным друг друга напрямую. База данных одна, но разбита на схемы — по одной на модуль. Внутри может использоваться брокер сообщений. Это архитектура, которую легко поддерживать в команде 3–10 человек, и которую при необходимости можно превратить в микросервисы без полного переписывания.

Микросервисы — каждый модуль живёт в отдельном процессе на своём порту, деплоится независимо, может масштабироваться отдельно. Звучит красиво. Но это требует отдельного CI/CD для каждого сервиса, мониторинга, брокера сообщений, паттернов надёжности и постоянного решения вопроса «кто кому что должен сообщить». Цена входа — высокая.

Конкретный пример из нашего разбора: образовательная платформа использует 13… (02:10) ▶ 02:10

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

Микросервисы решают проблему масштабирования команды и инфраструктуры — но сначала нужно иметь эту проблему.


Что такое Use Case и почему его нельзя добавить «потом»

Это центральный тезис второго разбора. Разберём по пунктам.

В типичном Django-проекте без архитектуры бизнес-логика размазана везде: во… (09:06) ▶ 09:06

В типичном Django-проекте без архитектуры бизнес-логика размазана везде: во вьюхах, в методах моделей, в сигналах, в сервисных слоях, которые сами вызывают другие сервисные слои. 800 тысяч строк кода — и разработчик не может тронуть один метод, не проверив полдюжины мест, куда он тянется через сигналы и цепочки вызовов.

Чистая архитектура решает это через три слоя:

Handler (хендлер / вьюха) - Принимает HTTP-запрос - Достаёт нужные данные (user ID, параметры) - Приводит их к примитивам — int, str, не Django-объекты - Передаёт в Use Case - Возвращает ответ клиенту - Никакой логики. Никаких условий. Только маршрутизация.

Use Case (бизнес-логика) - Изолированный класс - Принимает примитивы, не знает… (15:54) ▶ 15:54

Use Case (бизнес-логика) - Изолированный класс - Принимает примитивы, не знает о существовании Django, HTTP, базы данных - Содержит только бизнес-правила: «если сумма больше порога — применить скидку» - Обращается к данным через Repository — и в идеале не через прямой импорт, а через Dependency Injection - Возвращает результат обратно в Handler

Repository (репозиторий) - Здесь живёт ORM: filter, select_related, save и всё остальное - Возвращает данные в Use Case в виде QuerySet или простых объектов

Суть простая: Use Case не знает, откуда берутся данные. Из PostgreSQL, из Redis, из mock-объекта при тестировании — ему всё равно. Это и есть инверсия зависимостей в действии.

А вот тут важный момент, который часто упускают: выделить Use Case можно только в начале проекта. Точнее — чем позже, тем дороже. В проекте на 800 тысяч строк команда потратила 4–5 месяцев, чтобы перевести на чистую архитектуру ~30% кодовой базы. Те части, которые отрефакторили — работать с ними стало кардинально проще. Остальные 70% продолжают жить по старым правилам.


Иллюстрация из видео (36:50) ▶ 36:50

Stateless против Stateful: без этого горизонтальное масштабирование не работает

Одна из вещей, которую мы вынесли из разбора продакшен-платформы — это жёсткое требование к stateless-сервисам. Звучит банально, но на практике это ломает архитектуру чаще, чем кажется.

Stateful-сервис держит данные в локальной памяти процесса. Сессии, кеши в памяти, WebSocket-соединения без внешнего хранилища. Запустить второй экземпляр такого сервиса — и сразу проблема: балансировщик отправляет запрос на второй экземпляр, а там нет данных от предыдущей сессии. Либо нужна липкая сессия (sticky session), либо боль.

Stateless-сервис ничего не хранит внутри себя. Всё состояние — в PostgreSQL, Redis или другом внешнем хранилище. Любой экземпляр может обработать любой запрос, потому что данные достаёт из общего места. Запускаешь три экземпляра — и они все работают одинаково.

В разобранной платформе из 13 сервисов почти все stateless. Исключение — Notification Service: он держит постоянное SSE-соединение (Server-Sent Events) с браузером клиента. Соединение привязано к конкретному экземпляру. Горизонтальное масштабирование этого сервиса — нетривиальная задача, требующая дополнительных решений.

Redis в этом стеке работает как универсальный инструмент для… (56:15) ▶ 56:15

Redis в этом стеке работает как универсальный инструмент для stateless-архитектуры: хранит коды входа, данные о доступах пользователей, обеспечивает Rate Limiting, работает как распределённый кеш. PostgreSQL разбит на 13 схем — по одной на каждый микросервис — что при единой физической базе сохраняет логическую изоляцию данных.


Синхронная и асинхронная коммуникация между сервисами

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

Синхронная коммуникация (HTTP / gRPC)

Сервис А отправляет запрос сервису Б, ждёт ответа, и только после этого продолжает работу. Используется там, где нужен немедленный результат.

Конкретный пример: когда пользователь открывает страницу курса, Education Content Service синхронно запрашивает у Access Service — есть ли у этого пользователя доступ. Без ответа страницу не показать. Синхрон тут оправдан.

Минус: если сервис Б медленно отвечает или упал — сервис А висит и ждёт. Цепочки синхронных вызовов через несколько сервисов — это потенциальные каскадные сбои.

Асинхронная коммуникация (через брокер сообщений)

Сервис публикует событие в RabbitMQ и не ждёт. RabbitMQ хранит событие и доставляет его подписчикам, когда те готовы. Сервис-отправитель продолжает работу немедленно.

Пример из платформы: студент выполняет задание → Progress Service фиксирует это и публикует событие в RabbitMQ → AI-сервис забирает событие и запускает проверку pull request в фоне → одновременно Notification Service забирает то же событие и отправляет пользователю уведомление. Всё это происходит параллельно, клиент не ждёт ни секунды.

Асинхрон отлично подходит для длинных операций, уведомлений, фоновых задач. Но добавляет сложность: что если сервис опубликовал событие, а потом упал до того, как оно записалось в брокер?

Для этого используется Outbox Pattern: событие сначала записывается в таблицу базы данных (outbox), и только после успешной записи отправляется в брокер. Если произошёл сбой — отдельный процесс перечитает outbox и повторно отправит событие. Событие не потеряется.


Мониторинг — не опция, а часть архитектуры

Без логов, метрик и трассировок в production — это не архитектура, это лотерея. Когда что-то ломается (а ломается всегда), первый вопрос: «где именно?». Без наблюдаемости (observability) ответ на него — угадывание.

Стек, который используется в разобранной платформе, стал де-факто стандартом для self-hosted мониторинга:

  • Prometheus — хранит метрики (количество запросов, время ответа, использование памяти)
  • Loki — хранит логи
  • Tempo — хранит трассировки (traces), показывает полный путь запроса через все сервисы
  • Grafana — дашборды поверх всего этого, алертинг
  • OpenTelemetry (OTLP) — протокол передачи данных от сервисов к стеку мониторинга

Все четыре инструмента разворачиваются вместе и интегрируются через Grafana. (03:32) ▶ 03:32

Все четыре инструмента разворачиваются вместе и интегрируются через Grafana. Для соло-проекта или небольшой команды — это реальный и не избыточный вариант. Настроить один раз, и дальше у тебя есть ответ на вопрос «что происходит» в любой момент времени.

Из нашего разбора: при 13 микросервисах без трассировок понять, какой именно сервис замедляет запрос — задача нетривиальная. Tempo в связке с OpenTelemetry даёт граф вызовов с временными метками по каждому переходу между сервисами.


Что мы заметили: где материалы сходятся, а где расходятся

Оба разбора сделаны независимо, на разных стеках (.NET и Django), но в нескольких ключевых точках они говорят одно и то же. А кое-где расходятся — и это тоже интересно.

Где полное совпадение

Use Case как обязательный элемент с первого дня. Оба разбора настаивают на этом. Не «можно добавить потом», а «потом будет стоить в разы дороже». Пример с 4–5 месяцами рефакторинга 30% кодовой базы — это конкретная цена откладывания.

Язык и фреймворк вторичны. Принципы изоляции слоёв, инверсии зависимостей и

Использованные видео

Хотите такой же разбор для своего видео?

ZUB-AI проанализирует видео с YouTube, RuTube или VK и пришлёт структурированный отчёт. Первый анализ — бесплатно.

Попробовать ZUB-AI →