В первой статье я писал про SDD за один вечер — Telegram-бот, шесть команд Spec Kit, восемь часов от первого speckit.constitution до рабочего MVP. Это была проверка методологии на маленькой задаче.

С тех пор я прошёл 17 спринтов SDD на FullStack-приложении: B2C-трекер привычек и целей, два репозитория (backend и frontend), 251 тест на бэке и 77 на фронте, релиз в продакшен. Это уже не вечер — это полный цикл разработки FullStack-приложения по одной методологии.

Здесь — что не дало мне потерять контроль на этом масштабе. Не «как быстро я это сделал», а как методология держит управляемость, когда фич много, репозиториев два, спринты идут параллельно и каждая фича касается обеих сторон.

Что построено за 17 спринтов

Чтобы дальше говорить о методологии — короткий контекст того, что под ней.

LifeSync — пет-проект, B2C-трекер привычек и целей. Backend на Spring Boot 3.5 + Java 21, Hexagonal Architecture на 6 Maven-модулях, jOOQ вместо JPA, Apache Kafka с шестью независимыми консьюмерами и идемпотентностью через таблицу processed_events, JWT RS256 с ротацией refresh-токенов, OpenAPI 3.1 как источник правды для API. Frontend на React 19 + TypeScript 5.9, Vite 8, Tanstack React Query, Zustand, shadcn/ui + Tailwind CSS, тёмная тема, двуязычный интерфейс через react-i18next, мобильная адаптация до 375px.

В цифрах:

  • 17 спринтов SDD — 7 на backend, 10 на frontend.

  • Два репозитория — lifesync-backend (v1.0.2) и lifesync-frontend (v1.3.0). Деплой: backend на Railway, frontend на Vercel.

  • 149 000 слов спецификаций. На backend — около 61 000 слов на 6 фич (где третий спринт прошёл без SDD-артефактов), на frontend — около 89 000 слов на 10 фич. Это не считая constitution.md обоих репозиториев и кода как такового.

  • 251 тест на backend (170 unit + 81 integration на Testcontainers с PostgreSQL и Kafka) и 77 на frontend (55 unit на Vitest + 22 E2E на Playwright).

  • 19 Liquibase-миграций, JaCoCo подключён.

  • Релиз в продакшен — backend на Railway (без Kafka в demo-окружении ради бесплатного тира), frontend на Vercel: lifesync-frontend-ten.vercel.app.

Дальше — про то, что не дало этому всему разъехаться.

Опора 1. Три чата вместо двух

В первой статье я зафиксировал схему «два чата»: отдельный чат с Claude — для думания (обсуждение архитектурных решений, подготовка промтов), Claude Code в терминале IDE — для исполнения (генерация спек, написание кода, запуск команд).

На FullStack-проекте эта схема не выдержала. Backend и frontend живут в разных репозиториях, у них разные стеки, разные конституции, разные циклы спринтов. Нельзя было держать оба контекста в одной голове и в одном думающем чате — рано или поздно я начал бы переносить решения с одного стека на другой по инерции.

В итоге сложилась схема «три чата», не считая Claude Code в терминале IDE — он по-прежнему остаётся исполнителем в каждом репозитории:

  • Думающий чат для backend. Здесь живёт всё, что касается серверной части: Hexagonal-структура, Kafka-консьюмеры, JWT, jOOQ-запросы. Контекст этого чата — lifesync-backend/constitution.md + OpenAPI-спецификация + актуальная фича в работе.

  • Думающий чат для frontend. Сюда уходят React, TypeScript, React Query, shadcn/ui, темизация, i18n. Контекст — lifesync-frontend/constitution.md + ссылки на backend-репо и Swagger UI, чтобы я мог обращаться к актуальному API-контракту.

  • Третий, координационный чат. Для тем, которые касаются обоих репозиториев одновременно: деплой (Railway + Vercel + переменные окружения), кросс-репо фичи (например, сложная серверная валидация пароля с зеркальной клиентской), ретроспективы. В этом чате я готовил промты для двух других чатов, чтобы решения по общим фичам были согласованы.

То есть концепция «два чата на проект» из первой статьи сохраняется — думающий чат плюс Claude Code в терминале. Просто к ним добавился координационный, общий для двух репозиториев. Итого пять рабочих контекстов: думающий + Claude Code на backend, то же на frontend, и координационный на двоих.

