Catalog Service: пошаговая генерация от бизнес-описания до кода
Полный сеанс работы с Use Case Pattern на примере Catalog Service: конкретные промпты, выдержки из ответов AI, последовательность команд от бизнес-описания до работающего кода. Tier B за 3 часа.
Полный сеанс работы с методологией на конкретном сервисе. У нас есть спецификация Catalog Service и бизнес-описание маркетплейса. Здесь показана последовательность команд — как из бизнес-описания через серию вызовов AI-скиллов получаются именно эти артефакты.
Это не литературная фантазия и не записанный лог одной сессии — это репрезентация типичного сеанса: какие промпты подавать, какие выдержки ждать в ответе, какая команда идёт следующей. Реальная сессия будет отличаться в деталях (модель формулирует свободно), но общий контур — ровно такой.
Сервис выбран намеренно: Catalog — Tier B, средняя сложность. Не такой простой как Notification (Tier A, без агрегатов), не такой большой как Order (Tier C, с сагами). Хороший образец для понимания, как методология приземляется на реальный модуль.
Стартовая точка: бизнес-описание
В корне кейса лежит бизнес-бриф маркетплейса, написанный без архитектурных терминов — формулировка «как я понял задачу». Из него для Catalog нас интересуют несколько кусков:
Товар — конкретное изделие у конкретного продавца. Один и тот же iPhone у двух разных продавцов — это два разных товара.
Размещает товары. Создаёт карточку, заполняет описание, ставит цену и остаток, выбирает категорию. Карточка идёт на модерацию...
Order Service берёт у Catalog цену по
productIdдля оформления заказа.Только Россия и только рубли.
Этого достаточно, чтобы запустить методологический pipeline.
Шаг 1. Спецификация из бизнес-описания
Открываем Claude Code в проекте. Первая команда:
/ucp-spec-design Сервис каталога маркетплейса. Продавец публикует и
скрывает товары. Order Service берёт у Catalog цену по productId для
оформления заказа. Никаких категорий, поиска, фото — только базовая
карточка товара (title, description, price, seller_id, status).
Tier B — UseCase Pattern с CQRS, без DDD-агрегатов. ABAC: продавец
меняет только свои товары.
Скилл за 1-2 минуты возвращает структуру:
✓ Создан docs/spec/catalog-service.md (консолидированная версия)
✓ Создан docs/spec/01-context.md
✓ Создан docs/spec/02-language.md
... (всего 14 файлов под Tier B — без §8 events и §12 sagas)
Выдержки из созданных артефактов:
docs/spec/02-language.md — Ubiquitous Language
| Термин | Определение |
|---|---|
| **Product** | Карточка товара конкретного продавца. Один и тот же iPhone у двух
| | продавцов — два разных Product. Никакого «склеивания SKU». |
| **Seller** | Продавец маркетплейса. У продукта ровно один owner-seller. |
| **Status** | Состояние карточки: DRAFT, PUBLISHED, HIDDEN. Перевод между
| | состояниями — отдельный UseCase. |
docs/spec/04-lifecycle.md — State Machine
Скилл сам построил цикл из бизнес-описания:
DRAFT --PublishProduct--> PUBLISHED
PUBLISHED --HideProduct--> HIDDEN
HIDDEN --PublishProduct--> PUBLISHED
docs/spec/06-rules.md — Бизнес-правила
Шесть правил, выведенных из брифа автоматически:
BR-C1 Цена обязательна, > 0. Валидация в record-constructor CreateProductUseCase.
BR-C2 Валюта — на старте только RUB.
BR-C3 Уникальность ID — UUID на сервере, никогда от клиента.
BR-C4 Только владелец публикует/скрывает. seller_id из JWT vs product.seller_id.
BR-C5 Допустимые переходы: Publish из DRAFT|HIDDEN, Hide из PUBLISHED.
BR-C6 GET /products/{id} отдаёт только PUBLISHED. На DRAFT|HIDDEN — 404.
docs/spec/07-commands.md — UseCase'ы
| UseCase | Кто | Из какого статуса | Что делает |
|---|---|---|---|
| CreateProductUseCase | seller | — | создаёт DRAFT |
| PublishProductUseCase | seller | DRAFT | HIDDEN | переводит в PUBLISHED |
| HideProductUseCase | seller | PUBLISHED | переводит в HIDDEN |
docs/spec/15-acceptance.md — Acceptance criteria
Девять AC-правил, каждое — конкретное проверяемое условие:
AC-C1 POST /products от seller'а создаёт продукт DRAFT с автогенерённым UUID.
AC-C2 POST /products/{id}/publish переводит DRAFT|HIDDEN → PUBLISHED.
AC-C3 POST /products/{id}/hide переводит PUBLISHED → HIDDEN.
AC-C4 Чужой seller получает 404 (OWN_PRODUCT_REQUIRED).
... (и т.д.)
В консолидированном catalog-service.md всё то же, объединённое в один читаемый markdown — для шаринга с бизнесом и для ингеста в другие AI-сессии.
Что важно: скилл сам определил Tier (B), сам пропустил §8 events и §12 sagas (для Tier B это лишнее), сам вывел state machine из бизнес-описания. Ничего из этого мы вручную не указывали.
Шаг 2. План реализации из спеки
Теперь нужен оркестратор — план шагов реализации. Подключается superpowers:
/superpowers:writing-plans
Скилл читает docs/spec/ и производит план:
Plan для Catalog Service (Tier B)
Phase 1: Bootstrap
1.1 Скаффолд Gradle multi-module (никаких — одно-модульный для Tier B)
1.2 application.yml + application-local.yml + application-integration-test.yml
1.3 Liquibase changeset v-1.0/initial-schema.yaml — таблица products
1.4 jOOQ codegen из applied schema → ru.vikulinva.catalog.generated
1.5 SecurityConfig (3 профиля) с OAuth2 Resource Server
1.6 ServiceBeansConfig — DateTimeService, UuidGenerator под @ConditionalOnMissingBean
Phase 2: UseCase'ы
2.1 CreateProductUseCase + Handler + контроллер + тесты
2.2 PublishProductUseCase + Handler + контроллер + тесты
2.3 HideProductUseCase + Handler + контроллер + тесты
Phase 3: Queries
3.1 GetProductQuery + Handler + контроллер + тесты
3.2 ListMyProductsQuery + Handler + контроллер + тесты
Phase 4: Cross-cutting
4.1 ProblemDetails handler — RFC 9457 ошибки
4.2 ArchUnit тесты на инварианты слоёв
4.3 Финальный прогон скиллов методологии для review
Acceptance:
каждый шаг → gradle compileJava + gradle test зелёные
финал → все 9 AC из docs/spec/15-acceptance.md проверены тестом
План в контексте сессии. Дальше executing-plans будет идти по нему.
Шаг 3. Исполнение
/superpowers:executing-plans
Дальше скилл идёт по плану. На каждом шаге вызывает соответствующий ucp-*-design. Покажу выдержки трёх ключевых шагов.
Шаг 3.1. Bootstrap (Phase 1)
Скилл вызывает:
/ucp-bootstrap-design Catalog Service Tier B. Один модуль, Spring Boot 3.4,
PostgreSQL, jOOQ, Liquibase. OAuth2 Resource Server. Профили: prod / local /
integration-test. Generated-only persistence (BS-17/18) — никакого
JdbcTemplate/JPA, никаких handcrafted POJO для строк БД.
Что создаётся:
build.gradle.kts — Spring Boot 3.4 + jOOQ codegen + Liquibase
src/main/resources/
application.yml — production defaults
application-local.yml — postgres из docker-compose, security permitAll
application-integration-test.yml — WireMock-stubs, listener auto-startup off
src/main/java/.../config/
SecurityConfig.java — @Profile("!local & !integration-test"), OAuth2
LocalSecurityConfig.java — @Profile("local"), permitAll
TestJwtConfiguration.java — @Profile("integration-test"), permitAll
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 появляются generated POJO в build/generated/jooq/main/ru/vikulinva/catalog/generated/. Все правила BS-17/18 (jOOQ-only, generated-only) соблюдены.
Шаг 3.2. CreateProductUseCase (Phase 2.1)
Скилл вызывает:
/ucp-pattern-design Команда «продавец создаёт DRAFT-карточку товара».
Вход: SellerId, title, description, price (BigDecimal, > 0), currency.
Валидация в record-constructor (BR-C1, BR-C2). Возвращает Product
(generated POJO от jOOQ).
Что создаётся:
// 1. UseCase — record с валидацией в compact-constructor
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");
}
}
// 2. Handler — Spring-bean с @Transactional
@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);
}
@Override
public Class<CreateProductUseCase> useCaseType() {
return CreateProductUseCase.class;
}
}
// 3. Controller — реализует ProductsApi (OpenAPI-generated)
@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));
}
}
Плюс тесты:
@SpringBootTest
@Transactional
class CreateProductHandlerIntegrationTest extends BaseIntegrationTest {
@Autowired UseCaseDispatcher dispatcher;
@MockitoBean UuidGenerator uuidGenerator;
@MockitoBean DateTimeService dateTimeService;
@Test
void shouldCreateProductWithDraftStatus() {
when(uuidGenerator.generate()).thenReturn(UUID.fromString("..."));
when(dateTimeService.now()).thenReturn(Instant.parse("2026-04-29T10:00:00Z"));
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);
assertThat(product.getTitle()).isEqualTo("iPhone 15 Pro 256GB");
// ... и т.д.
}
@Test
void shouldRejectNonPositivePrice() {
assertThatThrownBy(() ->
new CreateProductUseCase(
new SellerId(UUID.randomUUID()),
"Test",
"",
BigDecimal.ZERO,
"RUB"
)
).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("price > 0");
}
// ... ещё 5 тестов на каждое BR-C1..6
}
ProductRepository — generated jOOQ POJO Product + repository-обёртка с DSL на ProductsTable. Никакого JPA-Entity.
Шаг 3.3. PublishProductUseCase (Phase 2.2)
Здесь интереснее — нужна проверка ABAC и переход статуса. Скилл вызывает:
/ucp-pattern-design Команда «продавец публикует свой товар».
Вход: SellerId requesterId, ProductId productId.
Логика: SELECT по id, проверка product.sellerId == requesterId (BR-C4),
проверка статус ∈ {DRAFT, HIDDEN} (BR-C5), UPDATE статус на PUBLISHED.
Если не владелец → OwnProductRequiredException (404).
Если статус неподходящий → InvalidStateTransitionException (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
}
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 атомарно. Между ними не должно происходить ничего другого с этим продуктом.
Шаги 3.4–3.5. HideProduct и Queries
По тому же шаблону. Каждый занимает ~1 минуту работы скилла + ~2 минуты на чтение и применение. Опускаю выдержки — структурно не отличаются от 3.2 и 3.3.
Шаг 3.6. API + ProblemDetails
/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). Currency только RUB.
Скилл создаёт openapi/catalog-api.yaml с полными контрактами и @ControllerAdvice ProblemDetailsHandler, который ловит все доменные исключения и формирует RFC 9457 ProblemDetails:
# выдержка из openapi/catalog-api.yaml
paths:
/products/{id}/publish:
post:
operationId: publishProduct
tags: [Products]
parameters:
- in: path
name: id
schema: { type: string, format: uuid }
required: true
responses:
'200':
description: Product published
content: { application/json: { schema: { $ref: '#/components/schemas/Product' } } }
'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' }
Из этого OpenAPI-yaml через openapi-generator-gradle-plugin генерируется интерфейс ProductsApi, который реализует наш ProductsController (мы это уже видели в шаге 3.2).
После каждого шага
/superpowers:verification-before-completion:
✓ ./gradlew compileJava — passed
✓ ./gradlew test — 23 tests, all green
✓ ./gradlew check — все проверки прошли
Не зелёное — не двигаемся дальше. Соблазн «потом починим» — обнуляется.
Шаг 4. Финальный разбор
Вызываем все скиллы методологии последовательно по diff-у:
/ucp-pattern-review
Скилл проходит по созданным файлам, цитирует правила:
✓ Контроллер не содержит бизнес-логики — соответствует R-3
✓ Handler с @Transactional — соответствует R-7
✓ UseCase — immutable record с валидацией в compact-ctor — соответствует R-1
⚠ В CreateProductHandler можно вынести setUpdatedAt в общий хелпер,
если он повторится в других UseCase'ах. Не блокер — suggestion.
/ucp-api-review
✓ kebab-case в URL
✓ camelCase в JSON
✓ ProblemDetails RFC 9457 corrects
✓ OpenAPI metadata (operationId, tags) присутствуют
/ucp-java-style-review
✓ Imports без wildcard
✓ Имена переменных без аббревиатур
✓ Длинных лямбд не найдено
✓ Все строки ≤ 120 символов
И финал — внешний взгляд:
/superpowers:requesting-code-review
Subagent-ревьюер находит более тонкие вещи:
В PublishProductHandler — порядок проверок: сначала ownership, потом status.
Это правильный порядок (не светим существование чужих продуктов через
status-error, отсюда BR-C4 первым). Хорошо.
Но в HideProductHandler порядок обратный — сначала status, потом ownership.
Это рассогласование. Унифицировать.
Реальная находка человека-эксперта, которую linter и unit-тест не поймали бы.
Что получилось
Чек-лист соответствия спецификации:
| AC из спеки | Покрыто в коде |
|---|---|
| AC-C1 (POST /products → DRAFT) | ✅ |
| AC-C2 (publish → PUBLISHED) | ✅ |
| AC-C3 (hide → HIDDEN) | ✅ |
| AC-C4 (чужой seller → 404) | ✅ |
| AC-C5 (invalid transition → 409) | ✅ |
| AC-C6 (price ≤ 0 → 400) | ✅ |
| AC-C7 (GET only PUBLISHED → 404 для DRAFT/HIDDEN) | ✅ |
| AC-C8 (GET /products/my пагинация) | ✅ |
| AC-C9 (Order Service smoke-test) | ✅ |
Все 9 acceptance criteria из docs/spec/15-acceptance.md имеют интеграционный тест. Ни одно правило не «забылось».
Соответствие BR-C1..C6 — то же самое. Каждое бизнес-правило явно проверяется тестом с @DisplayName("BR-C5: …") для трассируемости.
Сколько времени заняло
Реалистичная оценка для этого сценария:
| Этап | Время |
|---|---|
| Шаг 1 (спека) | 3-5 минут на работу скилла + 5-10 минут на чтение |
| Шаг 2 (план) | 2 минуты |
| Шаг 3 (исполнение) | ~2 часа: bootstrap + 5 UseCase/Query + API + auth + tests, по 15-20 минут на шаг |
| Шаг 4 (разбор) | 20-30 минут на финальный разбор + правки |
| Итого | ~3 часа от бизнес-описания до готового сервиса с тестами |
Для сравнения: вручную писать тот же сервис с нуля, без AI и методологии, занимает у опытного Java-разработчика 2-3 рабочих дня. С AI без методологии — 1 день, но без согласованности с другими сервисами и без покрытия acceptance criteria. С методологией и AI вместе — 3 часа.
Это и есть множитель производительности. Не «AI стал писать в 100 раз быстрее», а «методология + AI закрывают 8-часовой день в 3-часовой блок с гарантированным соответствием спецификации».
Что не делалось в этом walkthrough
Для честности — что осталось за кадром:
- Деплой в прод — этот walkthrough кончается на «зелёные тесты + готовый код в репо». До прода ещё CI/CD pipeline, infrastructure, observability. Это отдельная история и AI её сильно не ускоряет.
- Брейншторм требований — у нас был готовый бизнес-бриф. На реальном проекте начинается с
/superpowers:brainstormingили сессии Event Storming, и выход того брейнсторма уже идёт в/ucp-spec-design. - Интеграционные edge cases — если бизнес добавил «продукт может быть в нескольких категориях», это уже Tier B+ или C. Walkthrough на Catalog в Tier C выглядит иначе и потяжелее.
Что дальше
- Спецификация Catalog Service — что было на выходе шага 1, в полной форме
- Use Case Pattern: пошаговый гид по применению — два сценария методологии без привязки к конкретному сервису
- Скиллы UCP — каталог и установка — каждый из вызванных в этом walkthrough скиллов
- Order Service (Tier C) — следующий уровень сложности; по нему walkthrough получится длиннее (плюс события, сага, outbox)