Масштабирование монолита до 1 млн строк: уроки от тимлида до CTO
Что можно узнать из монолита на миллион строк кода? Джек Кинселла проработал 7 лет техлидом и CTO в MorphMarket — маркетплейсе на стеке Django / React (TS) / React Native с командой из 20 человек. За это время кодовая база выросла до 1 000 000 строк, а Джек прошёл путь от первого инженера до технического директора.
Он опубликовал 113 уроков, которые вынес из этого опыта. Мы отобрали самые ценные, сгруппировали по темам и адаптировали для русскоязычной аудитории. Каждый урок — практический: его можно взять и применить в своём проекте уже сегодня.
Ключевые выводы
— Кэширование — последняя мера. Сначала исправьте модель данных, индексы и N+1-запросы.
— E2E-тесты на happy path дают в 10 раз больше отдачи, чем юнит-тесты на отдельные методы.
— Доведите Sentry до inbox zero — это единственный самый мощный шаг для качества продукта.
— Не обвиняйте конкретного разработчика за баг в проде: это всегда каскад из 3+ ошибок системы.
— Деплой за 2 минуты вместо 28 — достижимо, и это меняет культуру принятия решений.
Про архитектуру и масштабирование
Используйте сервисные объекты вместо раздувания моделей
В фреймворках, где модели строятся вокруг существительных (Product, User, Auction), логика неизбежно скапливается в этих классах. Со временем каждый из них превращается в монстра на тысячи строк. Решение — выносить процессы в отдельные сервисные объекты: PlaceAuctionBidService, ProductBillingService. Суть — перестать мыслить существительными и начать мыслить глаголами.
Одна и та же вещь, реализованная дважды — провал технического лидерства
Если в кодовой базе есть два разных клиента к одному платёжному провайдеру, три файла с функциями для работы с датами или дублирующиеся UI-компоненты — это симптом отсутствия ответственного за архитектуру. Создавайте очевидные, легко находимые места для общего кода (common/datetime.py, common/geo.py) и отмечайте переиспользуемый код на ревью.
Полиморфные связи в БД — почти всегда ошибка
ORM-код для полиморфных отношений становится крайне запутанным, и вы теряете защиту через foreign key constraints. Проще добавить nullable FK для каждой связанной модели и написать абстракции на уровне запросов.
Дефолтные фильтры и сортировки в ORM — всегда ошибка
Скрытые дефолты вроде order_by('-id') или filter(is_deleted=False) становятся миной замедленного действия, когда в команде 20+ человек. Новый разработчик не знает про неявное поведение — и получает баг. А скрытый ORDER BY портит аналитические запросы, потому что ORM добавляет ключ сортировки в SELECT.
Вся инфраструктура — в коде
Когда Джек пришёл в MorphMarket, все 27 крон-задач были настроены вручную на продакшн-сервере. Большинство отсутствовали на staging. Перенос в код (а затем конфигурация AWS/Heroku/CloudFlare/Sentry через Terraform) сделал все окружения похожими друг на друга — и, что важно, сделал инфраструктуру доступной для чтения AI-агентам.
Про тестирование
E2E > интеграционные > юнит-тесты
Пользователям важно, чтобы система работала целиком. Один E2E-тест на happy path каждой ключевой фичи (регистрация, покупка, создание листинга) даёт на порядок больше уверенности, чем десятки юнит-тестов. Детали дорабатывайте интеграционными и юнит-тестами — они быстрее и проще в поддержке.
Автоматические тесты для шаблонных фич
В MorphMarket написали систему, которая автоматически тестирует около 300 админ-страниц: парсит имя страницы, находит фабрику, создаёт запись в БД и проверяет, что страница открывается. Тот же подход можно применить к любому CRUD-эндпоинту без выделенного теста.
Изолируйте тесты полностью
Хрупкость тестов убивает мотивацию команды. Основные ловушки:
- Всё, что связано со временем — замораживайте или мокайте
- Фиксируйте seed для всех источников случайности — в каждой библиотеке
- Запретите реальные HTTP-запросы в не-E2E тестах (используйте VCR-подобные библиотеки)
- Отдельный Redis для тестов с автоочисткой перед каждым тестом
- Тестовая среда не должна читать ни одной переменной из вашего .env
Мокайте только внешние объекты
Мок — полезный инструмент, но только для внешних зависимостей. Мокать внутренние методы тестируемого класса — антипаттерн. Если можно переписать тест без мока без потери скорости — так и сделайте.
CI должен проверять не только ошибки, но и раздражающие мелочи
В MorphMarket CI также ловит новые warning-и в тестах, рассинхронизацию SDK с бэкендом и забытые миграции в Django. Чем раньше замечена мелочь, тем дешевле её исправить.
Про отладку и мониторинг
Sentry до inbox zero — самый мощный шаг к качеству
Когда Джек пришёл в компанию, Sentry показывал 1 000 ошибок в час. После агрессивной фильтрации (игнор старых браузеров, расширений, пометка JavaScript-ошибок уникальным идентификатором) и починки реальных багов — меньше 1 ошибки в час. Результат: каждая новая ошибка стала событием, на которое команда реагирует немедленно.
Dead Man Switch — мониторьте то, что должно происходить
Исключения шумят и легко ловятся. А вот отсутствие события — нет. Если крон-система перестала работать или бэкапы БД не снимаются — вы узнаете об этом только через heartbeat-мониторы. Настройте их.
Username в каждой строке лога
По умолчанию в логах — только IP-адрес. Но IP ротируется, создаёт лишнее перенаправление при дебаге и не показывает картину при использовании нескольких устройств. Поместите текущий запрос в thread-local и добавьте username в каждую запись.
Трипвайр-алерты на 20 самых важных эндпоинтов
Деградация производительности подкрадывается незаметно — инкрементальные фичи постепенно ломают индексы и кэши. Установите порог в несколько стандартных отклонений от нормы на ключевых эндпоинтах. Команда узнает о проблеме в течение часов после деплоя, а не через неделю жалоб.
Громко падайте при ошибках
Не глушите исключения except: pass. Паттерн от Джека: крашиться локально, мягко отказывать в проде — но всегда отправлять ошибку в Sentry/логи. Чем раньше заметили — тем меньше ущерб.
Про работу в команде
Не обвиняйте одного человека за баг в проде
Любой инцидент — это каскад минимум трёх провалов: автор не заметил, ревьюер пропустил, QA не покрыло, тесты не проверили, архитектура допустила класс ошибки. Обвинение одного человека мешает команде видеть системные причины.
Все разработчики должны быть немного фулстек
В MorphMarket убрали строгое деление на фронтенд/бэкенд/ops. Переобучение заняло год, но окупилось: фронтендер добавляет поле в БД сам, а не ждёт бэкендера — это сокращает время доставки фич.
Пятницы технического долга
Разумный компромисс между CEO, который хочет фичи, и инженерами, которым нужна чистота кода: с понедельника по четверг приоритеты задаёт продукт, а по пятницам — CTO. Бонус: инженеры отдыхают на глубоких задачах перед выходными.
Запретите длинные PR
В MorphMarket был период, когда PR висели по 3 месяца и накапливали тысячи изменений. Их невозможно ревьюить, они конфликтуют с master и создают огромный деплой-риск. Новое правило: мерж в master каждые 1–2 дня, максимум — раз в неделю. Побочный эффект: старшие разработчики дают фидбэк раньше, и драматические переписывания случаются реже.
Поощряйте общение в удалённых командах
Джек начал с ежедневных постов о прогрессе, потом стал публично задавать вопросы (даже на которые знал ответ), делиться изменениями в коде и просить мнения. Это постепенно снизило барьер и сделало общение нормой, а не исключением.
Про производительность
N+1-запросы — враг номер один в ORM
Проблема проявляется на крупных эндпоинтах, которые постепенно расширяют разные авторы: новые фильтры и параметры создают спорадические N+1, которые возникают только при определённом сочетании фильтров и объёме данных. Настройте APM (например, New Relic) для автоматического обнаружения N+1 и обучите команду ловить их на ревью.
Кэширование — последняя мера
Код кэширования чудовищно сложен в поддержке. Сначала исправьте модель данных, допишите индексы, уберите N+1, оптимизируйте алгоритмы. И только после этого — кэш. Джек рассказывает о случаях, когда наивный кэш работал на малых объёмах, но убивал Redis (он однопоточный!) при росте данных на два порядка.
Таймауты на всех уровнях — иммунная система сервиса
Веб-запрос должен завершиться за 25 секунд — иначе убивается. Да, для длинных задач придётся использовать фоновые задачи, но без таймаутов одна тормозящая внешняя API может съесть все потоки сервера и уронить всё.
Выгружайте работу в фоновые задачи
Отправка email, push-уведомления, обработка вебхуков от платёжных систем — всё это должно уходить в очередь. Веб-процесс обязан оставаться быстрым.
ORDER BY id вместо ORDER BY created
Если поле created неизменяемо и совпадает по порядку с id — сортируйте по id. Индекс на id уже есть бесплатно в каждой таблице.
Удаляйте ненужные данные
В MorphMarket некоторые таблицы копили 10 лет данных — логи IP-адресов, записи push-уведомлений, сообщения девятилетней давности. Скрипт очистки делает эти таблицы компактнее и быстрее.
Про безопасность и надёжность
Начните с модели угроз: что может получить атакующий?
Для MorphMarket основные вектора: захват аккаунтов (фикс — обязательный 2FA), спам-фишинг (капча на регистрации + фильтры сообщений), DDoS (Cloudflare + rate limit на nginx + кэш ключевых эндпоинтов). Универсального подхода нет — каждый сервис должен понимать свою специфику.
CHECK-ограничения в БД — лучше предотвратить, чем чинить
Раньше в MorphMarket крон-задача раз в сутки проверяла целостность данных и слала алерты. Всё заменили на CHECK constraints на уровне БД — некоторые проверяют до 16 операций над разными колонками. Невалидные данные просто не могут быть записаны.
Вебхуки — рассадник race conditions
Действие в коде может вызвать вебхук, который прилетит быстрее, чем данные запишутся в БД. Получатель вебхука прочитает устаревшее состояние и может затереть свежие данные. Решение: откладывайте действия, вызывающие вебхуки, до on_commit в БД.
Логируйте намерение перед побочным эффектом — особенно для платежей
Перед списанием с карты или возвратом пишите в лог: "bill user XYZ $100 for Y". Простой Write-Ahead Log спасал MorphMarket в нескольких инцидентах.
Про деплой
Деплой должен быть быстрым — действительно быстрым
MorphMarket сократил деплой с 28 минут до 2. Как: разделили фронтенд и бэкенд, распараллелили, перешли на Rust-based компиляцию JS, вынесли сборку на мощные машины (MacBook M-series вместо commodity Heroku), убрали из слага всё ненужное, почистили старые ассеты из S3.
Smoke-тест после каждого деплоя в прод
Автоматическая проверка ключевых страниц через Playwright после каждого деплоя. Если последние 150 деплоев были скучными — соблазн перестать проверять вручную огромен. Автоматический smoke-тест не устаёт и не теряет бдительность.
Не деплойте ничего рискованного в пятницу
Баг может проявиться вечером или в субботу, когда никого нет. Мелкие хотфиксы и изменения в админке — можно. Всё остальное — до четверга.
Плейбук для инцидентов при деплое
Короткие инструкции: как убить залипшие соединения к БД, точечно почистить кэш (без flush, который разлогинит всех), форсировать обновление ассетов. Обученная команда сокращает даунтайм в разы.
Про карьерный рост — от тимлида до CTO
Научитесь говорить о производительности сотрудников
Джек признаётся, что панически боялся этих разговоров, потому что не любит конфликты. Но реальность проста: лучше открыто сказать человеку, где он не дотягивает, чем копить раздражение и уволить неожиданно. Большинство людей способны расти под менторингом — Джек это недооценивал.
Документ ожиданий от сотрудника
Если написать "мы ценим, когда вы решаете проблемы в #bug-discussion до того, как они дойдут до CTO", люди действительно начнут так делать. Мотивированный сотрудник хочет ясности. Он не может прочитать ваши мысли.
Одно крупное изменение за раз
Внедряете E2E-тесты? Доведите до стабильности, прежде чем браться за линтеры. Если система сделана плохо — команда не будет ей доверять и не будет использовать. Плюс есть предел того, сколько изменений люди могут усвоить одновременно.
CLI-инструменты + AI = максимальный рычаг
AI отлично работает с текстом. CLI-инструменты тоже. Дайте AI доступ к продакшн-логам, отчётам об ошибках, read-only реплике БД, CI-результатам и REPL — и получите огромный множитель продуктивности. Джек называет это "наблюдаемость через CLI — ваш главный усилитель для AI".
FAQ
Почему монолит, а не микросервисы?
Монолит на миллион строк с 20 разработчиками — рабочая модель, если соблюдать дисциплину. Микросервисы добавляют сетевую сложность, и для команды такого размера overhead координации перевешивает выгоды. Джек не утверждает, что микросервисы плохи — он утверждает, что переход оправдан только при конкретной боли, а не как дефолтный выбор.
С чего начать, если в проекте уже бардак?
С Sentry до inbox zero и таймаутов на веб-запросы. Первое даёт видимость реальных проблем, второе предотвращает каскадные отказы. Дальше — по статье: N+1, изоляция тестов, сервисные объекты.
Насколько эти уроки применимы за пределами Django?
Джек 17 лет работал с Rails, Laravel, Express и другими фреймворками — и подчёркивает, что большинство уроков переносимы. N+1, дефолтные сортировки, race conditions в вебхуках, инфраструктура как код — всё это не привязано к конкретному стеку.
Правда ли, что E2E-тесты лучше юнит-тестов?
Не лучше, а дают больше отдачи на первом этапе. Один E2E-тест на happy path покрывает весь стек. Юнит-тесты нужны для деталей и граничных случаев, но начинать покрытие лучше сверху вниз.
Выводы
113 уроков Джека Кинселлы — это не теория из книг, а выжимка из 7 лет ежедневной работы над монолитом, который обслуживает реальных пользователей. Главные принципы:
- Предотвращайте, а не чините — CHECK constraints, таймауты, линтеры
- Делайте невидимое видимым — логи, мониторинг, Sentry до inbox zero
- Инвестируйте в скорость обратной связи — быстрый деплой, быстрый CI, короткие PR
- Доверяйте людям и системе, а не контролю — fullstack-навыки, плейбуки, документ ожиданий
Оригинальная статья: Scaling a Monolith to 1M LOC: 113 Pragmatic Lessons from Tech Lead to CTO (Semicolon & Sons, Jack Kinsella).
Если знаете коллегу, который борется с растущим монолитом — отправьте ему эту статью. А в комментариях расскажите, какой урок оказался самым неожиданным для вас.