Когда я открывал второй чат для frontend, я переносил в него контекст backend через явные промты: ссылку на репозиторий, ссылку на Swagger UI, файл OpenAPI-спецификации, краткое описание архитектуры. Это работает как информационный мостик — frontend-чат получает не «общую идею», а конкретные точки опоры.

Что важно про этот мостик: он однонаправленный. Backend-чат не получает обратной связи от frontend-чата автоматически — если в процессе разработки UI я обнаруживал несоответствие в API, я возвращался в координационный чат, прорабатывал там, и оттуда уже шёл новый промт в backend-чат. Это медленнее, чем «один чат на всё», но это и есть та структура, которая не даёт двум стекам перепутаться.

Третий чат — это не просто удобство. Это разделение зон ответственности на уровне ИИ-собеседника. Когда вопрос конкретно про backend — backend-чат уже в контексте, не надо его поднимать с нуля. Когда вопрос про деплой — координационный чат знает обе стороны.

Опора 2. Две конституции, живущие по-разному

Конституция в SDD — это .specify/memory/constitution.md, документ-договор между мной и проектом: какие архитектурные принципы соблюдаются, какие технологии в стеке, какие правила разработки. Spec Kit генерирует на её основе подсказки для всех остальных команд: speckit.specifyspeckit.planspeckit.tasks опираются на конституцию, чтобы не предлагать решения, противоречащие зафиксированным правилам.

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

Backend constitution. Стартовая ратификация — пустой шаблон в первый день проекта, затем версия 1.1.0 с полным набором принципов. Дальше — 12 правок, документ вырос от 50 строк до 437. Что добавлялось:

  • v1.2.0 — стандарт работы с Liquibase (формат миграций, нумерация, структура changelog).

  • v1.2.1 — правила атомарных коммитов.

  • v1.2.2 — порядок code style правил.

  • v1.2.4 — naming convention для веток.

  • v1.3.0 — стандарт документирования OpenAPI как двенадцатый принцип.

  • v1.3.1, v1.3.2 — execution rules: как именно интерпретировать tasks.md при имплементации.

После Sprint 5 (Kafka) constitution.md backend больше не правился. Это не значит, что он умер — это значит, что в раннюю фазу я зафиксировал все правила, которые регулируют дальнейшую разработку. Когда правила работают, документ перестаёт расти. Это и есть здоровое состояние.

Frontend constitution. Стартовая версия — 50 строк, затем ратификация 1.0.0 со всеми пятью принципами (API-Layer Isolation, Server State via React Query, Component-Logic Separation, Type Safety NON-NEGOTIABLE, Design System Fidelity). Дальше — всего 4 коммита. Финальный размер — 137 строк.

Эволюция точечная: смена React Router v6 → v7 в Technology Constraints, позже — расширение под i18n с добавлением src/locales/ в обязательную структуру (Sprint 9). Никаких больших переписываний.

Почему две конституции эволюционируют так по-разному? Потому что бэкенд решает больше неочевидных вопросов на старте. Какой формат миграций? Как нумеровать коммиты? Какая структура OpenAPI? Каждый раз, когда в спринте всплывал новый вопрос, к которому конституция не давала ответа — я добавлял туда правило. На фронте таких вопросов было меньше: shadcn/ui задаёт паттерны, React Query — серверное состояние, TypeScript строгий — а остальное оказалось в существующих принципах.

Здесь важен один тезис: конституция — это живой документ. Когда в проекте появляется новая ситуация, не покрытая правилами — документ адаптируется. Это не «нарушение» — это часть методологии. Spec Kit прямо об этом пишет: конституция должна быть живой, иначе она становится мёртвой буквой, которую все игнорируют.

Adapting the rule, not breaking it. Разница принципиальная.

Опора 3. speckit.analyze как контур обратной связи

speckit.analyze — одна из шести команд Spec Kit, и формально она называется «cross-artifact consistency analysis report». На практике это команда, которая проверяет согласованность между четырьмя артефактами фичи: spec.mdplan.mdtasks.md, и фактическим кодом после имплементации. Если в спецификации описано требование, которое не отражено в плане, или в плане есть фаза, не разложенная на задачи, или код реализован в обход того, что было спланировано — analyze это находит.

