Хотите увидеть, как методология работает в действии? Эта статья — полный разбор одного прохода: берём бизнес-описание маркетплейса, и шаг за шагом превращаем его в готовый Spring Boot сервис с тестами.
Никакой магии — конкретные команды, конкретный код, конкретные результаты.
Зачем нужен такой проход — и что обычно идёт не так
Когда разработчик получает задачу «сделай сервис каталога», типичная проблема выглядит так: разные части кода написаны в разных стилях, часть бизнес-правил «угадана» из головы, а тесты проверяют то, что «кажется важным», а не то, что требовал заказчик.
В итоге: неделю пишем, неделю правим, потом приходит бизнес и говорит «это не совсем то».
Здесь показан другой подход: сначала формализуем требования, потом генерируем код, который этим требованиям точно соответствует. В конце — автоматическая проверка каждого правила тестом.
Стартовая точка: что у нас есть
Есть бизнес-описание маркетплейса — текст без архитектурных терминов, написанный «как я понял задачу». Вот ключевые куски, которые касаются каталога:
Товар — конкретное изделие у конкретного продавца. Один и тот же iPhone у двух разных продавцов — это два разных товара.
Продавец создаёт карточку, заполняет описание, ставит цену и остаток. Карточка идёт на модерацию перед публикацией.
Order Service берёт у Catalog цену по
productIdдля оформления заказа.Только Россия и только рубли.
Этого достаточно. Дальше — запускаем методологический конвейер.
Шаг 1. Из текста — в формальную спецификацию
Разработчик без формальной спеки работает по памяти и догадкам. Первый шаг — превратить вольный текст в структуру, с которой можно работать.
Команда в Claude Code:
/ucp-spec-design Сервис каталога маркетплейса. Продавец публикует и
скрывает товары. Order Service берёт у Catalog цену по productId для
оформления заказа. Никаких категорий, поиска, фото — только базовая
карточка товара (title, description, price, seller_id, status).
Уровень 2 — UseCase Pattern с CQRS, без DDD-агрегатов. ABAC: продавец
меняет только свои товары.
За 1–2 минуты скилл создаёт docs/spec/catalog-service-spec.md. Что там оказывается:
Единый язык терминов
Product — карточка товара конкретного продавца. Один iPhone у двух продавцов — два Product.
Seller — продавец. У каждого Product ровно один owner-seller.
Status — состояние карточки: DRAFT, PUBLISHED, HIDDEN.
Жизненный цикл карточки — скилл сам строит из текста:
DRAFT --PublishProduct--> PUBLISHED
PUBLISHED --HideProduct--> HIDDEN
HIDDEN --PublishProduct--> PUBLISHED
Бизнес-правила — шесть, выведенных автоматически:
BR-C1 Цена обязательна и больше нуля.
BR-C2 Валюта — только RUB.
BR-C3 ID товара — UUID, генерируется на сервере, никогда от клиента.
BR-C4 Только владелец может публиковать и скрывать свой товар.
BR-C5 Допустимые переходы: Publish из DRAFT или HIDDEN, Hide из PUBLISHED.
BR-C6 GET /products/{id} отдаёт только PUBLISHED. На DRAFT или HIDDEN — 404.
Команды — три операции на запись:
CreateProductUseCase — продавец создаёт DRAFT
PublishProductUseCase — продавец переводит DRAFT|HIDDEN → PUBLISHED
HideProductUseCase — продавец переводит PUBLISHED → HIDDEN
Критерии приёмки — девять проверяемых условий:
AC-C1 POST /products → создаёт DRAFT с автогенерённым UUID.
AC-C2 POST /products/{id}/publish → DRAFT|HIDDEN становится PUBLISHED.
AC-C3 POST /products/{id}/hide → PUBLISHED становится HIDDEN.
AC-C4 Чужой продавец получает 404 (OWN_PRODUCT_REQUIRED).
...и т.д.
Важная деталь: скилл сам определил уровень сложности, сам пропустил секции про события и саги (для этого сервиса они не нужны), сам вывел машину состояний из текста. Ничего из этого мы вручную не указывали.
Шаг 2. Из спецификации — в план работы
Теперь нужно разбить реализацию на управляемые шаги:
/superpowers:writing-plans
Скилл читает docs/spec/ и предлагает план вертикальными срезами — каждый шаг собирается и тестируется отдельно:
Phase 1: Bootstrap
— Gradle, application.yml, Liquibase, jOOQ codegen, SecurityConfig
Phase 2: Команды
— CreateProductUseCase + тесты
— PublishProductUseCase + тесты
— HideProductUseCase + тесты
Phase 3: Запросы
— GetProductQuery + тесты
— ListMyProductsQuery + тесты
Phase 4: Завершение
— ProblemDetails handler (RFC 9457 ошибки)
— Финальный разбор через review-скиллы
Правило: не двигаемся на следующий шаг, пока текущий не зелёный.
Шаг 3. Реализация по шагам
/superpowers:executing-plans
Скилл проходит по плану, на каждом шаге вызывая нужный инструмент. Покажу три ключевых момента.
Bootstrap — скелет сервиса
/ucp-bootstrap-design Catalog Service Уровень 2. Один модуль, Spring Boot 3.4,
PostgreSQL, jOOQ, Liquibase. OAuth2 Resource Server. Профили: prod / local /
integration-test. Только generated-код из jOOQ — никакого JPA, никакого JdbcTemplate.
Что появляется в проекте:
build.gradle.kts — Spring Boot 3.4 + jOOQ codegen + Liquibase
src/main/resources/
application.yml — настройки для продакшена
application-local.yml — postgres из docker-compose, без авторизации
application-integration-test.yml — WireMock-заглушки, тестовый режим
src/main/java/.../config/
SecurityConfig.java — OAuth2 Resource Server
LocalSecurityConfig.java — для локальной разработки
ServiceBeansConfig.java — Clock, DateTimeService, UuidGenerator
migrations/db/
changelog-master.yaml
changelog/v-1.0/initial-schema.yaml — CREATE TABLE products
docker-compose.yml — postgres:16-alpine
После ./gradlew update && ./gradlew generateJooq — в build/generated/jooq/ появляются generated-классы таблиц. Вручную POJO для строк базы данных не пишем.
CreateProductUseCase — первая команда на запись
/ucp-pattern-design Команда «продавец создаёт DRAFT-карточку товара».
Вход: SellerId, title, description, price (BigDecimal, > 0), currency.
Валидация в record-constructor (BR-C1, BR-C2). Возвращает Product.
Три части, которые создаются:
UseCase — описание намерения с валидацией прямо в нём:
public record CreateProductUseCase(
SellerId sellerId,
String title,
String description,
BigDecimal price,
String currency
) implements UseCaseCommand<Product> {
public CreateProductUseCase {
Objects.requireNonNull(sellerId, "sellerId");
if (title == null || title.isBlank())
throw new IllegalArgumentException("title required");
if (price == null || price.signum() <= 0)
throw new IllegalArgumentException("price > 0 required");
if (!"RUB".equals(currency))
throw new IllegalArgumentException("only RUB supported");
}
}
Handler — единственное место с бизнес-логикой и транзакцией:
@Component
@RequiredArgsConstructor
class CreateProductHandler implements UseCaseHandler<CreateProductUseCase, Product> {
private final ProductRepository productRepository;
private final UuidGenerator uuidGenerator;
private final DateTimeService dateTimeService;
@Override
@Transactional
public Product handle(CreateProductUseCase uc) {
var product = new Product()
.setId(uuidGenerator.generate())
.setSellerId(uc.sellerId().value())
.setTitle(uc.title())
.setDescription(uc.description())
.setPrice(uc.price())
.setCurrency(uc.currency())
.setStatus(ProductStatus.DRAFT)
.setCreatedAt(dateTimeService.now())
.setUpdatedAt(dateTimeService.now());
return productRepository.save(product);
}
}
Controller — только маппинг HTTP → UseCase, никакой логики:
@RestController
@RequiredArgsConstructor
class ProductsController implements ProductsApi {
private final UseCaseDispatcher dispatcher;
private final ProductMapper mapper;
private final AuthenticatedSeller authenticated;
@Override
@PreAuthorize("hasRole('seller')")
public ResponseEntity<ProductDto> createProduct(CreateProductRequest request) {
var useCase = new CreateProductUseCase(
authenticated.sellerId(),
request.getTitle(),
request.getDescription(),
request.getPrice(),
request.getCurrency()
);
var product = dispatcher.dispatch(useCase);
return ResponseEntity.status(HttpStatus.CREATED).body(mapper.toDto(product));
}
}
Плюс тесты с проверкой каждого бизнес-правила:
@Test
void shouldCreateProductWithDraftStatus() {
var product = dispatcher.dispatch(new CreateProductUseCase(
new SellerId(UUID.fromString("...")),
"iPhone 15 Pro 256GB",
"Состояние новое",
new BigDecimal("89990.00"),
"RUB"
));
assertThat(product.getStatus()).isEqualTo(ProductStatus.DRAFT);
}
@Test
void shouldRejectNonPositivePrice() {
assertThatThrownBy(() ->
new CreateProductUseCase(
new SellerId(UUID.randomUUID()), "Test", "",
BigDecimal.ZERO, "RUB"
)
).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("price > 0");
}
PublishProductUseCase — проверка владельца и переход статуса
Здесь интереснее — нужны две защиты: кто имеет право, и можно ли сейчас.
/ucp-pattern-design Команда «продавец публикует свой товар».
Вход: SellerId requesterId, ProductId productId.
Логика: загрузить, проверить owner == requester (BR-C4), проверить
статус ∈ {DRAFT, HIDDEN} (BR-C5), перевести в PUBLISHED.
Если не владелец → 404. Если статус неподходящий → 409.
Handler:
@Override
@Transactional
public Product handle(PublishProductUseCase uc) {
var product = productRepository.findById(uc.productId())
.orElseThrow(() -> new ProductNotFoundException(uc.productId()));
if (!product.getSellerId().equals(uc.requesterId().value())) {
throw new OwnProductRequiredException(); // BR-C4 — сначала owner
}
if (product.getStatus() != ProductStatus.DRAFT
&& product.getStatus() != ProductStatus.HIDDEN) {
throw new InvalidStateTransitionException(
"Publish allowed only from DRAFT or HIDDEN, current: " + product.getStatus());
}
product.setStatus(ProductStatus.PUBLISHED)
.setUpdatedAt(dateTimeService.now());
return productRepository.save(product);
}
Обратите внимание на порядок: сначала проверяем, кто владелец, потом — корректность перехода. Это важно: если чужой продавец подаёт неправильный статус, он должен получить «не найдено», а не «неверный переход» — иначе мы раскрываем информацию о существовании чужих товаров.
@Transactional здесь по-настоящему важен: SELECT, две проверки и UPDATE должны быть атомарны.
Остальные шаги
HideProduct и запросы (GetProduct, ListMyProducts) — по той же схеме. Каждый занимает примерно 15–20 минут, включая чтение кода и применение. Структурно они не отличаются от примеров выше.
API и ошибки
/ucp-api-design Catalog API. Эндпоинты POST /products, /publish, /hide,
GET /{id}, GET /my. ProblemDetails по RFC 9457: PRODUCT_NOT_FOUND (404),
OWN_PRODUCT_REQUIRED (404), INVALID_STATE_TRANSITION (409),
INVALID_PRICE (400), INVALID_CURRENCY (400). Валюта только RUB.
Скилл создаёт openapi/catalog-api.yaml. Из него через плагин генерируется интерфейс ProductsApi — тот самый, который реализует наш контроллер. Контракт и реализация не расходятся: они связаны через генератор.
Выдержка из yaml:
paths:
/products/{id}/publish:
post:
operationId: publishProduct
responses:
'200':
description: Product published
'404':
description: Product not found or doesn't belong to seller
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
'409':
description: Invalid state transition
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
После каждого шага — обязательная проверка
/superpowers:verification-before-completion
✓ ./gradlew compileJava — passed
✓ ./gradlew test — 23 tests, all green
✓ ./gradlew check — все проверки прошли
Не зелёное — не двигаемся дальше. Это дисциплина, а не формальность.
Шаг 4. Финальный разбор
После того как всё написано и тесты зелёные, запускаем review-скиллы:
/ucp-pattern-review
/ucp-api-review
/ucp-java-style-review
Они проходят по файлам и цитируют правила. Большинство — подтверждения что всё верно. Иногда — конкретное замечание:
⚠ В CreateProductHandler можно вынести setUpdatedAt в общий хелпер,
если он повторится в других UseCase'ах. Не блокер — suggestion.
И финальный внешний взгляд:
/superpowers:requesting-code-review
Здесь нашлась реальная проблема: в HideProductHandler порядок проверок был обратный — сначала статус, потом владелец, хотя в PublishProductHandler правильно — сначала владелец. Рассогласование. Исправили.
Такое не поймает ни линтер, ни обычный unit-тест.
Что получилось: все требования покрыты
| Критерий из спецификации | Покрыт тестом |
|---|---|
| AC-C1: POST /products → DRAFT | да |
| AC-C2: publish → PUBLISHED | да |
| AC-C3: hide → HIDDEN | да |
| AC-C4: чужой продавец → 404 | да |
| AC-C5: недопустимый переход → 409 | да |
| AC-C6: цена ≤ 0 → 400 | да |
| AC-C7: GET только PUBLISHED, остальное 404 | да |
| AC-C8: GET /products/my с постраничной выдачей | да |
| AC-C9: Order Service smoke-test | да |
Все девять критериев приёмки покрыты интеграционными тестами. Каждый тест помечен @DisplayName("BR-C5: …") — можно отследить, какое правило проверяет какой тест.
Сколько времени занимает
| Этап | Время |
|---|---|
| Спецификация | 3–5 мин. скилл + 5–10 мин. чтение |
| План | 2 мин. |
| Реализация (bootstrap + 5 команд/запросов + API + auth + тесты) | ~2 часа |
| Финальный разбор и правки | 20–30 мин. |
| Итого | ~3 часа от текста до готового сервиса |
Для сравнения: написать тот же сервис с нуля вручную занимает у опытного Java-разработчика 2–3 рабочих дня. С AI без методологии — около дня, но без гарантий согласованности и без явной трассировки к требованиям. С методологией и AI — 3 часа, и каждый критерий приёмки покрыт тестом.
Что осталось за кадром
Для честности:
- Развёртывание — этот проход заканчивается на «зелёные тесты + код в репозитории». До продакшена ещё CI/CD, инфраструктура, наблюдаемость. AI это не ускоряет существенно.
- Поиск требований — у нас был готовый бизнес-бриф. На реальном проекте он появляется из брейнсторминга или Event Storming, и уже его выход идёт в
/ucp-spec-design. - Усложнение — если бизнес добавляет «товар может быть в нескольких категориях», это уже другой уровень сложности с агрегатами и событиями. Проход выглядел бы иначе.
Коротко
- Бизнес-описание → формальная спецификация за 1–2 минуты командой
/ucp-spec-design. - Спецификация содержит единый язык, жизненный цикл, бизнес-правила и критерии приёмки — всё в одном документе.
- Реализация режется на вертикальные срезы: каждый шаг (UseCase + Handler + Controller + тесты) собирается и проверяется отдельно.
- Каждый бизнес-правило покрыт тестом с явной ссылкой на правило (
BR-C4,AC-C7). - Порядок проверок в Handler важен: сначала owner, потом статус — иначе раскрываем информацию о чужих данных.
@Transactionalнужен там, где SELECT + проверки + UPDATE должны быть атомарны.- Финальный review-скилл и code review находят то, что не ловят тесты: рассогласование поведения между похожими UseCase'ами.
- Итог: ~3 часа вместо 2–3 дней, с полным покрытием всех критериев приёмки.
Что почитать дальше
- Спецификация Catalog Service — полная спецификация, которая получилась на шаге 1.
- Use Case Pattern: пошаговый гид по применению — как применять методологию в других сценариях.
- Скиллы UCP — каталог и установка — каждый из вызванных здесь инструментов.
- Order Service (более сложный уровень) — следующий уровень с событиями, сагой и transactional outbox.