domain-modeling
Modela o agregado, as invariantes, as portas e os eventos a partir do caso de uso.
10-dominio.md, bloco a bloco
Um parcial real: UC-001-create-customer, no bank-app, em trechos. Passe o mouse ou clique num bloco para ver o que a skill fixa ali. As seções do arquivo também são chips: clique em um para ir até ela.
UC-001 · Domain and application
Partial ofdocs/use-cases/UC-001-create-customer/. Owner:domain-modeling. Inherits the canonical names from00-caso-de-uso.md— doesn't reinvent them. Contains no code. Form references in.claude/skills/domain-modeling/templates/*.java.example.
| Field | Value |
|---|---|
| Mother spec | 00-caso-de-uso.md |
| Aggregate | Customer |
| Divergences from the mother spec | none — the open rows of its component table are closed here: name stays String, birthDate stays LocalDate, today's date comes from an injected java.time.Clock |
Package base: dev.nerviz.bankapp. Paths below are relative to src/main/java/dev/nerviz/bankapp/.
## 1 · Aggregate and value objects
Aggregate root: Customer. Boundary: the customer alone — no reference to another aggregate.
| Field | Type | Why this type |
|---|---|---|
id | CustomerId | Value object over UUID (v7). Prevents passing another aggregate's id in its place |
name | String | Free text — counter-catalog of @.claude/rules/value-objects.md. Trim, not blank and 2 to 120 characters are invariants of Customer |
securityNumber | SecurityNumber | Formation rule (exactly 11 digits) and own behaviour (masked()) — criteria 1 and 4 |
birthDate | LocalDate | java.time is already an immutable value with a unit; not wrapped (value-objects.md § Numbers). The age rules depend on today and live in Customer.register |
registeredAt | Instant | Creation instant, from the injected Clock |
Value objects
| VO | Wraps | Own invariant | Factory |
|---|---|---|---|
CustomerId | UUID | not null | CustomerId.of(UUID) |
SecurityNumber | String | not null; exactly 11 characters, each 0–9; no check-digit validation (mother spec, user's decision). of trims surrounding whitespace only — no mask removal: punctuation is rejected, not stripped | SecurityNumber.of(String) |
SecurityNumber exposes value() (the 11 digits, for persistence mapping) and masked() (********* followed by the last 2 digits). Its toString() returns masked(), so a SecurityNumber concatenated into a log line or an exception message never prints the full value.
All record, immutable, validation in the compact constructor. Shape: templates/ValueObject.java.example.
… cortado aqui, o resto está no arquivo
## 2 · Invariants
Exception family: NEW — five classes in the domain.exception package: DomainException (abstract, errorCode required), NotFoundException, ValidationException, BusinessRuleViolationException, ConflictException. Shapes in templates/DomainException.java.example and the four typed ones. NotFoundException is unused by this case and still created: the family is created once, whole.
| # | Invariant | Where it's enforced | Exception | errorCode |
|---|---|---|---|---|
| 1 | id is present | Customer compact constructor | ValidationException | CUSTOMER_ID_REQUIRED |
| 2 | name is present and not blank after trimming | Customer compact constructor | ValidationException | CUSTOMER_NAME_REQUIRED |
| 3 | name, trimmed, has 2 to 120 characters; the trimmed value is stored | Customer compact constructor | ValidationException | CUSTOMER_NAME_LENGTH |
| 4 | securityNumber is present | SecurityNumber compact constructor / Customer compact constructor | ValidationException | SECURITY_NUMBER_REQUIRED |
| 5 | securityNumber is exactly 11 digits 0–9 | SecurityNumber compact constructor | ValidationException | SECURITY_NUMBER_INVALID |
| 6 | birthDate is present | Customer compact constructor | ValidationException | BIRTH_DATE_REQUIRED |
| 7 | birthDate is not after today | Customer.register | ValidationException | BIRTH_DATE_IN_FUTURE |
| 8 | completed age at most 130: birthDate.plusYears(131) is after today | Customer.register | ValidationException | BIRTH_DATE_TOO_OLD |
| 9 | strictly over 18: birthDate.plusYears(18) is before today (equal is rejected) | Customer.register | BusinessRuleViolationException | CUSTOMER_UNDERAGE |
| 10 | registeredAt is present | Customer compact constructor | ValidationException | REGISTERED_AT_REQUIRED |
| 11 | securityNumber is unique in the system | outside the aggregate — CreateCustomerUseCase checks through CustomerRepository.existsBySecurityNumber; the unique constraint closes the race and the adapter translates its violation | SecurityNumberAlreadyRegisteredException (extends ConflictException) | SECURITY_NUMBER_ALREADY_REGISTERED |
Order inside register: 7, then 8, then 9 — a future date reports itself as such, not as underage. plusYears on 29 February yields 28 February in a non-leap year (mother spec).
… cortado aqui, o resto está no arquivo
## 3 · Ports
Input — clean architecture: no interface. The concrete use case is the input boundary.
application/usecase/customer/CreateCustomerUseCase.java
class CreateCustomerUseCase
CreateCustomerUseCase(CustomerRepository customerRepository, Clock clock)
CustomerId create(CreateCustomerCommand command)
create:
- builds
SecurityNumber.of(command.securityNumber())(invariants 4-5); customerRepository.existsBySecurityNumber(securityNumber)→trueraisesCustomer.register(new id, command.name(), securityNumber, command.birthDate(), clock)customerRepository.save(customer)and returns itsid().
The transaction opens and closes on this method (@.claude/rules/architecture-ddd.md § Application) — the annotation lives on this application-layer class. Constructor injection, private final fields. Throws ValidationException, BusinessRuleViolationException and SecurityNumberAlreadyRegisteredException.
Validating the format (step 1) before the uniqueness lookup keeps a malformed number from reaching the database query.
Command — application/usecase/customer/CreateCustomerCommand.java
… cortado aqui, o resto está no arquivo
## 4 · Events
none — the mother spec fixes no publication: no other system needs to learn that a customer was created. No CustomerCreated is designed; a future case that needs it adds it in its own ## Impact on approved use cases.
## 5 · Components to create
Refines the 00-caso-de-uso.md rows marked Detailed by: domain-modeling.
| File | Type | State |
|---|---|---|
domain/model/CustomerId.java | record VO | NEW |
domain/model/SecurityNumber.java | record VO | NEW |
domain/model/Customer.java | record aggregate | NEW |
domain/exception/DomainException.java | abstract class | NEW |
domain/exception/NotFoundException.java | class | NEW |
domain/exception/ValidationException.java | class | NEW |
domain/exception/BusinessRuleViolationException.java | class | NEW |
domain/exception/ConflictException.java | class | NEW |
domain/exception/SecurityNumberAlreadyRegisteredException.java | class, extends ConflictException | NEW |
application/usecase/customer/CreateCustomerCommand.java | record | NEW |
application/usecase/customer/CreateCustomerUseCase.java | class | NEW |
application/port/CustomerRepository.java | interface | NEW |
Requirement for the wiring, not a domain class: a java.time.Clock bean (Clock.systemUTC()) in infrastructure/config/ClockConfig.java — NEW, none exists. Written by the executor with the configuration; recorded here because the use case's constructor needs it.
## Design patterns
none — no force in the spec, no symptom on disk. The static factories register / rehydrate are the naming rule's creation convention (@.claude/rules/naming.md § Methods), not a catalog pattern; nothing in the mother spec enumerates variants or rules per type, and src/main/java holds no domain or application class yet.
## Impact on approved use cases
none — UC-001 is the first case.
O que lê, escreve e recusa
O contrato da própria skill, do SKILL.md, no texto original.
Class
design — the territory is skill_classes.design in @.claude/schemas/extensions.json and ArchHook.java guard enforces it. Writes inside the use case folder and nothing else; a write anywhere outside is refused with exit 2.
Reads
docs/use-cases/UC-NNN-<slug>/00-use-case.md (mandatory — stops without it), @.claude/rules/architecture-ddd.md (Domain and Application sections), @.claude/rules/value-objects.md (criterion for which field becomes a value object), @.claude/rules/naming.md, @.claude/rules/error-handling.md, @.claude/rules/code-quality.md, @.claude/rules/logging.md (which fields to flag as masking candidates), @.claude/rules/spec-diagrams.md (the diagrams of block 1) with references/mermaid-class-and-state.md, @.claude/rules/authorization.md § Boundary (only when Access is the owner — step 4c), @.claude/rules/spec-external-facts.md (an identifier or value of another system that a domain name or invariant states carries a source), and the active blueprint's packages.map.
Writes
docs/use-cases/UC-NNN-<slug>/10-domain.md. Only that file.
Does not write Java code
The classes in domain/ and application/ come from the executor agent — the exception family included: this skill owns the shape (the templates/ exemplars) and the state (NEW or REUSE, in the partial), not the writing. Does not touch 00-use-case.md (use-case-design), the other partials, or .claude/rules/**.
Does not collide with `gof-design-patterns`
That skill owns the catalog and, invoked standalone, refactors code that already hurts; this one decides which catalog row shapes the domain of the case being designed, and writes the decision into the partial — step 5b. Different artifacts, one owner of each.
Does not
decide persistence, mapping, or transport technology — 20-persistence.md and 30-rest.md have their own owners.
Fonte: SKILL.md § Contract, no nerviz e4cae27.
O que o guard faz com ela
design é a classe da skill. O ArchHook abre uma fase quando a skill é chamada e confere cada escrita contra o território da classe. O texto à direita é o do próprio hook.
write_allow da classe design
- ✓
docs/use-cases/UC-*/** - ✓
docs/use-cases/BACKLOG.md src/**: migrations e classes são do executordocker-compose.yml: arquivo de outra classecaso de uso já aprovado: as specs ficam congeladas
A recusa sai com código 2 e a mensagem volta para o modelo.
PreToolUse Skill(domain-modeling) audit call exit 0 guard call exit 0 # a skill of another class replaces the phase # write_allow: docs/use-cases/UC-*/**, docs/use-cases/BACKLOG.md
PreToolUse Write(docs/use-cases/UC-005-record-kyc-verdict/10-dominio.md) guard write exit 0 PostToolUse audit file exit 0
PreToolUse Write(docker-compose.yml) guard write exit 2 ❌ `domain-modeling` is class `design` — docker-compose.yml is outside its territory. write_allow: docs/use-cases/UC-*/**, docs/use-cases/BACKLOG.md Put the content in the spec as a code block, or finish the run and invoke the skill that owns this path. If the path is legitimately this skill's, widen skill_classes.design in .claude/schemas/extensions.json — retrying will not help.
# UC-005-record-kyc-verdict-spec.md: status: approved PreToolUse Edit(docs/use-cases/UC-005-record-kyc-verdict/10-dominio.md) guard write exit 2 ❌ UC-005-record-kyc-verdict is approved — its specs are immutable. Record the change in the new use case's "Impact on approved use cases" section, and the one-line entry in UC-005-record-kyc-verdict/CHANGELOG.md, which stays writable.
Onde ela fica no trilho
Cada cartão é um passo do /new-feature; a seta leva o parcial que um passo entrega ao seguinte. Passos tracejados só rodam quando o caso de uso precisa.
/new-feature orchestrator · opus/new-feature-implement Quem lê o parcial depoisDepois da aprovação, o /new-feature-implement faz o executor construir o código. A spec é consolidada por referência e vale onde ela e um parcial divergem; o parcial é lido, por seção, para o detalhe.