Я запускаю speckit.analyze после реализации задач спринта — это обязательная часть моего цикла. Иногда — ещё и перед реализацией, после того как появился tasks.md, когда хочется убедиться, что задачи действительно закрывают спецификацию. За 17 спринтов analyze ловил несколько критичных вещей и регулярные мелкие. Самый яркий пример — Sprint 5 backend, события через Kafka.

Я закончил имплементацию шести консьюмеров со встроенной идемпотентностью, прогнал тесты, всё зелёное. Запустил speckit.analyze post-impl. Нашёлся неочевидный недочёт: в нескольких use case'ах публикация события в Kafka шла внутри транзакции, без try-catch вокруг publishEvent. Это значило, что если Kafka в моменте недоступна — исключение поднимается наверх и откатывает успешно зафиксированную транзакцию БД.

То есть данные сохранились, потом откатились, событие не отправилось — система в неконсистентном состоянии.

Я бы это в комит-ревью пропустил. Тесты были зелёные, потому что в Testcontainers Kafka всегда доступна. Сценарий «Kafka недоступна, БД доступна» в integration-тестах не покрывался. speckit.analyze это поймал не потому, что прогнал какой-то умный тест — а потому, что прошёлся по спецификации, увидел требование «события публикуются после успешной транзакции», и сравнил его с фактом «публикация внутри транзакции». Расхождение.

Поправил try-catch, добавил тест на DLQ retry, и заодно — асинхронный IT-тест на streak-расчёт, который тоже всплыл в этом анализе.

Это не магия. Это формализованный контур обратной связи: каждая фича заканчивается тем, что артефакты сравниваются между собой и с кодом. Расхождения сигнализируются. Я их разбираю и фиксирую. Это ловит вещи, которые иначе уехали бы в продакшен.

Ключевое наблюдение про analyzeон не заменяет тесты или ревью. Он работает на другом уровне — на уровне согласованности артефактов между собой. Тесты проверяют, что код делает что-то. analyze проверяет, что код делает то, что было обещано в спецификации. Это разные слои.

Опора 4. API-first как договор между двумя репозиториями

Когда два репозитория существуют отдельно, главная угроза согласованности — API-контракт. Если backend выкатывает новый endpoint и забывает про frontend, или frontend ожидает поле, которое в API называется иначе — координация ломается. На обычной командной разработке это решается переписками, JIRA-тикетами, ревью. У меня была другая опция: API-first.

В backend я завёл отдельный Maven-модуль lifesync-api-spec, в котором лежит единственный файл — lifesync-api.yaml, OpenAPI 3.1 спецификация на все endpoint'ы. На сегодня это 2669 строк YAML, 32 endpoint'а. Backend генерирует Java-интерфейсы из этого YAML через openapi-generator-maven-plugin (контроллеры реализуют сгенерированные интерфейсы — писать их вручную запрещено конституцией). Frontend читает тот же YAML как источник правды и пишет TypeScript-типы в src/types/ руками: 5 файлов, около 230 строк типов.

Почему frontend пишет типы руками, а не генерирует? Это сознательное решение. Генератор TypeScript-типов из OpenAPI — рабочая опция (openapi-typescriptorval@hey-api), и я её рассматривал. Не пошёл по двум причинам:

  1. Контроль над формой типов. Сгенерированные типы часто буквально повторяют структуру API, со всей nullability и opt'ами. Для UI часто удобнее немного перекомпонованный тип, который ближе к тому, что отрисовывается на экране. Ручное написание оставляет это под контроль.

  2. Типов мало, изменений ещё меньше. 230 строк типов на 32 endpoint'а — это контролируемый объём. Каждое изменение API сопровождалось ручной правкой типов на фронте, и это не превращалось в трудоёмкую задачу.

Внутри specs/ каждой фичи фронта лежит локальный contracts/-каталог с markdown-описанием endpoint'ов, которые эта фича использует — auth-api.mdgoals-api.mdhabits-api.md. Это не дублирование YAML и не альтернативный контракт, а навигационный срез под конкретную фичу для думающего чата: какие endpoint'ы я зову, какие поля жду, какие ошибки могут прилететь. Источник правды — по-прежнему YAML на 2669 строк, но его не нужно держать перед глазами целиком, когда работаешь над одной фичей.

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

