Как я перестал «тушить пожары» и переписал легаси-систему для логистического гиганта на современный SaaS
Привет, VC! Я Денис, и я разработчик с 8-летним опытом в enterprise-разработке. Сегодня я расскажу историю, которая стоила мне нескольких тысяч седых волос, паре бессонных ночей и одного экзистенциального кризиса в 3 часа ночи над консолью с красными ошибками. Но она научила меня главному: бизнес-логика — это не просто код. Это фундамент, на котором либо строят небоскрёб, либо медленно закапывают деньги и нервы.
Глава 1. Звонок и «маленькая CRM»
Это было около двух лет назад. Середина рабочей недели, я допиваю третий кофе, когда в мессенджере появляется сообщение от Игоря — владельца одной из крупнейших логистических компаний в регионе. Флот из 300+ грузовиков, склады, десятки водителей, контрагенты по всей стране.
Запрос звучал обманчиво просто: «Денис, мы устали зависеть от готовых решений. Хотим своё, родное. Чтобы и автопарк видеть в реальном времени, и путевые листы печатать прямо из браузера, и чтобы всё — в одном окне. Никаких Excel-таблиц, никаких звонков диспетчеру с вопросом — а где Петров сейчас. Сделаешь?»
Мы встретились в офисе Игоря на следующий день. Я записал требования на 3 страницах A4, выпил ещё кофе и подумал: «Задача интересная, стек понятен, сроки реальные». Мы ударили по рукам.
На тот момент быстрый старт на Flask + Jinja2 + MySQL казался очевидным выбором. Стек проверенный, команда его знает, нужно было выкатывать фичи «вчера» — и мы их выкатывали. Первые три месяца система росла как на дрожжах: модуль автопарка, карточки водителей, путевые листы с печатью, простой дашборд.
Клиент был доволен. Мы — тоже. Но дьявол, как известно, кроется в деталях.
Глава 2. Технический ад: 30 000 строк макарон и архитектура «на честном слове»
Прошло ещё несколько месяцев. Система обрастала новыми модулями: расчёт ГСМ, договоры с контрагентами, учёт ТО и ремонтов, табели рабочего времени, складской учёт. Каждая новая «хотелка» выкатывалась в продакшн за 3-5 дней.
А потом я однажды утром открыл проект и понял, что мы в ловушке. Вот что скрывалось под капотом:
- Монолит на 30 000+ строк кода. Один файл routes.py на 4 200 строк — просто потому что «так исторически сложилось».
- Никакой типизации. Переменная fuel_cost в одном месте была числом, в другом — строкой, в третьем — словарём. Python молчал, баги всплывали в продакшне.
- База данных MySQL превратилась в «братскую могилу» из 80 таблиц. Связи между ними держались на честном слове и паре SELECT * FROM ... WHERE id = {magic_number}.
- Фронтенд на Alpine.js разросся до состояния, когда страница с отчётом по ГСМ грузилась 8 секунд на офисном компьютере с Core i7.
- Полное отсутствие тестов. Потому что «некогда было» — и теперь любой рефакторинг превращался в русскую рулетку.
Я помню день, когда Игорь написал мне: «Денис, мы добавили нового контрагента — и у трёх водителей пропали путевые листы за прошлый месяц. Это нормально?» Это было не нормально. Я потратил 11 часов, чтобы найти баг: изменение в одном месте триггерило каскад побочных эффектов в трёх модулях, никак между собой формально не связанных.
Я превратился из разработчика в пожарного. 80% рабочего времени — не на новые фичи, а на латание дыр, объяснение клиенту почему «само сломалось» и мат у монитора в 11 вечера.
Глава 3. Точка невозврата: «Режем по живому»
Бизнес начал ощущать это физически. Простой запрос — добавить новый тип техники в справочник — занимал неделю, потому что это затрагивало расчёт ГСМ, форму путевого листа, отчёты и ещё пять мест, о существовании которых я вспоминал только когда что-то ломалось.
Конкурирующий продукт, который Игорь показал мне на встрече, выглядел свежо и быстро. «Вот видишь — у них это есть, а у нас нет», — сказал он. И был прав.
Я пришёл к нему и сказал прямо, без лишней дипломатии: «Игорь, у нас два пути. Первый — продолжать тратить деньги на ремонт разваливающегося сарая. Каждая новая фича будет стоить дороже предыдущей, а баги будут становиться всё экзотичнее. Второй — мы сносим всё до основания и строим современный технологичный завод. Это займёт время и деньги прямо сейчас, но через полгода у тебя будет платформа, которая не будет тебя ограничивать».
Игорь помолчал минуту. Потом кивнул: «Считай». Мы договорились об условиях — и начали.
Главный принцип, который я поставил во главу угла с первого дня: параллельная работа. Старая система продолжала крутиться в продакшне. Новая разрабатывалась рядом. Никаких «заморозим разработку на три месяца» — бизнес не может себе этого позволить.
Глава 4. Новый стек: Fastify, TypeScript, PostgreSQL и Gravity UI
Выбор стека — это всегда компромисс между тем, что ты знаешь, что хочет рынок и что выдержит нагрузку через 3 года. Вот что мы выбрали и почему:
Backend: Fastify + TypeScript
Переход на строгую типизацию — это не просто «модно». Это конкретно измеримый результат: около 90% глупых ошибок («передал строку вместо числа», «забыл поле в объекте») отлавливаются ещё на этапе написания кода, до деплоя. TypeScript с правильно настроенными строгими режимами (strict: true, noImplicitAny, strictNullChecks) превратил процесс разработки из «молюсь и деплою» в предсказуемый инженерный процесс.
Fastify вместо Express — выбор осознанный. Бенчмарки показывают в 2-3 раза большую пропускную способность на тех же железках. Для системы, где одновременно работают 50+ пользователей и периодически прилетают тяжёлые отчёты, это ощутимо. Плюс встроенная схемная валидация через JSON Schema из коробки.
База данных: PostgreSQL + Zod
MySQL мы заменили на PostgreSQL. Причины: нормальная поддержка JSON-полей, оконные функции для сложных отчётов, более строгая типизация на уровне самой БД. Но главное изменение — мы внедрили Zod для валидации схем на уровне приложения.
Теперь каждый объект, который приходит в API или уходит в базу, проходит через строгую схему. Попытка записать строку в числовое поле, null в обязательное поле или неизвестный ключ в объект — всё это вызывает понятную ошибку с точным указанием проблемы ещё до обращения к БД. Никаких магических сбоев в продакшне.
Фронтенд: Gravity UI от Яндекса
Это был самый неочевидный выбор, о котором меня чаще всего спрашивают. Почему не Material UI, не Ant Design, не что-то более «стандартное»?
Gravity UI — это дизайн-система Яндекса, которую они открыли в публичный доступ. Она создавалась для real-world enterprise-продуктов с высокой плотностью данных: таблицы с сотнями строк, сложные фильтры, вложенные формы, дашборды. Именно это нам и нужно. Компоненты выглядят дорого и профессионально из коробки, без часов кастомизации CSS.
Результат: интерфейс, который на демо клиенты принимают за продукт уровня SAP или 1С:ERP — но с нормальным UX.
Архитектура: модульный монолит с прицелом на микросервисы
Мы не стали сразу прыгать в микросервисы — это была бы преждевременная оптимизация для команды из 2 человек. Вместо этого — модульный монолит: каждый бизнес-домен (автопарк, водители, путевые листы, ГСМ, договоры, склад) существует как изолированный модуль со своими роутами, сервисами, репозиториями и типами. Модули общаются через чётко определённые интерфейсы, не через прямые вызовы функций соседа.
Это даёт 80% преимуществ микросервисов (изоляция, понятные границы, параллельная разработка) без 100% их головной боли (распределённые транзакции, сетевые сбои, отладка через 5 сервисов).
Глава 5. Что мы построили: функционал, о котором конкуренты только мечтают
Технический рерайт — это было лишь средство. Вот что он позволил нам реализовать:
- Умные анкеты водителей. Перед рейсом водитель проходит динамическую анкету на смартфоне: вопросы меняются в зависимости от типа маршрута, груза и истории предыдущих рейсов. Подозрительные ответы автоматически эскалируются диспетчеру.
- Фотоконтроль ремзоны. Механики фотографируют автомобиль до и после ТО прямо в системе. Фото привязываются к карточке ТО, хранятся хронологически. Никаких споров «а кто поцарапал крыло».
- Полная автоматизация договоров. Договор с контрагентом генерируется из шаблона в один клик, уходит на подпись через интегрированный сервис ЭДО, статус отслеживается прямо в системе.
- SaaS-режим с изолированными тенантами. Компания может развернуть отдельную среду для партнёра или филиала за 3 минуты. Полная изоляция данных, свои роли и права доступа, своя кастомизация.
- Аналитика в реальном времени. Дашборд с живыми метриками: расход топлива по автомобилям, KPI водителей, загрузка автопарка, прогноз ТО. Данные обновляются без перезагрузки страницы.
Результаты: цифры, которые говорят сами за себя
- Time-to-market новой фичи: с 1-2 недель до 1-3 дней.
- Время ответа API: среднее время запроса снизилось с 800 мс до 120 мс.
- Количество критических багов в продакшне: с 8-10 в месяц до 0-1.
- Время на «тушение пожаров»: с 80% рабочего времени до менее 10%.
Вместо вывода: три вещи, которые я понял
Первое. Технический долг — это не абстракция из статей на Хабре. Это реальные деньги, реальное время и реальные нервы — ваши и клиента. Чем дольше вы его игнорируете, тем дороже он обходится.
Второе. Разговор с клиентом о полном рерайте — это продажа, а не техническое обсуждение. Говорите на языке бизнеса: скорость выхода фич, надёжность, масштабируемость. «Нам нужна строгая типизация» — это не аргумент. «Новый функционал будет выходить в 5 раз быстрее, а бизнес перестанет терять деньги на простоях» — это аргумент.
Третье. Архитектурные решения живут дольше, чем технологии. TypeScript, Fastify, PostgreSQL — через 5 лет их могут заменить что-то другое. Но принцип «изолированные модули с чёткими контрактами» будет работать всегда.
Кстати, этот опыт работы с архитектурой и автоматизацией натолкнул меня на идею ещё более амбициозного проекта. Но об этом я расскажу чуть позже. Если вы прямо сейчас узнали свой проект в описании «30 000 строк макарон» — напишите в комментариях или в личку. Поговорим.