Как я за три недели переписал Econumo с PHP на Go и почти перестал читать код

В декабре 2024 года я писал, что пересмотрел стек Econumo после трёх лет поддержки. Там была фраза: «Переписывать, конечно, я проект сейчас уже не возьмусь, но выводы на будущее сделал». Через полтора года я переписал весь проект. Бэкенд с PHP + Symfony на Go, фронтенд с Vue на React. Почти всю работу сделал Claude Code, а я его направлял и тестировал.
Расскажу, как это было и почему я теперь почти не читаю код, который попадает в продукт. Я пока не на 100% уверен, что такой подход применим к любому продукту, но в целом направление понятно.
Приложение, которое не планировалось переписывать
Econumo, это приложение для семейного бюджета, существует с 2021 года. Я собрал MVP за две недели на PHP, Symfony, PostgreSQL и Vue, потому что так быстрее, и потому что хотел заодно изучить Vue. С тех пор оно работало стабильно. Реальной причины что-то менять не было.
Работая над другими продуктами, я заметил, что почти перестал писать код руками. Его пишут агенты, а я ставлю задачи и проверяю результат. Но технологии я продолжал выбирать так, как будто пишу сам: что привычнее. А для агента критерии другие.
Что нужно агенту
Если код пишет человек, ему важны удобство, высокая читаемость и наличие инструментов. Агенту нужна обратная связь: быстрая, однозначная и желательно автоматическая. Чтобы он написал код, сразу получил ошибку компилятора, поправил, прогнал тесты, поправил ещё раз. Чем меньше слоёв магии между его правкой и сообщением об ошибке, тем лучше.
С этой точки зрения PHP мне перестал подходить. Вот несколько наиболее конкретных причин:
- Интерпретируемость. Типы, PHPStan и IDE помогают, но часть проблем всё равно выплывает только когда код выполнился. У агента больше шансов пропустить ошибку.
- Зависимости. Приложение держалось на Symfony и десятках пакетов: 40 прямых зависимостей и 107 пакетов в
composer.lock. Я не обновлял многие из них около двух лет, потому что обновление к тому моменту стало задачей требующей кучи времени. Из прошлой статьи вы можете помнить, как Dependabot однажды принёс обновление, после которого сломалась авторизация. - Цепочка поставок. В пакетных экосистемах всё больше кода, который написан агентами. Проверить руками каждую зависимость нельзя. Чем их меньше, тем меньше поверхность, за которой надо следить.
Почему Go
Я хочу от языка: компиляция и быстрый компилятор, большая стандартная библиотека, встроенные тесты, простой синтаксис, один бинарник, малый расход ресурсов. Под это подходил Go.
- Простота. Синтаксис небольшой и однообразный.
- Компилятор. Невалидный код отлавливается сразу, а не в рантайме.
- Стандартная библиотека. Роутер, HTTP, тесты, JSON, криптография есть из коробки. После миграции у бэкенда 9 прямых зависимостей против 40 в PHP версии.
- Один бинарник. Его легко деплоить, откатывать и запускать в маленьком контейнере.
Rust я тоже рассматривал. Он безопаснее и быстрее, но заметно сложнее, а для этого приложения простота важнее максимального контроля.
Ещё одно открытие сделал уже по ходу. Организация Go приложения в пакеты очень хорошо подходит для работы с агентом. Когда каждая фича лежит в своём пакете со своими обработчиками, моделями, работой с БД и тестами, изменения остаются локальными. По списку затронутых пакетов видно, что именно агент менял, а если маленькая правка затронула десять несвязанных пакетов, это повод насторожиться. Структура пакетов стала ещё и способом ревью.
SQL тоже проходит проверку до запуска. Для работы с базой я использую sqlc: пишу (точнее, агент пишет) обычные SQL-запросы в файлах, а sqlc превращает их в типизированный Go-код. Схема берётся из тех же миграций, которые применяются при старте, поэтому запрос не может разойтись со схемой. Если агент написал запрос к колонке, которой нет, или переименовал колонку и забыл про запрос, генерация падает, и это видно сразу, ещё до тестов. Для двух баз (SQLite и PostgreSQL) генерируется по отдельному пакету из одних и тех же запросов, а там, где SQL действительно отличается, есть отдельные варианты. Сейчас в проекте около 580 именованных запросов, и каждый из них проверен компилятором и sqlc, а не только тестами.
Это ещё одна ступень обратной связи для агента: ошибка в SQL ловится так же быстро и однозначно, как ошибка в типах.
Триггер
Когда вышла модель Claude Fable я был впечатлён результатами экспериментов, и я решил попробовать ради спортивного интереса мигрировать проект с PHP на Go. Когда увидел, что работает, решил довести до конца.
Работал примерно по 20-30 часов в неделю три недели подряд, то есть порядка 60-90 часов. Первый PR с бэкендом открыт 20 июня, бета новой версии вышла 9 июля. Деньги ушли только на подписки: Claude Max 20x за $200 в месяц (взял самый большой план ради запаса лимитов под Fable) и Codex примерно за $25 в месяц. Подписки были на два месяца, но управился я примерно за один. Лимит на июль, кажется, я потратил полностью. Отдельных расходов на API не было.
В то время доступ к Fable был нестабильным: его объявляли доступным на неделю или две, потом продлевали. Всё это время я работал и переживал, что вот-вот доступ заберут, а я не успею закончить миграцию. Отчасти поэтому миграция двигалась быстро.
Сначала функционал, потом красота
Главное правило, которое я себе поставил: сначала добиться того же поведения, и только потом улучшать архитектуру. Если одновременно менять и логику, и структуру, непонятно, что сломалось.
- Механический перевод. Агент перенёс PHP в Go почти один в один: те же эндпоинты, форматы запросов и ответов, бизнес-логика, поведение БД. Получилось не красиво, но предсказуемо.
- Тесты. Перенёс и расширил. В PHP-версии было около 7 тысяч строк тестов. После миграции стало около 20 тысяч строк. Покрытие довёл до порога 80% и добавил проверку, которая требует, чтобы каждый новый маршрут попадал в каталог тестов.
- Контракт API заморожен. Старый фронтенд не должен был заметить, что бэкенд сменился. Формат JWT, хеширование паролей, шифрование email проверялись по эталонным ответам живого PHP и на копии боевой БД.
- Подмена на живом приложении. Go-бэкенд подключил к той же базе и тому же фронтенду. Миграция данных для пользователей не нужна: бинарник при старте узнаёт старые миграции Doctrine и применяет только новые.
- Рефакторинг. Только когда поведение совпало, агент разложил проект по пакетам, убрал лишние абстракции и заменил часть сторонних пакетов своей реализацией.
Econumo поддерживает SQLite и PostgreSQL, поэтому одни и те же тесты прогоняются на обеих базах. Иначе агент легко внесёт изменение, которое работает на одной и ломается на другой.
Итог по архитектуре. Было слоёное приложение с элементами DDD: контроллеры, сервисы, репозитории, домен, инфраструктура. Стало приложение по фичам, где одна фича живёт в одном пакете.
Фронтенд: переписать, а не перенести
С фронтендом я поступил иначе. Старый Vue-код я писал, когда сам учил Vue, и там накопилась куча технического долга. Переносить как есть большого смысла не было.
Claude переписал большую часть почти с нуля на React. Он сначала смотрел на существующие экраны и поведение, воссоздавал структуру, подключал к неизменному API и прогонял UI и скриншот-тесты. Первый проход занял около 8 часов, всего агент работал около 15 часов. Потом я два-три дня просматривал все экраны руками и поправлял. Я использовал Chrome MCP для доступа к браузеру, чтобы Claude видел как было раньше.
Почему React? Для React больше всего примеров, компонентов и обсуждений, на которых учились модели, и агент в нём ошибается реже. Это тоже предположение, но практика его пока подтверждает. Размер кода почти не изменился: около 21 тысячи строк Vue и TypeScript превратились в 19 тысяч строк React.
К слову, после миграции, зависимостей во фронтенде стало больше, а не меньше: было 20, стало 38, так как очень много полезных библиотек типа Shadcn.
Что получилось в цифрах
Обе версии замерялись одинаково: Docker + пустая база SQLite, 16 потоков читают API в течение 15 секунд. PHP-образ последнего релиза (v0.10.1), Go-образ текущей версии (v1.7.0).
- Образ. 1,59 ГБ против 66,5 МБ.
- Память. PHP 57 мб после старта и до 100 мб под нагрузкой, Go 14 и 20 мб.
- Скорость. PHP около 193 запросов в секунду при медианной задержке 79 мс. Go около 1190 запросов в секунду и 12 мс.
Забавный факт. Первая версия на Go (v1.0.0) занимала 6 МиБ в простое и отвечала быстрее, чем текущая (v1.7.0): за три месяца добавились MCP, мобильное приложение, регулярные транзакции, биллинг, и всё это не бесплатно.
Как я теперь ревьюю
Я перестал читать код фронтенда, а бэкенд читаю выборочно:
- Какие пакеты и файлы изменились. Масштаб правки сразу виден. Маленькая фича с широким следом это тревожный знак.
- Что стало с базой. Миграции, удалённые колонки, переименования, новые таблицы, странные индексы. Ошибку в БД сложнее исправить.
API-контракты, тесты, скриншоты, CI, Dependabot и ревью от моделей заменяют все остальное.
Для фронтенда скриншоты оказались ключевыми, потому что превращают «выглядит странно» в воспроизводимую проверку.
Режим наблюдателя: Claude пишет, Codex ревьюит
Основной агент у меня Claude Code. Но писать и проверять свой код одной и той же моделью, это такое себе. Поэтому я добавил в Claude skill с инструкцией: когда фича готова и проверена, он не докладывает «готово», а запускает ревью от Codex.
Как это работает:
- Claude доводит фичу, прогоняет тесты, сборку и линтеры.
- Создаёт feature-ветку, коммитит только файлы этой задачи и пушит.
- Запускает два ревью Codex параллельно: общее и отдельное, по безопасности.
- Читает замечания (они размечены от P0 до P3), сам проверяет каждое по коду. Настоящие чинит, ненастоящие отклоняет и объясняет почему.
- Повторяет, пока есть серьёзные замечания, но не больше трёх кругов.
- Отчитывается: что нашёл Codex, что исправлено, что отклонено. В основную ветку изменения попадают уже после этого, и PR он без моей команды не открывает.
Получается две модели от разных компаний, которые смотрят на один и тот же код с разных сторон, и я как последняя инстанция. Часть замечаний Codex оказывается false-positive, и Claude это сам видит. Но реальные находки бывают, и это на порядок дешевле, чем ловить их потом в продакшене.
Что было после
После выпуска v1.0.0 (11 июля) прошло около трёх с половиной месяцев. Вышло семь минорных версий, до v1.7.0. За это время добавились MCP-сервер, регулярные транзакции, мобильное приложение, подтверждение email, rate limiting, биллинг. Багов после переноса было мало.
Для сравнения, по датам публикации релизов на GitHub. За весь 2025 год на PHP вышло 3 релиза (v0.5.2, v0.6.0, v0.7.0). С января по май 2026 вышло 5 (от v0.8.0 до v0.10.1). После миграции, с 10 июля по 25 октября, за три с половиной месяца вышло 20 релизов, от беты v1.0.0 до v1.7.0.
Итого: примерно за месяц подписок (Claude $200, Codex около $25) я перенёс проект целиком и стал выпускать фичи чаще и эффективнее.
Это и есть для меня главный результат. Сокращение потребления ресурсов до 14 МБ вместо 250 - это приятный бонус. Важно, что я могу теперь развивать продукт очень быстро, в разы быстрее, чем раньше и с минимальными багами. Качество держится на: компиляторе, тестах, скриншотах, границы пакетов, и интеллекте моделей.
Выводы
- Выбирайте технологии по качеству обратной связи для агента и по эффекту для продукта.
- Минимум зависимостей. Каждая внешняя зависимость несёт риски.
- При миграции сперва воспроизведите поведение, а только потом рефакторите.
- Границы пакетов работают как границы безопасности.
- Тесты это исполняемое знание о продукте, с ними реализацию можно менять целиком.
- Самое пристальное внимание уделяйте мало-обратимому: миграциям и внешним контрактам.
- Роль человека смещается вверх: что делаем, где риск, что нельзя ломать.
Код проекта открыт: github.com/econumo/econumo. Планы, по которым агент работал, лежат в docs/superpowers, а сами переписывания были в PR #46 (бэкенд) и #58 (фронтенд).
Если вы хотите вести семейный или личный бюджет, попробуйте Econumo. Есть бесплатный self-hosted вариант, а для тех, кому не хочется возиться с установкой, есть облачная версия с lifetime-покупкой. Это один из немногих продуктов, которым я сам пользуюсь на регулярной основе в последние 5 лет.