← назад к разделу

Хотите увидеть, как методология работает в действии? Эта статья — полный разбор одного прохода: берём бизнес-описание маркетплейса, и шаг за шагом превращаем его в готовый 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.