В первой статье я писал про 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.specify, speckit.plan, speckit.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.md, plan.md, tasks.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-typescript, orval, @hey-api), и я её рассматривал. Не пошёл по двум причинам:
Контроль над формой типов. Сгенерированные типы часто буквально повторяют структуру API, со всей nullability и opt'ами. Для UI часто удобнее немного перекомпонованный тип, который ближе к тому, что отрисовывается на экране. Ручное написание оставляет это под контроль.
Типов мало, изменений ещё меньше. 230 строк типов на 32 endpoint'а — это контролируемый объём. Каждое изменение API сопровождалось ручной правкой типов на фронте, и это не превращалось в трудоёмкую задачу.
Внутри specs/ каждой фичи фронта лежит локальный contracts/-каталог с markdown-описанием endpoint'ов, которые эта фича использует — auth-api.md, goals-api.md, habits-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 и где это окупается, а где нет.
Репозитории проекта:
Backend — github.com/zahaand/lifesync-backend
Frontend — github.com/zahaand/lifesync-frontend