Что важно понимать про эту схему: speckit.analyze не проверяет согласованность между репозиториями автоматически. Каждая команда Spec Kit работает в пределах одного репозитория. Я делал кросс-репо проверки руками: после изменений API в backend — в координационном чате готовил промт «вот изменения в API, проверь типы и компоненты frontend на необходимость правок», и проходил по этому промту в frontend-чате.

Где было трудно

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

Sprint 8 backend, рассинхрон plan.md vs spec.md. speckit.analyze показал расхождение: plan.md описывал три фазы имплементации, в spec.md была упомянута четвёртая. По коду я уже сделал все четыре, но в плане — синхронизации не было. Случай несложный (одна правка в plan.md), но показательный: методология не позволила мне забыть про эту синхронизацию. Без analyze я бы заметил это только в ретроспективе спустя три спринта.

Sprint 10 frontend, релизный с накопленными доработками. Это был последний спринт фронта, и он отличался от предыдущих: не одна большая фича, а набор мелких UX-улучшений и багфиксов, которые накопились перед релизом. SDD-цикл здесь работал иначе — короткий spec.md, плотный tasks.md на список доработок, обычный analyzeв конце. Главный урок: не каждый спринт идёт по идеальному циклу. Иногда фича — это «накопленные мелочи перед релизом», и она требует своих tasks.md и analyze, но без больших спецификаций.

Backend Sprint 3, который прошёл мимо SDD. В backend между Sprint 2 и Sprint 4 был короткий спринт настройки локального окружения для разработки — один fix-коммит, без specs/003-*/. Тогда я не делал спецификацию: задача казалась слишком технической для SDD-цикла. Сегодня бы я сделал спеку даже на это — specs/003-local-dev-setup/spec.md со списком требований к dev-окружению занял бы 50 строк, но дал бы фиксацию: какие порты, какие переменные окружения, какой docker-compose. Урок на будущее: SDD одинаково полезен на больших фичах и на маленьких настроечных задачах.

Что я забрал из этого опыта

Чувство контроля на 17 спринтах сохранилось до последней фичи. Это не потому, что я какой-то особенный — это потому, что методология даёт систему обратной связи на нескольких уровнях одновременно.

  • Спецификации фиксируют намерение. Что я хотел построить.

  • Конституция фиксирует правила. Как я договорился это строить.

  • speckit.analyze ловит расхождения. Что разъехалось между намерением, правилами и кодом.

  • Три чата разделяют контексты. Backend, frontend, общая координация — каждый со своим документальным фундаментом.

  • API-first — договор между двумя репозиториями. Один YAML, два потребителя.

Когда эти пять компонентов работают вместе, масштаб проекта перестаёт быть угрозой управляемости. Спринтов может быть 17, может быть 50 — структура контроля не меняется. Меняется только содержание.

Главный сдвиг от первой статьи: методология масштабируется без потери качества. Один вечер с Telegram-ботом и 17 спринтов с FullStack-приложением — это одна и та же методология, отличающаяся в деталях (третий чат, две конституции вместо одной), но не в сути. Это и есть тот ответ на вопрос «работает ли SDD за пределами игрушек», который у меня после первой статьи был ещё открытым.

Сейчас он закрыт. Работает. Я готов делать следующий проект и расширять существующие через ту же методологию — и я знаю, на каких местах нужно держать голову острее, чтобы масштаб не растащил процесс.

Что дальше

Это третья статья в серии материалов из моих проектов:

  • Первая — про SDD на одном вечере на примере Telegram-бота.

  • Вторая — технический разбор архитектурных решений на парсере бизнес-формул через ANTLR4.

В следующих статьях планирую разбирать конкретные технические темы из LifeSync:

  • Hexagonal Architecture на Maven Multi-Module — как разложить backend на шесть модулей с чистым domain без Spring и use case'ами через @Bean в конфигурации.

  • jOOQ вместо Hibernate — почему я отказался от ORM и где это окупается, а где нет.

Репозитории проекта:

Комментарии (0)