Want to see the methodology in action? This article is a full walkthrough of a single pass: we take a marketplace business brief and, step by step, turn it into a finished Spring Boot service with tests.
No magic — specific commands, specific code, specific results.
Why do such a pass — and what usually goes wrong
When a developer gets the task "build a catalog service", the typical problem looks like this: different parts of the code are written in different styles, some of the business rules are "guessed" from memory, and the tests check what "seems important" rather than what the customer actually asked for.
The result: a week of writing, a week of fixing, and then the business shows up and says "that's not quite it."
Here we show a different approach: first we formalize the requirements, then we generate code that matches them exactly. And at the end — an automatic check of every rule by a test.
Starting point: what we have
We have a marketplace business brief — text without any architectural terminology, written "as I understood the task." Here are the key pieces that concern the catalog:
Product — a specific item from a specific seller. The same iPhone from two different sellers is two different products.
A seller creates a listing, fills in the description, sets a price and stock. The listing goes through moderation before it is published.
Order Service pulls the price from Catalog by
productIdwhen placing an order.Russia only, rubles only.
That is enough. Now we run the methodology pipeline.
Step 1. From text to a formal specification
A developer without a formal spec works from memory and guesswork. The first step is to turn free-form text into a structure you can actually work with.
The command in Claude Code:
/ucp-spec-design Marketplace catalog service. A seller publishes and
hides products. Order Service pulls the price from Catalog by productId to
place an order. No categories, search, or photos — only the basic
product listing (title, description, price, seller_id, status).
Level 2 — UseCase Pattern with CQRS, no DDD aggregates. ABAC: a seller
changes only their own products.
In 1–2 minutes the skill creates docs/spec/catalog-service-spec.md. Here is what ends up in it:
A shared vocabulary of terms
Product — a specific seller's product listing. The same iPhone from two sellers is two Products.
Seller — a seller. Each Product has exactly one owner-seller.
Status — the listing state: DRAFT, PUBLISHED, HIDDEN.
The listing lifecycle — the skill builds it from the text on its own:
DRAFT --PublishProduct--> PUBLISHED
PUBLISHED --HideProduct--> HIDDEN
HIDDEN --PublishProduct--> PUBLISHED
Business rules — six of them, derived automatically:
BR-C1 Price is required and greater than zero.
BR-C2 Currency — RUB only.
BR-C3 Product ID — a UUID, generated on the server, never from the client.
BR-C4 Only the owner may publish and hide their product.
BR-C5 Allowed transitions: Publish from DRAFT or HIDDEN, Hide from PUBLISHED.
BR-C6 GET /products/{id} returns only PUBLISHED. For DRAFT or HIDDEN — 404.
Commands — three write operations:
CreateProductUseCase — a seller creates a DRAFT
PublishProductUseCase — a seller moves DRAFT|HIDDEN → PUBLISHED
HideProductUseCase — a seller moves PUBLISHED → HIDDEN
Acceptance criteria — nine verifiable conditions:
AC-C1 POST /products → creates a DRAFT with an auto-generated UUID.
AC-C2 POST /products/{id}/publish → DRAFT|HIDDEN becomes PUBLISHED.
AC-C3 POST /products/{id}/hide → PUBLISHED becomes HIDDEN.
AC-C4 A stranger seller gets 404 (OWN_PRODUCT_REQUIRED).
...and so on.
An important detail: the skill determined the complexity level itself, skipped the sections on events and sagas itself (this service doesn't need them), and derived the state machine from the text itself. None of this did we specify by hand.
Step 2. From the specification to a work plan
Now we need to break the implementation down into manageable steps:
/superpowers:writing-plans
The skill reads docs/spec/ and proposes a plan in vertical slices — each step builds and is tested on its own:
Phase 1: Bootstrap
— Gradle, application.yml, Liquibase, jOOQ codegen, SecurityConfig
Phase 2: Commands
— CreateProductUseCase + tests
— PublishProductUseCase + tests
— HideProductUseCase + tests
Phase 3: Queries
— GetProductQuery + tests
— ListMyProductsQuery + tests
Phase 4: Finishing up
— ProblemDetails handler (RFC 9457 errors)
— Final review through review skills
The rule: we don't move on to the next step until the current one is green.
Step 3. Implementation, step by step
/superpowers:executing-plans
The skill walks through the plan, calling the right tool at each step. Let me show three key moments.
Bootstrap — the service skeleton
/ucp-bootstrap-design Catalog Service, Level 2. A single module, Spring Boot 3.4,
PostgreSQL, jOOQ, Liquibase. OAuth2 Resource Server. Profiles: prod / local /
integration-test. Only generated code from jOOQ — no JPA, no JdbcTemplate.
What appears in the project:
build.gradle.kts — Spring Boot 3.4 + jOOQ codegen + Liquibase
src/main/resources/
application.yml — production settings
application-local.yml — postgres from docker-compose, no authorization
application-integration-test.yml — WireMock stubs, test mode
src/main/java/.../config/
SecurityConfig.java — OAuth2 Resource Server
LocalSecurityConfig.java — for local development
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
After ./gradlew update && ./gradlew generateJooq, the generated table classes show up in build/generated/jooq/. We do not write POJOs for database rows by hand.
CreateProductUseCase — the first write command
/ucp-pattern-design Command "a seller creates a DRAFT product listing".
Input: SellerId, title, description, price (BigDecimal, > 0), currency.
Validation in the record constructor (BR-C1, BR-C2). Returns Product.
Three parts get created:
UseCase — a statement of intent with the validation right inside it:
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 — the single place with business logic and a transaction:
@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 — only the HTTP → UseCase mapping, no logic:
@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));
}
}
Plus tests that check every business rule:
@Test
void shouldCreateProductWithDraftStatus() {
var product = dispatcher.dispatch(new CreateProductUseCase(
new SellerId(UUID.fromString("...")),
"iPhone 15 Pro 256GB",
"Brand new condition",
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 — ownership check and status transition
This one is more interesting — you need two guards: who is allowed, and is it allowed right now.
/ucp-pattern-design Command "a seller publishes their product".
Input: SellerId requesterId, ProductId productId.
Logic: load, check owner == requester (BR-C4), check
status ∈ {DRAFT, HIDDEN} (BR-C5), move to PUBLISHED.
If not the owner → 404. If the status is invalid → 409.
The 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 first
}
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);
}
Note the order: first we check who the owner is, then the validity of the transition. This matters: if a stranger sends an invalid status, they should get "not found", not "invalid transition" — otherwise we reveal the existence of other people's products.
@Transactional here really matters: the SELECT, the two checks, and the UPDATE must be atomic.
The remaining steps
HideProduct and the queries (GetProduct, ListMyProducts) follow the same pattern. Each takes roughly 15–20 minutes, including reading the code and applying it. Structurally they don't differ from the examples above.
API and errors
/ucp-api-design Catalog API. Endpoints POST /products, /publish, /hide,
GET /{id}, GET /my. ProblemDetails per RFC 9457: PRODUCT_NOT_FOUND (404),
OWN_PRODUCT_REQUIRED (404), INVALID_STATE_TRANSITION (409),
INVALID_PRICE (400), INVALID_CURRENCY (400). Currency RUB only.
The skill creates openapi/catalog-api.yaml. A plugin generates the ProductsApi interface from it — the very one our controller implements. The contract and the implementation don't drift apart: they are wired together through the generator.
An excerpt from the 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' }
After every step — a mandatory check
/superpowers:verification-before-completion
✓ ./gradlew compileJava — passed
✓ ./gradlew test — 23 tests, all green
✓ ./gradlew check — all checks passed
Not green — we don't move on. This is discipline, not a formality.
Step 4. The final review
Once everything is written and the tests are green, we run the review skills:
/ucp-pattern-review
/ucp-api-review
/ucp-java-style-review
They walk through the files and quote the rules. Most of the time — confirmations that everything is correct. Occasionally — a concrete note:
⚠ In CreateProductHandler, setUpdatedAt could be extracted into a shared helper
if it recurs in other UseCases. Not a blocker — a suggestion.
And a final outside look:
/superpowers:requesting-code-review
Here a real problem turned up: in HideProductHandler the order of checks was reversed — status first, then owner — even though in PublishProductHandler it was correct, owner first. An inconsistency. We fixed it.
Neither a linter nor an ordinary unit test would catch this.
The result: every requirement is covered
| Criterion from the specification | Covered by a test |
|---|---|
| AC-C1: POST /products → DRAFT | yes |
| AC-C2: publish → PUBLISHED | yes |
| AC-C3: hide → HIDDEN | yes |
| AC-C4: stranger seller → 404 | yes |
| AC-C5: invalid transition → 409 | yes |
| AC-C6: price ≤ 0 → 400 | yes |
| AC-C7: GET only PUBLISHED, everything else 404 | yes |
| AC-C8: GET /products/my with paginated output | yes |
| AC-C9: Order Service smoke-test | yes |
All nine acceptance criteria are covered by integration tests. Each test is tagged with @DisplayName("BR-C5: …") — you can trace which rule is checked by which test.
How long it takes
| Stage | Time |
|---|---|
| Specification | 3–5 min. skill + 5–10 min. reading |
| Plan | 2 min. |
| Implementation (bootstrap + 5 commands/queries + API + auth + tests) | ~2 hours |
| Final review and fixes | 20–30 min. |
| Total | ~3 hours from text to a finished service |
For comparison: writing the same service from scratch by hand takes an experienced Java developer 2–3 working days. With AI but no methodology — about a day, but with no guarantees of consistency and no explicit traceability back to the requirements. With the methodology and AI — 3 hours, and every acceptance criterion is covered by a test.
What was left out of frame
To be honest about it:
- Deployment — this pass ends at "green tests + code in the repository." Getting to production still means CI/CD, infrastructure, observability. AI doesn't speed that up meaningfully.
- Finding the requirements — we had a ready business brief. On a real project it comes out of brainstorming or Event Storming, and its output is what feeds into
/ucp-spec-design. - Growing complexity — if the business adds "a product can belong to several categories," that's a different complexity level with aggregates and events. The pass would look different.
In short
- Business brief → formal specification in 1–2 minutes with the
/ucp-spec-designcommand. - The specification holds the shared vocabulary, the lifecycle, the business rules, and the acceptance criteria — all in one document.
- Implementation is sliced into vertical slices: each step (UseCase + Handler + Controller + tests) builds and is verified on its own.
- Every business rule is covered by a test with an explicit reference to the rule (
BR-C4,AC-C7). - The order of checks in a Handler matters: owner first, status second — otherwise we leak information about other people's data.
@Transactionalis needed wherever SELECT + checks + UPDATE must be atomic.- The final review skill and code review catch what tests don't: behavioral inconsistency between similar UseCases.
- The bottom line: ~3 hours instead of 2–3 days, with full coverage of every acceptance criterion.
Further reading
- Catalog Service specification — the full specification that came out of step 1.
- Use Case Pattern: a step-by-step guide to applying it — how to apply the methodology in other scenarios.
- UCP skills — catalog and installation — each of the tools invoked here.
- Order Service (a more advanced level) — the next level up, with events, a saga, and a transactional outbox.