↑ ↓ 移动 · Enter 打开 · Esc 关闭点按一个结果

架构
skill · design 类 · opus

persistence-architect

设计聚合如何落盘并读回:schema、映射、适配器和迁移。

skill · designopus写入 20-persistencia.md仅限 docs/use-cases/UC-*/**
§ 它写出的文件

20-persistencia.md,逐块说明

一份真实的分文件:bank-app 中的 UC-001-create-customer,为节选。把鼠标移到某一块上或点击它,可以看到 skill 在这里固定了什么。文件的各个章节也是标签:点击即可跳转。

bank-app/docs/use-cases/UC-001-create-customer/20-persistencia.md

UC-001 · Persistence

Partial of docs/use-cases/UC-001-create-customer/. Owner: persistence-architect. Inherits the aggregate and ports from 10-dominio.md — doesn't reinvent them. Contains no code. Form references in .claude/skills/persistence-architect/templates/. Rules applied, never reproduced: @.claude/rules/persistence.md.
FieldValue
Mother spec00-caso-de-uso.md
Domain partial10-dominio.md
REST partial30-rest.md — block 4 schema requirements read: idempotency_keys (NEW), its retention
AggregateCustomer
EnginePostgreSQL 16 — postgres:16-alpine in docker-compose.yml and in TestcontainersConfiguration
Expected volume at 12 monthsnot given; one insert per creation, no listing — sizing below doesn't depend on it
Divergences from 10-dominio.mdnone

Paths relative to src/main/java/dev/nerviz/bankapp/ unless they start with src/. Survey: no migration, no @Entity, no idempotency_keys in the project — everything here is NEW.

## 1 · Schema

Table customers — one per aggregate root.

ColumnTypeNullableDefaultConstraint
iduuidno—PK — v7, assigned by the application
namevarchar(120)no—— trimmed value, 2 to 120 characters (domain)
security_numberchar(11)no—uq_customers_security_number UNIQUE (security_number) — business key, arbiter of invariant 11
birth_datedateno——
registered_attimestamptzno—filled by the application's Clock, never by the database
versionbigintno0optimistic lock — no update case yet; present so the first one doesn't need an expand migration

Indexes

IndexColumnsWhy
pk_customersidPK
uq_customers_security_numbersecurity_numberUniqueness, and serves existsBySecurityNumber

No index on name or birth_date: no query filters by them.

Table idempotency_keys — shared by every Idempotency-Key endpoint, NEW, first in the project. Columns, types and index exactly as templates/IdempotencyKeyTable.sql.example — the set is @.claude/rules/api-rest.md § Idempotency's, not redecided here: TTL 24 h (expires_at = created_at + 24 h), IN_PROGRESS lease 5 minutes, caller_identity = anonymous for this public endpoint (30-rest.md § 4).

Personal data at rest — @.claude/rules/personal-data.md § At rest.

… 此处截断,其余见文件

## 2 · Mapping

Aggregate Customer → entity CustomerEntity in infrastructure/persistence/customer/. Entity, Spring Data interface, mapper and adapter live there, package-private (@.claude/rules/persistence.md § Boundary).

Aggregate fieldDomain typeColumnTranslation
idCustomerIdidCustomerId.value() ↔ CustomerId.of(uuid)
nameStringnamedirect
securityNumberSecurityNumbersecurity_numberSecurityNumber.value() ↔ SecurityNumber.of(text); field carries @JdbcTypeCode(SqlTypes.CHAR) and @Column(length = 11) — char(11) is not String's default inference
birthDateLocalDatebirth_datedirect
registeredAtInstantregistered_atdirect, UTC
——versionentity-only, @Version long version, invisible to the domain

Reconstruction goes through Customer.rehydrate(...), never register — the date rules must not re-run against today (10-dominio.md § 1). Mapper: CustomerPersistenceMapper, static, package-private.

CustomerEntity extends AssignedIdEntity<UUID> — the id is assigned, so the entity is Persistable (@.claude/rules/persistence.md § Identity and keys). AssignedIdEntity is NEW, written once in infrastructure/persistence/shared/AssignedIdEntity.java, public abstract (templates/JpaEntity.java.example, last block); IdempotencyKeyEntity extends it too.

## 3 · Adapter and ports

PortSignature (from 10-dominio.md)QueryFetchIndex serving it
CustomerRepository.existsBySecurityNumberboolean existsBySecurityNumber(SecurityNumber securityNumber)Spring Data derived existsBySecurityNumber(String) — SELECT … LIMIT 1—uq_customers_security_number
CustomerRepository.saveCustomer save(Customer customer)saveAndFlush — INSERT (new entity, isNew = true)—pk_customers

CustomerRepositoryJpaAdapter (blueprint convention <Capability><Technology>Adapter) implements CustomerRepository; CustomerJpaRepository extends JpaRepository<CustomerEntity, UUID>. No @Transactional here — the use case owns the transaction.

Constraint translation. save calls saveAndFlush so the INSERT reaches the database inside the adapter's try. A DataIntegrityViolationException whose constraint name is uq_customers_security_number becomes SecurityNumberAlreadyRegisteredException with the original as cause; any other integrity violation is rethrown as a ConflictException with errorCode CUSTOMER_PERSISTENCE_CONFLICT. The message never carries the security number.

Concurrency: two requests with the same number — both pass existsBySecurityNumber, the second INSERT hits the unique constraint and is translated as above. No collection returned, so no pagination or JOIN FETCH.

Idempotency — NEW, shapes in templates/IdempotencyKeyStore.java.example and templates/IdempotentExecution.java.example, packages per the blueprint's packages.map:

… 此处截断,其余见文件

## 4 · Migrations

FileTypeContent
V1__create_customers.sqlexpandcustomers, its PK and unique constraint
V2__create_idempotency_keys.sqlexpandshared idempotency_keys and its expires_at index

New tables, no expand/contract pair. The executor creates each file verbatim from these blocks, re-checking <N> against the disk.

src/main/resources/db/migration/V1__create_customers.sql

-- Personal data: security_number (national identification number) and birth_date.
-- Retention: life of the customer record (UC-001 20-persistencia.md § 1).
CREATE TABLE customers (
    id              uuid          NOT NULL,
    name            varchar(120)  NOT NULL,
    security_number char(11)      NOT NULL,
    birth_date      date          NOT NULL,
    registered_at   timestamptz   NOT NULL,
    version         bigint        NOT NULL DEFAULT 0,
    CONSTRAINT pk_customers PRIMARY KEY (id),
    CONSTRAINT uq_customers_security_number UNIQUE (security_number)
);

src/main/resources/db/migration/V2__create_idempotency_keys.sql

… 此处截断,其余见文件

## 5 · Configuration

Added to src/main/resources/application.yml; what is already there (open-in-view: false, ddl-auto: validate, Flyway enabled, datasource variables) stays as is.

PropertyValueWhy
spring.datasource.hikari.maximum-pool-size10One short insert per request; a pool bigger than Postgres's default max_connections share only moves the queue
spring.datasource.hikari.minimum-idle10Equal to the max — no latency spike opening connections
spring.datasource.hikari.connection-timeout3000Rule — fail fast instead of holding the request
spring.datasource.hikari.validation-timeout2000Rule
spring.datasource.hikari.max-lifetime1800000Below the database side's connection timeout
spring.datasource.hikari.pool-namebank-app-poolReadable pool metrics
spring.jpa.show-sqlfalseRule
spring.jpa.properties.jakarta.persistence.query.timeout5000Rule — query timeout defined; the case's two queries are by unique key
spring.jpa.properties.hibernate.jdbc.time_zoneUTCRule — instants stored in UTC
spring.jpa.properties.hibernate.jdbc.batch_size50Harmless with single inserts; batching ready for later cases
spring.jpa.properties.hibernate.order_insertstrueRule when batching
spring.jpa.properties.hibernate.query.fail_on_pagination_over_collection_fetchtrueRule
spring.flyway.validate-on-migratetrueRule
spring.flyway.out-of-orderfalseRule
logging.level.org.hibernate.orm.jdbc.bindWARNTRACE would log the bound security number and birth date

Shape in templates/application-persistence.yml.example.

## 6 · Declared dependencies

none — spring-boot-starter-data-jpa, postgresql, flyway-database-postgresql and com.fasterxml.uuid:java-uuid-generator are already in pom.xml. The Postgres service exists in docker-compose.yml (postgres:16-alpine): no pending compose service.

## Design patterns

none — no force in the spec, no symptom on disk: one aggregate, no existing adapter whose skeleton this one would repeat. The idempotency store is the mandated shared mechanism.

## Deferred

DecidedMissingNormIntended owner
idempotency_keys rows expire after 24 ha scheduled, batched DELETE FROM idempotency_keys WHERE expires_at < now() — nothing deletes expired rows; the table grows by one row per creation call. No personal data in it@.claude/rules/api-rest.md § Idempotency (TTL), @.claude/rules/personal-data.md § At rest ("a retention window nobody reads is no retention")backlog — user's decision
Customer personal data kept for the life of the recorda use case that deletes or anonymizes a customer — until it exists, security_number and birth_date are kept indefinitely@.claude/rules/personal-data.md § At restbacklog

## Impact on approved use cases

none — UC-001 is the first case.

## Implementation order

  1. V1__create_customers.sql, V2__create_idempotency_keys.sql
  2. infrastructure/persistence/shared/AssignedIdEntity
  3. CustomerEntity + CustomerPersistenceMapper + CustomerJpaRepository + CustomerRepositoryJpaAdapter
  4. IdempotencyKeyEntity + IdempotencyKeyJpaRepository + IdempotencyKeyStore
  5. Properties in application.yml
  6. Integration tests against the real database (Testcontainers) — 40-testes.md
在 GitHub 上打开完整文件 →
§ 行为

它读什么、写什么、拒绝什么

skill 自己的契约,取自 SKILL.md,保留原文。

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: no migration under src/, no service in the compose file, both refused with exit 2.

Reads

docs/use-cases/UC-NNN-<slug>/00-use-case.md (its Storage row mandatory, its Audit row when present) and 10-domain.md (mandatory — stops without either), @.claude/rules/persistence.md, @.claude/rules/architecture-ddd.md (Adapters and Composition sections), @.claude/rules/naming.md, @.claude/rules/error-handling.md, @.claude/rules/lombok.md, @.claude/rules/value-objects.md, @.claude/rules/api-rest.md § Idempotency (only when step 4a applies), @.claude/rules/messaging.md § Publication timing (only when step 4b applies), and the active blueprint's packages.map. Also reads two partials whose schema requirements are this pass's input, each mandatory under its own condition: 30-rest.md for a REST-triggered case (block 4), 31-graphql.md for a GraphQL-triggered one (§ 9 — batch statements and the connection order's index), and 25-messaging.md whenever 10-domain.md's Events block names external delivery (§ 6 — the shared outbox table under Form B, a dedupe table for a consumer). And 35-jobs.md under Form B or a scheduled trigger (§ 3 replica count, § 6 tool tables, claim requirement and prune operation). And 28-http-client.md whenever 10-domain.md declares a port of kind external HTTP (§ 10 — a stored idempotency key, an unknown-outcome column). And 23-search.md when Search index is not none (§ 8 — the source's version and its change event). And 32-security.md when an audited aggregate is written through HTTP (the principal recorded as the actor). Missing any of them where it's required stops this skill instead of starting a design that a later pass would have to revise.

Writes

docs/use-cases/UC-NNN-<slug>/20-persistence.md. Nothing else — the migration SQL lives inside it.

Never writes under `src/`

Not a migration, not a class, not a property file. The executor materializes every file there from this partial.

Does not write Java code

The entities, adapters, and repositories come from the executor agent.

Does not edit `docker-compose.yml`, and does not invoke `docker-architect` either

When step 9 finds the chosen engine has no container yet, it records the user's answer in the partial's ## Compose services (@.claude/skills/docker-architect/references/compose-service-decision.md), and /new-feature-implement adds a materialize row after the code is green. The guard enforces both halves: the file is outside this class's territory, and a build-class skill is unreachable from inside a design run.

Does not decide

the storage family of an aggregate, nor weigh one family against another (00-use-case.md's Storage row), nor whether an aggregate is audited and at which level (its Audit row), the use case boundary (00-use-case.md), the domain model (10-domain.md), the transport (30-rest.md), or the tests (40-tests.md). Doesn't touch .claude/rules/**.

Does not collide with `messaging-architect`

: that one decides the publication form and owns the relay pass, the appender, and the declaration of OutboxRelayGateway / OutboxEventRecord. This one owns the outbox_events table, its mapping, and the adapter implementing that port. A form choice is never made here — an outbox requirement that contradicts 25-messaging.md is a divergence to report.

Does not collide with `jobs-architect`

: that one decides when things run — the relay's poll interval, the switch, coordination across instances, the prune's cadence — and declares OutboxRetentionGateway. This one implements that port, owns the statement it runs, the retention window, and the final form of every scheduling table 35-jobs.md § 6 asks for. The audit history's prune splits the same way: AuditHistoryPruneJob and AuditHistoryRetentionGateway are that skill's, the statement and app.audit.history.retain-for are this one's.

Does not collide with `domain-modeling`

: that one declares the output port, this one says how it's served. The port's signature belongs to the other; if it needs to change, report the divergence instead of rewriting it.

来源:SKILL.md § Contract(nerviz e4cae27)。

§ guard 与 hook

guard 如何处理它

design 是这个 skill 的类。ArchHook 在 skill 被调用时开启一个阶段,并按该类的领地检查每次写入。右侧文字是 hook 自己的输出。

design 类的 write_allow

  • ✓docs/use-cases/UC-*/**
  • ✓docs/use-cases/BACKLOG.md
  • src/**:迁移和类归执行器
  • docker-compose.yml:另一个类的文件
  • 已批准的用例:其 spec 已冻结

拒绝以退出码 2 结束,消息回到模型。

PreToolUse  Skill(persistence-architect)
  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
§ 编排

它在轨道上的位置

每张卡片是 /new-feature 的一步;箭头带着一步交给下一步的分文件。虚线的步骤仅在用例需要时运行。

/new-feature-implement 之后谁读这份分文件

批准之后,/new-feature-implement 让执行器构建代码。spec 按引用整合,与分文件不一致时以 spec 为准;分文件按章节读取,用于细节。

20-persistencia.mddocs/use-cases/UC-NNN-…/
按章节读取,用于细节
java-spring-boot-developeragent · executor
写出代码
src/**把一个用例设计成规格,不写代码
← 返回链路