bmad-method-quarkus 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/package.json +1 -1
  2. package/removals.txt +10 -0
  3. package/src/bmm-skills/agents/bmad-quarkus-build/SKILL.md +19 -16
  4. package/src/bmm-skills/agents/bmad-quarkus-build/customize.toml +7 -5
  5. package/src/bmm-skills/agents/bmad-quarkus-build/skills/bqa-setup/assets/module-help.csv +0 -1
  6. package/src/bmm-skills/agents/bmad-quarkus-build/skills/bqa-setup/assets/module.yaml +2 -9
  7. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-error-handling-i18n/SKILL.md +61 -27
  8. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-grpc-services/SKILL.md +60 -41
  9. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-hexagonal-core/SKILL.md +504 -215
  10. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-kafka-messaging/SKILL.md +199 -34
  11. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-observability-otel/SKILL.md +47 -31
  12. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-openapi-tmforum/SKILL.md +42 -25
  13. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-sql-jdbc-agroal/SKILL.md +150 -87
  14. package/src/bmm-skills/module.yaml +0 -7
  15. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/SKILL.md +0 -108
  16. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/customize.toml +0 -62
  17. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/references/architecture-review.md +0 -17
  18. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/references/prompt-quality-canon.md +0 -79
  19. package/src/bmm-skills/agents/bmad-quarkus-dev/.memlog.md +0 -12
  20. package/src/bmm-skills/agents/bmad-quarkus-dev/.memlog.md:Zone.Identifier +0 -0
  21. package/src/bmm-skills/agents/bmad-quarkus-dev/SKILL.md +0 -86
  22. package/src/bmm-skills/agents/bmad-quarkus-dev/SKILL.md:Zone.Identifier +0 -0
  23. package/src/bmm-skills/agents/bmad-quarkus-dev/customize.toml +0 -37
  24. package/src/bmm-skills/agents/bmad-quarkus-dev/customize.toml:Zone.Identifier +0 -0
  25. package/src/bmm-skills/agents/bmad-quarkus-dev/references/enrich-stories.md +0 -19
  26. package/src/bmm-skills/agents/bmad-quarkus-dev/references/enrich-stories.md:Zone.Identifier +0 -0
  27. package/src/bmm-skills/agents/bmad-quarkus-dev/references/prompt-quality-canon.md +0 -79
  28. package/src/bmm-skills/agents/bmad-quarkus-dev/references/prompt-quality-canon.md:Zone.Identifier +0 -0
  29. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/SKILL.md +0 -80
  30. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/SKILL.md:Zone.Identifier +0 -0
  31. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module-help.csv +0 -9
  32. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module-help.csv:Zone.Identifier +0 -0
  33. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module.yaml +0 -16
  34. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module.yaml:Zone.Identifier +0 -0
  35. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/cleanup-legacy.py +0 -287
  36. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/cleanup-legacy.py:Zone.Identifier +0 -0
  37. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-config.py +0 -441
  38. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-config.py:Zone.Identifier +0 -0
  39. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-help-csv.py +0 -246
  40. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-help-csv.py:Zone.Identifier +0 -0
  41. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md +0 -181
  42. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md:Zone.Identifier +0 -0
  43. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md +0 -141
  44. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md:Zone.Identifier +0 -0
  45. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md +0 -392
  46. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md:Zone.Identifier +0 -0
  47. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md +0 -148
  48. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md:Zone.Identifier +0 -0
  49. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md +0 -180
  50. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md:Zone.Identifier +0 -0
  51. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md +0 -128
  52. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md:Zone.Identifier +0 -0
  53. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md +0 -245
  54. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md:Zone.Identifier +0 -0
@@ -1,21 +1,38 @@
1
1
  ---
2
2
  name: quarkus-hexagonal-core
3
- description: Foundation for building Java + Quarkus backend services compiled to native binaries (GraalVM/Mandrel) with strict hexagonal architecture (ports & adapters). Applies to ANY Java/Quarkus backend project — DDD bounded contexts, standalone services, internal tools alike. Use this skill whenever creating, scaffolding, reviewing, or modifying ANY Quarkus backend service, module, REST/gRPC/Kafka adapter, repository, use case, or domain entity — even if the user only says "create a service", "add an endpoint", "new microservice", or "backend project". Also the canonical source for CLASS NAMING CONVENTIONS across all layers (entities, use cases, ports, adapters, DTOs, mappers, exceptions, tests), the domain-monorepo layout (apps/, libs/, contracts/, deploy/, db/, docs/), deployable app naming ({module}-{service}-ms, tenant-agnostic), and the mandatory per-app service.yaml + README.md documentation standard consult it whenever naming/renaming any Java class, scaffolding an app folder, or writing service docs. Enforces Java 25, JDBC-only persistence via Agroal (NO Panache, NO Hibernate/ORM), ArchUnit boundary + naming tests, and native-image compatibility. Project-specific directives (CLAUDE.md, ADRs, explicit instructions) override this standard where they conflict.
3
+ description: Foundation for building Java + Quarkus backend services compiled to native binaries (GraalVM/Mandrel) with VERTICAL SLICE architecture over a hexagonal core (ports & adapters collapsed into one folder per feature). Applies to ANY Java/Quarkus backend project — DDD bounded contexts, standalone services, internal tools alike. Use this skill whenever creating, scaffolding, reviewing, or modifying ANY Quarkus backend service, slice, feature, REST/gRPC/Kafka adapter, Sql data class, Handler, or DTO — even if the user only says "create a service", "add an endpoint", "new microservice", or "backend project". Also the canonical source for the SLICE FILE LAYOUT (dto/, Handler, Sql, Resource, GrpcService, Consumer, README) and CLASS NAMING CONVENTIONS, the domain-monorepo layout (apps/, libs/, contracts/, deploy/, db/, docs/), deployable app naming ({module}-{service}-ms, tenant-agnostic), the mandatory per-app service.yaml + README.md standard, and the Handler contract (@Transactional only on process(), never setAutoCommit/commit/rollback, one Connection per business operation). Enforces Java 25, JDBC-only persistence via Agroal (NO Panache, NO Hibernate/ORM), ArchUnit slice-isolation + naming tests, and native-image compatibility. Project-specific directives (CLAUDE.md, ADRs, explicit instructions) override this standard where they conflict.
4
4
  ---
5
5
 
6
- # Quarkus Hexagonal Core (Native-First)
6
+ # Quarkus Vertical Slice + Hexagonal Core (Native-First)
7
7
 
8
- Foundation skill for all backend services. Other skills in this set (error-handling-i18n, kafka-messaging, openapi-tmforum, grpc-services, observability-otel) build on the structure defined here — apply this one first when scaffolding.
8
+ Foundation skill for all backend services. The other skills in this set (sql-jdbc-agroal, error-handling-i18n, openapi-tmforum, grpc-services, kafka-messaging, observability-otel) build on the structure defined here and use the vocabulary defined here — apply this one first when scaffolding.
9
9
 
10
- **Scope & precedence:** these standards apply to any Java/Quarkus backend project, whether or not it is organized as DDD bounded contexts where examples mention a BC code (`bc05`), read it as "short module/domain prefix". They are the default, not law: a project directive that says otherwise (CLAUDE.md, an ADR, an explicit user instruction) wins over this skill. When you deviate because of such a directive, follow the directive and mention which rule was overridden.
10
+ **Scope & precedence:** these standards apply to any Java/Quarkus backend project, whether or not it is organized as DDD bounded contexts. **Module naming:** every module/bounded context is identified in code by a short, lowercase, semantic name (`iam`, `tenant`, `wallet`, `customer`) — never by an inventory/governance code like `bc01`/`bc02`. If the organization keeps a numbered BC inventory, the `bcNN` name mapping lives in governance docs (domain README identity table, service catalog); packages, topics, config keys, URLs, metrics and proto packages only ever carry the semantic name. They are the default, not law: a project directive that says otherwise (CLAUDE.md, an ADR, an explicit user instruction) wins over this skill. When you deviate because of such a directive, follow the directive and mention which rule was overridden.
11
+
12
+ ## The central idea: one feature = one folder
13
+
14
+ Classic layered hexagonal architecture spreads a single feature across eight or more files (inbound port, use case, command, result, outbound port, JDBC adapter, DTO, mapper) in four packages. This standard keeps the **hexagon's dependency rule** but collapses it into a **vertical slice**: everything one business capability needs lives in one folder, and the hexagon is expressed by *class role*, not by package depth.
15
+
16
+ | Hexagon role | Slice file | Rule |
17
+ |---|---|---|
18
+ | Driving (inbound) adapters | `<Slice>Resource` (REST), `<ProtoService>GrpcService`, `<Event>Consumer` (Kafka) | transport in/out only — no business logic, no SQL, no `Connection` |
19
+ | Application core | `<Slice>Handler` | 100% of the business logic + the transaction boundary. Knows no transport type |
20
+ | Driven (outbound) adapter | `<Slice>Sql` | plain JDBC only. No transaction control, no business rules |
21
+ | Driven (outbound) integrations | capability-named beans in `common/client` | own all Mutiny/`await`; expose plain types to Handlers |
22
+ | Data contracts | `dto/*Dto`, `dto/*Payload` | shape + form validation only |
23
+
24
+ Dependency rule (unchanged from hexagonal): **transport → Handler → Sql / integration bean**. Never the reverse, never transport → Sql, never Handler → transport type (`RestResponse`, proto message, Kafka `Message`, `Uni`).
25
+
26
+ The trade the slice makes explicit: **fewer classes, stricter roles.** There are no `*UseCase` interfaces, no `*Command`/`*Result` wrappers, no `*Mapper` classes and no `*Repository` ports. A port with exactly one implementation is a file that only costs compilation time — the `Handler`/`Sql` split already gives you the seam, and `@Mock <Slice>Sql` gives you the test double. Introduce an interface only when a **second** real implementation exists (see "When to add an interface back").
11
27
 
12
28
  ## Baseline constraints (unless a project directive overrides)
13
29
 
14
30
  1. **Java 25 LTS** (`maven.compiler.release=25`). Records, sealed interfaces, pattern matching, virtual threads where blocking I/O is unavoidable.
15
31
  2. **Native binary is the delivery artifact.** Every dependency and pattern must be GraalVM/Mandrel-compatible. Build with the Mandrel container image so builds don't depend on a local GraalVM.
16
- 3. **No ORM.** Persistence is plain JDBC through the Agroal datasource. No Panache, no Hibernate, no JPA annotations in any layer.
17
- 4. **Domain layer has zero framework imports.** No `jakarta.*`, no `io.quarkus.*`, no Jackson annotations in `domain`. Enforced by ArchUnit.
18
- 5. **Swagger UI only in dev/test** (see quarkus-openapi-tmforum skill).
32
+ 3. **No ORM.** Persistence is plain JDBC through the Agroal pool. No Panache, no Hibernate, no JPA annotations anywhere.
33
+ 4. **Blocking stack, two reactive exceptions.** `Uni`, `Multi`, `PgPool`, `SqlConnection` are banned in `Handler`, `Sql`, `Resource` and `Consumer`. Mutiny is confined to (a) `*GrpcService` server adapters, where the generated interface demands it, and (b) integration beans in `common/client`, which `await()` internally and hand back plain types.
34
+ 5. **Slices are independent.** A slice never imports another slice's `Handler`, `Sql` or `dto`. Shared code goes to `common/` or `libs/` (see "Shared code").
35
+ 6. **Swagger UI only in dev/test** (see quarkus-openapi-tmforum skill).
19
36
 
20
37
  ## Monorepo layout (per domain/module)
21
38
 
@@ -33,7 +50,7 @@ One monorepo per domain (bounded context in DDD projects) holds its deployable a
33
50
  ├── libs/ # modules shared within the domain
34
51
  ├── contracts/ # versioned API contracts (TMF/OpenAPI, JSON Schemas, protos)
35
52
  ├── deploy/ # k8s manifests, pipelines, observability-as-code
36
- ├── db/ # database migrations
53
+ ├── db/ # database migrations (versioned always; applied at boot only where the Flyway flag is on)
37
54
  ├── docs/ # architecture, ADRs, runbooks
38
55
  └── README.md # domain onboarding doc (see "Domain README")
39
56
  ```
@@ -46,136 +63,368 @@ One monorepo per domain (bounded context in DDD projects) holds its deployable a
46
63
 
47
64
  Suffixes: `-ms` (backend microservice — every Quarkus service here), `-mf` (microfrontend remote), `-module` (host/shell). Example: `wallet-backend-core-ms`. **Client and tenant never appear in app or code names** — they are runtime configuration (namespaces, labels, Helm values) applied at deploy time; the code is identical for every client and tenant.
48
65
 
49
- ## Package layout (per service/module)
66
+ ## Slice layout (the canonical structure)
67
+
68
+ An app is a flat list of slices under the base package, plus one `common/`. There are no `domain/`, `application/`, `infrastructure/` packages.
50
69
 
51
70
  ```
52
- com.<company>.<module> # <module> = BC code in DDD projects, service name otherwise
53
- ├── domain/ # PURE Java. Entities, value objects, domain events, domain services
54
- │ ├── model/ # Records for VOs, sealed interfaces for events
55
- │ ├── event/
56
- └── exception/ # Domain exceptions with stable error codes
57
- ├── application/ # Use cases + ports. May use CDI annotations only (@ApplicationScoped)
58
- │ ├── port/
59
- ├── in/ # Inbound ports (use case interfaces)
60
- │ └── out/ # Outbound ports (repository, publisher, external service interfaces)
61
- └── usecase/ # One class per use case, implements inbound port
62
- └── infrastructure/ # Adapters. All framework code lives here
63
- ├── rest/ # JAX-RS resources + DTOs + mappers (inbound adapter)
64
- ├── grpc/ # gRPC services (inbound) and clients (outbound)
65
- ├── persistence/ # JDBC repositories implementing out ports
66
- ├── messaging/ # Kafka producers/consumers, outbox
67
- └── config/ # @ConfigMapping interfaces, CDI producers
71
+ src/main/java/com/<company>/<module>/
72
+ ├── common/ # cross-slice code ONLY (see "Shared code")
73
+ │ ├── exception/BusinessException.java # single code-carrying business exception
74
+ │ ├── exception/PersistenceException.java # technical exception family
75
+ ├── error/GlobalExceptionHandler.java # the one REST ExceptionMapper
76
+ ├── error/GrpcExceptionInterceptor.java
77
+ │ ├── error/ErrorCatalog.java # code -> HTTP status + reason
78
+ │ ├── error/ErrorDto.java # TMF630 Error wire shape
79
+ ├── error/SqlStateTranslator.java # SQLState -> error code
80
+ ├── i18n/MessageResolver.java # Accept-Language -> localized message
81
+ │ ├── client/CredentialsValidator.java # outbound gRPC/REST integrations
82
+ ├── messaging/OutboxRelayJob.java # + OutboxDispatcher, OutboxConfig, OutboxSql
83
+ ├── audit/AuditRecorder.java
84
+ │ └── util/StringUtils.java
85
+ └── create_party_individual/ THE SLICE (snake_case folder)
86
+ ├── dto/
87
+ │ ├── CreatePartyIndividualRequestDto.java
88
+ │ ├── CreatePartyIndividualResponseDto.java
89
+ │ └── ContactMediumDto.java
90
+ ├── CreatePartyIndividualResource.java # REST endpoint → invokes the Handler
91
+ │ # (or PartyIndividualResource shared by sibling slices — see below)
92
+ ├── PartyGrpcService.java # gRPC endpoint → invokes the Handler (conditional)
93
+ ├── PartyCreatedConsumer.java # Kafka consumer → invokes the Handler (conditional)
94
+ ├── CreatePartyIndividualHandler.java # business logic + transaction boundary
95
+ ├── CreatePartyIndividualSql.java # SQL constants + JDBC access methods
96
+ └── README.md # slice documentation
97
+
98
+ src/main/proto/customer/v1/
99
+ └── party.proto # published to contracts/ (see grpc skill)
100
+
101
+ src/main/resources/messages/
102
+ ├── errors.properties # default locale (en), keyed by error code
103
+ ├── errors_es.properties
104
+ └── errors_pt.properties
105
+
106
+ src/test/java/com/<company>/<module>/create_party_individual/
107
+ └── CreatePartyIndividualHandlerTest.java # mirrors the slice
68
108
  ```
69
109
 
70
- Rules of dependency: `infrastructure application domain`. Never the reverse. DTOs never leak into application/domain; map at the adapter boundary.
110
+ Minimum viable slice = **4 files**: a DTO pair in `dto/`, `Handler`, `Sql`, one transport class. gRPC and Kafka classes are generated only when the slice plan marks `needs_grpc` / `needs_kafka`. A slice that only reads still has a `Handler` — it is the single entry point, even when `validate()` is a no-op.
71
111
 
72
- ## Class naming conventions (canonical)
112
+ ### Slice naming variables
73
113
 
74
- These names are the standard for ALL services and all sibling skills. A class name must reveal its layer and role without opening the file. Identifiers, packages, SQL objects and comments are **English only** — Spanish appears only in i18n message bundles and user-facing text (see quarkus-error-handling-i18n skill).
114
+ Every generator and every review uses these three names consistently:
75
115
 
76
- ### Domain layer (`domain`)
77
-
78
- | Artifact | Convention | Example |
116
+ | Variable | Meaning | Example |
79
117
  |---|---|---|
80
- | Entity / aggregate root | singular noun, no suffix | `User`, `Subscription` |
81
- | Value object | singular noun record, no suffix | `Email`, `Money`, `Msisdn` |
82
- | Typed identifier | `<Entity>Id` record | `UserId`, `SubscriptionId` |
83
- | State enum | `<Entity>Status` | `UserStatus` |
84
- | Domain event | past-tense verb phrase, **no** suffix | `UserRegistered`, `SubscriptionActivated` |
85
- | Domain service / policy | `<Concept>Policy`, `<Concept>Calculator` | `PasswordPolicy`, `PricingCalculator` |
86
- | Domain exception | `<Thing><Problem>Exception` | `UserNotFoundException`, `DuplicateEmailException` |
87
- | Exception base | `DomainException` | |
118
+ | `SLICE_FOLDER` | folder name, `snake_case`, verb-first | `create_party_individual` |
119
+ | `SLICE_PACKAGE` | `<PACKAGE_BASE>.<SLICE_FOLDER>` | `com.alva.customer.create_party_individual` |
120
+ | `SERVICE_CLASS_PREFIX` | `PascalCase` of the slice; prefix of `Handler`, `Sql`, `Resource`, DTOs | `CreatePartyIndividual` |
88
121
 
89
- `*Service` is **never** used in the domain layer — a domain service is a policy, a calculator or a factory, and saying so beats a generic suffix.
122
+ Slice names are **verb + resource** (`create_party_individual`, `find_party_by_id`, `update_party_status`)they name a capability, not an entity. A slice that would need a name like `party_management` is really several slices.
90
123
 
91
- ### Application layer (`application`)
124
+ ## Class naming conventions (canonical)
125
+
126
+ Identifiers, packages, SQL objects and comments are **English only** — localized text lives exclusively in message bundles (see quarkus-error-handling-i18n skill).
92
127
 
93
128
  | Artifact | Convention | Example |
94
129
  |---|---|---|
95
- | Inbound port (`port/in`) | `<Verb><Entity>UseCase` — always an interface | `RegisterUserUseCase`, `GetUserUseCase` |
96
- | Use case implementation (`usecase`) | `<Verb><Entity>Service` | `RegisterUserService` |
97
- | Command (write input) | `<Verb><Entity>Command` | `RegisterUserCommand` |
98
- | Query (read input) | `<Verb><Entity>Query` | `FindUsersQuery` |
99
- | Use case output | `<Verb><Entity>Result` | `RegisterUserResult` |
100
- | Outbound port persistence | `<Entity>Repository` | `UserRepository` |
101
- | Outbound port — other capability | capability noun, **no technology** | `EventPublisher`, `CredentialsValidator`, `AuditRecorder` |
130
+ | Business logic core | `${SERVICE_CLASS_PREFIX}Handler` | `CreatePartyIndividualHandler` |
131
+ | Data access | `${SERVICE_CLASS_PREFIX}Sql` | `CreatePartyIndividualSql` |
132
+ | REST adapter | `<Resource>Resource` — the TMF resource name, per openapi skill | `PartyIndividualResource` |
133
+ | gRPC adapter | `<ProtoService>GrpcService` — mirrors the proto service | `PartyGrpcService` |
134
+ | Kafka consumer | `<Event>Consumer` | `PartyCreatedConsumer` |
135
+ | Scheduled entry point | `<Task>Job` | `OutboxRelayJob` |
136
+ | Request DTO | `${SERVICE_CLASS_PREFIX}RequestDto` | `CreatePartyIndividualRequestDto` |
137
+ | Response DTO | `${SERVICE_CLASS_PREFIX}ResponseDto` | `CreatePartyIndividualResponseDto` |
138
+ | TMF create/update DTO | `<Resource>CreateDto` / `<Resource>UpdateDto` when implementing a TMF `X_Create`/`X_Update` | `PartyIndividualCreateDto` |
139
+ | Filter/criteria DTO | `Filter<Resource>Dto` | `FilterPartyDto` |
140
+ | Nested/shared-shape DTO | `<Resource>Dto` | `PartyDto`, `ContactMediumDto` |
141
+ | Kafka payload DTO | `<Event>Payload` | `PartyCreatedPayload` |
142
+ | Outbound integration bean | capability noun, **no technology** | `CredentialsValidator`, `AuditRecorder` |
143
+ | Config mapping | `<Area>Config` (`@ConfigMapping` interface) | `OutboxConfig` |
144
+ | Business exception | `BusinessException` (single, code-carrying, in `common/exception`) | — |
145
+ | Technical exceptions | `PersistenceException`, `TransientPersistenceException`, `PersistenceTimeoutException` | — |
146
+ | Concurrency-conflict exception | `StaleVersionException` (standalone, code-carrying, in `common/exception` — → 409 / `ABORTED` via its own mappers) | — |
147
+ | Global handlers | `GlobalExceptionHandler`, `GrpcExceptionInterceptor` | — |
148
+ | Test class | `<Class>Test` (unit), `<Class>IT` (`@QuarkusIntegrationTest`), `ArchitectureTest` | `CreatePartyIndividualHandlerTest` |
102
149
 
103
- One use case per class, one public method named after the verb (`register`, `getById`). The `*Service` suffix is **reserved** for use-case implementations in `application/usecase` (plus proto-generated gRPC stubs); it never appears on repositories, mappers or helper classes.
150
+ `Handler` and `Sql` carry **no** transport or technology prefix the slice folder already says which feature they belong to, and there is exactly one of each per slice. The three inbound adapters keep their **transport-idiomatic** names (`Resource`, `GrpcService`, `Consumer`) because those names are contracts with JAX-RS, protobuf and the topic catalog respectively, not ours to rename.
104
151
 
105
- ### Infrastructure layer (`infrastructure`)
152
+ ### Two slices, one TMF resource path
106
153
 
107
- **Inbound (driving) adapters suffixed by transport:**
154
+ Several slices may expose operations under the same TMF base path (`create_party_individual` → `POST`, `find_party_by_id` → `GET /{id}`). Two options, pick one per app and stay consistent:
108
155
 
109
- | Adapter | Convention | Example |
110
- |---|---|---|
111
- | REST resource | `<Resource>Resource` | `UserResource`, `DigitalIdentityResource` |
112
- | gRPC endpoint | `<ProtoService>GrpcService` | `UserGrpcService` |
113
- | Kafka consumer | `<Event>Consumer` | `UserRegisteredConsumer` |
114
- | Scheduled job | `<Task>Job` | `OutboxRelayJob` |
156
+ - **One `Resource` class per TMF resource** (default, safest): the class lives in the slice folder of its *primary* capability and injects the `Handler` of each slice it fronts. Keeps JAX-RS path resolution unambiguous and produces one clean OpenAPI tag.
157
+ - **One `Resource` class per slice** sharing a base `@Path`: legal in Quarkus REST as long as no two classes declare the same method + sub-path combination, but the spec does not guarantee it across implementations. If you choose this, the `/q/openapi` snapshot test (openapi skill) is what protects you — it fails loudly on a collision.
115
158
 
116
- **Outbound (driven) adapters prefixed by technology, keeping the port name:**
159
+ ### Banned and reserved names
160
+
161
+ - Suffixes `*Impl`, `*Util`, `*Utils`, `*Helper`, `*Manager`, `*Repository`, `*UseCase`, `*Mapper`, `*Dao`. If a class needs `Util`, it needs a real responsibility name; the sanctioned exceptions are `common/util/StringUtils` and the `Jdbc` namespace helper from the sql skill.
162
+ - `*Service` **except** `*GrpcService` and proto-generated stubs. A class named `PartyService` is business logic that belongs in a `Handler`.
163
+ - `*DTO` / `*VO` / `*Bean` / `*POJO` in all-caps or as decoration — the standard is `*Dto`.
164
+ - Names that shadow JDK or Jakarta types: never declare `Response`, `Entity`, `Optional`, `Record`, or `ConcurrentModificationException` (use `StaleVersionException`). An accidental import silently changes behaviour.
165
+ - Exceptions named after HTTP statuses (`NotFoundException`, `BadRequestException`) — status comes from `ErrorCatalog`, not from the class name.
166
+ - Nested `record` / `static class` redeclaring a DTO that already exists in `dto/` — always import the real class.
167
+ - Abbreviations that are not org-wide vocabulary (`UsrRepo`, `SubsSvc`). Semantic module names (`iam`, `tenant`) are fine in packages and config, not in class names; inventory codes (`bc05`) appear nowhere in code.
168
+
169
+ ### Other identifiers (for reference)
117
170
 
118
- | Adapter | Convention | Example |
171
+ | Kind | Convention | Example |
119
172
  |---|---|---|
120
- | JDBC repository | `Jdbc<Port>` | `JdbcUserRepository` |
121
- | Outbox / Kafka publisher | `Outbox<Port>`, `Kafka<Port>` | `OutboxEventPublisher` |
122
- | gRPC client adapter | `Grpc<Port>` | `GrpcCredentialsValidator` |
123
- | REST client adapter | `Rest<Port>` | `RestPaymentGateway` |
173
+ | Deployable app | `{module}-{service}-{type}` kebab-case, tenant-agnostic (`-ms` backend) | `wallet-backend-core-ms` |
174
+ | Slice package | lowercase `snake_case`, verb-first | `com.alva.customer.create_party_individual` |
175
+ | SQL constant | `UPPER_SNAKE_CASE` matching the method | `INSERT_PARTY`, `SELECT_PARTY_BY_ID` |
176
+ | SQL table / column | `snake_case`, singular table, schema-qualified | `customer.party`, `created_at` |
177
+ | Proto | `PascalCase` messages/services, `snake_case` fields, package `vN` | `GetPartyRequest`, `party_id` |
178
+ | Kafka topic / channel | see quarkus-kafka-messaging skill | `alva.customer.party.created.v1` |
179
+ | Error code (= bundle key) | `<MOD>-<HTTP>-<seq>` (`<MOD>` = uppercase code derived from the semantic module name or its main entity, never `bcNN`) | `PTY-400-001` |
180
+ | Span name | `usecase.<sliceCamelCase>` | `usecase.createPartyIndividual` |
124
181
 
125
- The port says *what*, the prefix says *with what* so a second implementation (`InMemoryUserRepository` in tests) reads naturally.
182
+ ## The Handlerlogic and transaction contract
126
183
 
127
- **Supporting classes:**
184
+ `${SERVICE_CLASS_PREFIX}Handler` centralizes **100%** of the slice's business logic. This section is the contract every Handler must satisfy; generators and reviewers check it line by line.
128
185
 
129
- | Artifact | Convention | Example |
130
- |---|---|---|
131
- | REST DTO | `<Resource>Dto`, `<Resource>CreateDto`, `<Resource>UpdateDto` | `DigitalIdentityCreateDto` |
132
- | Kafka payload DTO | `<Event>Payload` | `UserRegisteredPayload` |
133
- | Mapper | `<Entity><Transport>Mapper` | `UserRestMapper`, `UserGrpcMapper`, `UserEventMapper` |
134
- | Row mapper | `<Entity>RowMapper` | `UserRowMapper` |
135
- | Config mapping | `<Area>Config` (`@ConfigMapping` interface) | `OutboxConfig` |
136
- | Global handlers | `GlobalExceptionHandler`, `GrpcExceptionInterceptor` | |
137
- | Technical exceptions | `PersistenceException`, `TransientPersistenceException`, `PersistenceTimeoutException`, `StaleVersionException` | |
186
+ ### Mandatory method order
138
187
 
139
- The DTO ↔ domain distinction matters: `UserDto` lives in `infrastructure/rest/dto` and is what the wire sees; `User` is the domain entity. They are never the same class, even when the fields match today.
188
+ ```
189
+ 1. package
190
+ 2. imports
191
+ 3. @ApplicationScoped
192
+ 4. @Inject DataSource dataSource;
193
+ 5. @Inject ${SERVICE_CLASS_PREFIX}Sql sql;
194
+ 6. (optional) @Inject <integration beans from common/client, OutboxDispatcher from common/messaging>
195
+ 7. private void validate(RequestDto request)
196
+ 8. private <T> execution(RequestDto request) // T = internal result (UUID, DTO, List<Dto>…)
197
+ 9. private ResponseDto getResult(<T> internalResult)
198
+ 10. @Transactional public ResponseDto process(RequestDto request)
199
+ ```
140
200
 
141
- ### Banned and reserved names
201
+ Field `@Inject` is the standard **inside a slice** — it keeps the four-file slice readable and matches the generator templates. (Constructor injection remains the rule for `common/` and `libs/` beans, which are unit-tested outside CDI.)
142
202
 
143
- - Suffixes `*Impl`, `*Util`, `*Utils`, `*Helper`, `*Manager`, `*Handler` (the single `GlobalExceptionHandler` is the only sanctioned `*Handler`). If a class needs `Util`, it needs a real responsibility name.
144
- - `*DTO` / `*VO` / `*Bean` / `*POJO` in all-caps or as decoration — the standard is `*Dto`.
145
- - Names that shadow JDK or Jakarta types: never `ConcurrentModificationException` (use `StaleVersionException`), `Entity`, `Optional`, `Record`. An accidental `java.util` import silently changes behaviour.
146
- - Abbreviations that are not org-wide vocabulary (`UsrRepo`, `SubsSvc`). Module/BC codes (`bc05`) are fine in packages and config, not in class names.
203
+ ### `validate()` input validation
147
204
 
148
- ### Other identifiers (for reference)
205
+ Bean Validation on the DTO (`@NotBlank`, `@Size`, `@Pattern`) covers *form*. `validate()` covers *business rules*: mandatory-if, cross-field consistency, allowed state transitions, tenant coherence. It throws `BusinessException` and never returns a boolean or an error list.
149
206
 
150
- | Kind | Convention | Example |
151
- |---|---|---|
152
- | Deployable app | `{module}-{service}-{type}` kebab-case, tenant-agnostic (`-ms` backend) | `wallet-backend-core-ms` |
153
- | Package | lowercase, singular, no underscores | `com.alva.bc05.application.usecase` |
154
- | Constant | `UPPER_SNAKE_CASE` | `FIND_BY_ID` |
155
- | SQL table / column | `snake_case`, singular table | `app_user`, `created_at` |
156
- | Test class | `<Class>Test` (unit/`@QuarkusTest`), `<Class>IT` (`@QuarkusIntegrationTest`), `ArchitectureTest` | `JdbcUserRepositoryTest` |
157
- | Proto | `PascalCase` messages/services, `snake_case` fields | `GetUserRequest`, `user_id` |
158
- | Kafka topic / channel | see quarkus-kafka-messaging skill | `alva.bc05.user.registered.v1` |
159
- | Error code | `<MOD>-<HTTP>-<seq>` (`<MOD>` = short module/domain code) | `USR-404-001` |
207
+ ```java
208
+ private void validate(CreatePartyIndividualRequestDto request) {
209
+ if (StringUtils.isBlank(request.getGivenName())) {
210
+ throw new BusinessException("PTY-400-001", "givenName");
211
+ }
212
+ }
213
+ ```
214
+
215
+ **The Handler never formats or translates a message.** It throws a stable error code plus the interpolation arguments; `GlobalExceptionHandler` + `MessageResolver` resolve the localized text from `Accept-Language` at the edge (see quarkus-error-handling-i18n skill). A Handler that injects a `ResourceBundle` and passes a rendered string is wrong — it silently pins every response to one language.
216
+
217
+ ### `execution()` — orchestration inside the container transaction
218
+
219
+ - Opens **one** `Connection` from the injected `DataSource` in try-with-resources and passes it to every `Sql` call in the operation. This is the performance contract: one connection per business operation, whether or not a transaction is active. Under JTA, Agroal would return the same enlisted connection anyway; on a read path with no `@Transactional`, per-method `getConnection()` would acquire *N* separate pool leases for *N* queries — passing `conn` makes both paths cost exactly one.
220
+ - Calls `${SERVICE_CLASS_PREFIX}Sql` methods in business order (insert `party` first to get `partyId`, reuse it for dependent inserts). Before writing a call, re-read the real `Sql` method signature and pass exactly those parameters, in that order and type.
221
+ - Catches every checked `SQLException` and rethrows it as `BusinessException` via `SqlStateTranslator` — an unchecked exception is what triggers the container rollback, and a propagated checked exception would not.
222
+ - Date fields arrive from the DTO as `String`. To use them as `LocalDate`/`ZonedDateTime`, declare a **new local variable** and convert explicitly (`LocalDate.parse(...)`) — never reassign the DTO field.
223
+ - Domain events are published by inserting an outbox row through this slice's own `Sql`, in this same transaction — never by emitting to Kafka directly (see quarkus-kafka-messaging skill). Who relays that row (Debezium, `OutboxRelayJob`, or a post-commit emit) is a runtime property, `app.messaging.outbox.relay-mode`, and changes nothing here.
224
+ - External services are called through capability-named beans injected from `common/client` — never a raw `@GrpcClient` stub, which would drag Mutiny into the Handler.
225
+
226
+ ```java
227
+ private UUID execution(CreatePartyIndividualRequestDto request) {
228
+ try (Connection conn = dataSource.getConnection()) {
229
+ UUID partyId = sql.insertParty(conn, request.getTenantId(), "Individual",
230
+ "Active", request.getCreatedBy());
231
+ sql.insertIndividual(conn, partyId, request.getGivenName(), request.getFamilyName());
232
+ if (request.getContactMedia() != null) {
233
+ for (ContactMediumDto medium : request.getContactMedia()) {
234
+ sql.insertContactMedium(conn, partyId, medium.getMediumType(), medium.getContactValue());
235
+ }
236
+ }
237
+ UUID eventId = sql.insertOutboxEvent(conn, partyId, "party",
238
+ "com.alva.customer.party.created.v1", "alva.customer.party.created.v1",
239
+ payloadJson(partyId, request), traceparent()); // traceparent captured from Span.current() (see kafka/observability skills)
240
+ dispatcher.dispatchAfterCommit(eventId); // no-op unless relay-mode=direct (see kafka skill)
241
+ return partyId;
242
+ } catch (SQLException e) {
243
+ throw SqlStateTranslator.translate("PTY-500-001", e);
244
+ }
245
+ }
246
+ ```
247
+
248
+ ### `getResult()` — response mapping
249
+
250
+ Builds the response DTO from what `execution()` returned. It touches neither `Sql` nor the `Connection` — pure mapping. This is where the (deleted) `*Mapper` class went.
251
+
252
+ ```java
253
+ private CreatePartyIndividualResponseDto getResult(UUID partyId) {
254
+ return CreatePartyIndividualResponseDto.builder()
255
+ .partyId(partyId.toString())
256
+ .build();
257
+ }
258
+ ```
259
+
260
+ ### `process()` — the single entry point
261
+
262
+ ```java
263
+ @Transactional
264
+ @WithSpan("usecase.createPartyIndividual")
265
+ public CreatePartyIndividualResponseDto process(CreatePartyIndividualRequestDto request) {
266
+ validate(request);
267
+ UUID partyId = execution(request);
268
+ return getResult(partyId);
269
+ }
270
+ ```
271
+
272
+ Every transport adapter calls exactly this method. It is the only public method of the Handler, the only place `@Transactional` (`jakarta.transaction.Transactional`) appears, and the natural custom-span boundary (see quarkus-observability-otel skill).
273
+
274
+ ### Transaction rules — no exceptions
275
+
276
+ The container (Quarkus + Narayana JTA) owns the transaction: it **commits** when `process()` returns normally and **rolls back** on any propagated `RuntimeException`, `BusinessException` included.
277
+
278
+ - **`conn.setAutoCommit(...)`, `conn.commit()`, `conn.rollback()` are forbidden anywhere in a slice.** There is no valid scenario for them here — not "for safety", not "to make sure it saved", not as generic JDBC good practice. The line right after `try (Connection conn = dataSource.getConnection())` is never `conn.setAutoCommit(false)`.
279
+ - Closing the `Connection` (try-with-resources) returns it to the pool; it does **not** end the JTA transaction.
280
+ - `@Transactional` never goes on `validate()`, `execution()`, `getResult()` or any private method: CDI interceptors do not fire on self-invocation, so the transaction would silently never open.
281
+ - **Read-only slices skip `@Transactional` entirely** — a single `SELECT`, or several reads that tolerate a non-repeatable view, cost less without a JTA transaction. Use `@Transactional` the moment there is a write, or when several reads must see one consistent snapshot.
282
+ - Need a different propagation for one step? Extract it into a separate `@ApplicationScoped` bean in `common/` with its own `@Transactional(REQUIRES_NEW)` public method and inject it — never annotate a private method. `QuarkusTransaction.requiringNew()` is the programmatic alternative for batch loops (see sql skill).
283
+
284
+ **Pre-delivery checklist for every Handler:**
285
+
286
+ - [ ] `setAutoCommit` does not appear in the file.
287
+ - [ ] `.commit()` does not appear in the file.
288
+ - [ ] `.rollback()` does not appear in the file.
289
+ - [ ] `@Transactional` appears at most once, on `process()` — and is absent on a read-only slice.
290
+ - [ ] Exactly one `dataSource.getConnection()`, passed to every `Sql` call.
291
+ - [ ] Every `Sql` call matches the real signature (name, order, arity, types).
292
+ - [ ] `BusinessException` is thrown with a code + args, never with a pre-rendered message.
293
+ - [ ] No `Uni`/`Multi`/`PgPool`, no `@GrpcClient` stub, no transport type, no DTO redeclared as a nested class.
294
+
295
+ ### When to add an interface back
296
+
297
+ Reintroduce a port interface (and only then) when one of these is true, and say so in the slice README:
298
+
299
+ - A **second real implementation** exists in production code (two data sources, a stub for a partner API, a feature-flagged alternative).
300
+ - The implementation lives in another deployable and is injected across a module boundary.
301
+ - The consumer is `common/`/`libs/` code that must not depend on a slice.
302
+
303
+ "Mocking is easier" is not a reason — Mockito mocks the concrete `Sql` class fine.
304
+
305
+ ## The Sql class — data adapter
306
+
307
+ Full standard (pool config, batches, jsonb, pagination, keyset paging, SQLState translation) lives in the **quarkus-sql-jdbc-agroal** skill. Slice-specific shape:
308
+
309
+ - `@ApplicationScoped`, `@Inject DataSource dataSource;`, then `private static final String` query constants, then one public method per query.
310
+ - Constants are text blocks or comma-first concatenations, schema-qualified, with positional `?` placeholders only. **Check the real schema name before writing a query** — if the schema or table is a reserved word (`order`, `user`, `group`), quote it: `"order".party`.
311
+ - If the PK has a database default (`id uuid DEFAULT customer.uuidv7() NOT NULL`), the `id` column is excluded from the INSERT column list and never passed as a parameter. Prefer `INSERT ... RETURNING id` over `getGeneratedKeys()`.
312
+ - **Every method's first parameter is `Connection conn`** — supplied by the Handler, never obtained inside the method. That signature is the contract the Handler must honour exactly.
313
+ - Methods `throws SQLException` — they never swallow it and never translate it. Translation to `BusinessException` happens in the Handler via `SqlStateTranslator`.
314
+ - **No transaction control here.** No `commit`, no `rollback`, no `setAutoCommit`.
315
+ - Row mapping is hand-written into DTOs (`PartyDto.builder()...`) — no reflection-based mappers, which also keeps native images small.
316
+
317
+ ## DTOs
318
+
319
+ - Live in `${SLICE_PACKAGE}.dto`, one file per DTO **including nested ones** — never a `static class` or local `record`.
320
+ - Lombok: `@Data`, `@NoArgsConstructor`, `@AllArgsConstructor` on every serializable DTO; `@Builder` on responses and on anything the Handler builds in parts.
321
+ - **Date fields are always `String`** in a DTO (request and response), ISO-8601. Conversion happens exclusively in the Handler.
322
+ - Form validation only (`jakarta.validation.constraints.*`); business rules belong in `validate()`.
323
+ - OpenAPI documentation with `@Schema` on every contract-relevant field. When implementing a TMF spec, wire names are not negotiable — the Java class gets the `Dto` suffix, the serialized schema keeps the TMF name (`@Schema(name = "PartyIndividual")`). See quarkus-openapi-tmforum skill.
324
+ - `@RegisterForReflection` on anything Jackson touches that Quarkus does not process at build time — Kafka payloads especially.
325
+ - No business logic inside a DTO.
326
+
327
+ ## Transport adapters
328
+
329
+ All three are thin: extract metadata, invoke `handler.process(...)`, format the response. Any `if` that encodes a business rule is in the wrong file. None of them catches and formats errors — they let `BusinessException` propagate to the global handler/interceptor.
330
+
331
+ **REST** — full standard in **quarkus-openapi-tmforum** and **quarkus-error-handling-i18n**:
332
+
333
+ - `<Resource>Resource` class with `@Path` + `@Tag`, injecting the slice `Handler`(s).
334
+ - Returns `RestResponse<T>` synchronously. Never `Uni`/`Multi`.
335
+ - `@Blocking` on JDBC-backed methods. With a plain (non-reactive) return type Quarkus REST already dispatches to a worker thread, so the annotation is redundant *today* — keep it as an explicit threading contract that survives a later signature change. `@RunOnVirtualThread` is the alternative for high-concurrency blocking endpoints (see sql skill §7).
336
+ - Extracts `tenantId`, `partyId`, `partyRolList`, `language` from headers when the contract requires them, and sets them on the request DTO before calling `process()`.
337
+ - Full OpenAPI annotations per method: `@Operation` with `operationId`, one `@APIResponse` per relevant status, `@Parameter` for headers and path/query params, `Location` header on create.
338
+
339
+ **gRPC** — full standard in **quarkus-grpc-services**:
340
+
341
+ - Proto in `src/main/proto/<module>/v<major>/`, published to `contracts/`, package `<org>.<module>.v1`, `snake_case` fields.
342
+ - `@GrpcService` class named after the proto service, injecting the slice `Handler`.
343
+ - One of the two sanctioned Mutiny sites: `Uni.createFrom().item(() -> {...}).runSubscriptionOn(Infrastructure.getDefaultWorkerPool())`, with proto ↔ DTO conversion inside the lambda.
344
+
345
+ **Kafka** — full standard in **quarkus-kafka-messaging**:
346
+
347
+ - `<Event>Consumer` class, `@ApplicationScoped`, injecting the slice `Handler`.
348
+ - `@Incoming` methods are `@Blocking` (JDBC below). Idempotency check + `process()` + DLQ strategy per the kafka skill.
349
+ - Producing domain events is **not** done here — the Handler inserts an outbox row in its transaction and the relay selected by `app.messaging.outbox.relay-mode` (Debezium / `OutboxRelayJob` / `OutboxDispatcher`) publishes it.
350
+
351
+ ## Errors and i18n
352
+
353
+ Governed by **quarkus-error-handling-i18n**; the slice-relevant rules:
354
+
355
+ - One exception type for business failures: `BusinessException(String code, Object... args)` in `common/exception`, carrying a stable `<MOD>-<HTTP>-<seq>` code and the interpolation arguments. No subclass-per-error hierarchy.
356
+ - Technical failures keep the `PersistenceException` family; `SqlStateTranslator` maps SQLState → error code (`23505` → conflict, `40001` → transient, `57014` → timeout).
357
+ - Optimistic-lock conflicts throw `StaleVersionException` — standalone, unchecked, code-carrying (not a `PersistenceException` subtype, not SQLState-derived): the `Handler` throws it when a versioned `UPDATE` returns 0 rows; dedicated mappers return 409 (REST) / `ABORTED` (gRPC).
358
+ - `ErrorCatalog` maps code → HTTP status + reason. `MessageResolver` resolves the localized message from `Accept-Language` against `messages/errors[_<lang>].properties`, **keyed by the error code itself**, default locale `en`.
359
+ - **Message resolution happens at the edge, never in the Handler.** `GlobalExceptionHandler` (REST) and `GrpcExceptionInterceptor` (gRPC) are the only places a message string is produced.
360
+ - Native: `quarkus.locales=en,es,pt` and `quarkus.native.resources.includes=messages/*.properties`.
361
+
362
+ Adding an error is four steps: pick the next code, register it in `ErrorCatalog`, add the key to **every** locale bundle, throw it from `validate()`/`execution()`.
363
+
364
+ ## Observability
365
+
366
+ Governed by **quarkus-observability-otel**; the slice-relevant rules:
367
+
368
+ - `@WithSpan("usecase.<sliceCamelCase>")` on `process()` — one span per business operation, matching the skill's `usecase.<verb><Entity>` convention since the slice name *is* the verb+entity.
369
+ - Logger per class via `Log`/`Logger.getLogger(X.class)`; the Handler's class name already carries the slice. Never `System.out`.
370
+ - Trace ids come only from `Span.current().getSpanContext()` — never hand-built strings. Outbox rows carry `traceparent` + a `traceContext` block in the payload.
371
+ - Audit writes go through the `AuditRecorder` bean in `common/audit`, which captures the span itself.
372
+ - No PII or secrets in spans, attributes or logs.
373
+
374
+ ## Shared code
375
+
376
+ `common/` holds only what **two or more slices** genuinely share, or what is structurally cross-cutting: the exception/error/i18n machinery, integration beans, `OutboxRelayJob`/`OutboxDispatcher`, `AuditRecorder`, `@ConfigMapping` interfaces, `StringUtils`.
377
+
378
+ Promotion rule: **duplicate twice, extract on the third.** Two slices with similar-looking code is the expected cost of slice independence — premature extraction rebuilds the shared-layer coupling this structure exists to avoid. What must never be shared: a `Handler`, an `Sql`, or a slice's `dto` package. If slice B needs slice A's data, it queries it through its own `Sql` method or consumes A's event — it does not import A.
379
+
380
+ Code shared across **apps** in the domain goes to `libs/` and uses constructor injection.
160
381
 
161
382
  ## Scaffolding checklist
162
383
 
163
384
  When creating a new service:
164
385
 
165
386
  1. App folder `apps/<module>-<service>-ms/` per the monorepo layout above.
166
- 2. `pom.xml` with BOM `io.quarkus.platform:quarkus-bom` (min version 3.38.2), extensions: `quarkus-rest`, `quarkus-rest-jackson`, `quarkus-agroal`, `quarkus-jdbc-postgresql`, `quarkus-arc`, `quarkus-smallrye-health`, `quarkus-flyway` (schema migrations), plus skills-specific extensions as needed.
167
- 3. `application.properties` baseline (below).
168
- 4. ArchUnit test (below) — add it in the same commit as the first class, not later.
169
- 5. Native profile using Mandrel container build.
170
- 6. `Dockerfile` at the app root, based on `quay.io/quarkus/quarkus-micro-image` (or distroless) running the native binary.
171
- 7. `service.yaml` + per-app `README.md` (below)in the same PR as the first endpoint, not later; CI blocks merges without them.
387
+ 2. `pom.xml` with BOM `io.quarkus.platform:quarkus-bom` (latest 3.x LTS), extensions: `quarkus-rest`, `quarkus-rest-jackson`, `quarkus-agroal`, `quarkus-jdbc-postgresql`, `quarkus-arc`, `quarkus-narayana-jta`, `quarkus-hibernate-validator`, `quarkus-smallrye-health`, `quarkus-smallrye-openapi`, `quarkus-opentelemetry`, `quarkus-flyway` (kept even where migrations are disabled — see sql skill §9), plus Lombok and skill-specific extensions as needed.
388
+ 3. `common/` package: `BusinessException`, `ErrorCatalog`, `ErrorDto`, `GlobalExceptionHandler`, `MessageResolver`, `SqlStateTranslator`, `StringUtils`.
389
+ 4. `application.properties` baseline (below).
390
+ 5. ArchUnit test (below) in the same commit as the first slice, not later.
391
+ 6. Native profile using the Mandrel container build.
392
+ 7. `Dockerfile` at the app root — **always a native-image Dockerfile** (see "Dockerfile (native image mandatory)" below). A JVM image (`quarkus-app/` + JRE base) is never the delivery artifact.
393
+ 8. `service.yaml` + per-app `README.md` (below) — in the same PR as the first endpoint; CI blocks merges without them.
394
+
395
+ When creating a new slice, in this order (each step reads the previous file's real signatures):
396
+
397
+ 1. `dto/` — request, response, nested DTOs.
398
+ 2. `<Slice>Sql.java` — query constants + one method per query, `Connection` first.
399
+ 3. `<Slice>Handler.java` — `validate` → `execution` → `getResult` → `process`.
400
+ 4. Transport class(es): `Resource` always; `GrpcService`/`Consumer` only if the plan requires them (+ the `.proto` / channel config).
401
+ 5. New error codes registered in `ErrorCatalog` and added to **every** locale bundle.
402
+ 6. `<Slice>HandlerTest.java`.
403
+ 7. Slice `README.md`.
172
404
 
173
405
  ### application.properties baseline
174
406
 
175
407
  ```properties
176
408
  quarkus.datasource.db-kind=postgresql
177
409
  quarkus.datasource.jdbc.max-size=16
178
- quarkus.flyway.migrate-at-start=true
410
+
411
+ # Schema migrations are OPT-IN and off by default — the app does not own the schema
412
+ # in a DBA/pipeline-managed environment (see quarkus-sql-jdbc-agroal skill §9)
413
+ quarkus.flyway.enabled=false
414
+ quarkus.flyway.migrate-at-start=false
415
+ %dev.quarkus.flyway.enabled=true
416
+ %dev.quarkus.flyway.migrate-at-start=true
417
+ %test.quarkus.flyway.enabled=true
418
+ %test.quarkus.flyway.migrate-at-start=true
419
+
420
+ # Outbox relay: who publishes the outbox row (see quarkus-kafka-messaging skill)
421
+ app.messaging.outbox.relay-mode=debezium
422
+ app.messaging.outbox.poller.every=off
423
+
424
+ # i18n (see error-handling skill) — locales must be declared for native
425
+ quarkus.locales=en,es
426
+ quarkus.default-locale=en
427
+ quarkus.native.resources.includes=messages/*.properties
179
428
 
180
429
  # Native build via Mandrel container (no local GraalVM needed)
181
430
  quarkus.native.container-build=true
@@ -199,6 +448,25 @@ quarkus.http.port=8080
199
448
 
200
449
  Build: `./mvnw package -Dnative` → binary in `target/*-runner`.
201
450
 
451
+ ### Dockerfile (native image — mandatory)
452
+
453
+ The `Dockerfile` at the app root always packages the **native binary** — never a JVM/`fast-jar` image. The binary is produced by the Mandrel container build (`./mvnw package -Dnative` with `quarkus.native.container-build=true`, so no local GraalVM is needed), and the image just runs it:
454
+
455
+ ```dockerfile
456
+ FROM quay.io/quarkus/quarkus-micro-image:2.0
457
+ WORKDIR /work/
458
+ COPY --chown=1001:root target/*-runner /work/application
459
+ EXPOSE 8080
460
+ USER 1001
461
+ ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]
462
+ ```
463
+
464
+ Rules:
465
+ - Base image: `quay.io/quarkus/quarkus-micro-image` (or a distroless equivalent per platform standard) — no JDK/JRE layer in the final image.
466
+ - Non-root user (`USER 1001`), binary copied as `application`, port matching `quarkus.http.port`.
467
+ - If the pipeline cannot run the Maven native build before `docker build`, use a multi-stage Dockerfile whose build stage runs the Mandrel builder image — the final stage is still the micro image with only the binary.
468
+ - A `Dockerfile.jvm` may exist for local debugging only; it is never what CI publishes.
469
+
202
470
  ## Per-app documentation (mandatory, blocking)
203
471
 
204
472
  Every deployable under `apps/<app>/` carries two files, versioned with the code and updated in the **same PR** that changes the service's behavior:
@@ -210,23 +478,37 @@ Required `README.md` sections:
210
478
 
211
479
  1. **Identity** — app name, module (`part-of`), type (`ms`), owning team, repository, criticality, part of core yes/no
212
480
  2. **Purpose & responsibility** — description + out of scope
213
- 3. **Tenancy model** — `shared` | `per-tenant` | `pool`; tenant identification (validated JWT claim, never a free header); data isolation; per-tenant parameters
214
- 4. **APIs exposed** — table: api/version, protocol, route/topic, contract file, auth, visibility
215
- 5. **Dependencies** — table: name, type, criticality (`hard`|`soft`), timeout/retry, behavior if down
216
- 6. **Data & persistence** — engine, database/schema, tenant isolation, migrations (`db/`), cache, events
217
- 7. **Configuration** — table: variable, description, type, required, default, varies-by (`environment`|`client`|`tenant`)
218
- 8. **Secrets** — table: logical name, what it is, source (vault), who provides it, rotation period — **never the value**
219
- 9. **Runtime & resources** — runtime/version, ports, requests/limits, replicas/HPA
220
- 10. **Health checks** — liveness; readiness = **hard dependencies only**; startup probe
221
- 11. **Deployment dependencies** — classified: startup / readiness-critical / hard runtime / soft runtime
222
- 12. **Observability** — configurable log levels (`TRACE`–`FATAL`), the ConfigMap variable holding `logging_level`, its default, JSON log format
223
- 13. **GitOps deployment** — location in the config repo, delivery strategy, feature flags
224
- 14. **Rollback & recovery** — procedure, reversible migrations (N/N-1 compatible), RTO/RPO
225
- 15. **Post-deploy validation** — smoke tests, contract tests, tenant-isolation tests
226
- 16. **Troubleshooting** — symptom cause diagnosis
481
+ 3. **Slice inventory** — table: slice folder, capability, exposed channels (REST/gRPC/Kafka), link to the slice README
482
+ 4. **Tenancy model** — `shared` | `per-tenant` | `pool`; tenant identification (validated JWT claim, never a free header); data isolation; per-tenant parameters
483
+ 5. **APIs exposed** — table: api/version, protocol, route/topic, contract file, auth, visibility
484
+ 6. **Dependencies** — table: name, type, criticality (`hard`|`soft`), timeout/retry, behavior if down
485
+ 7. **Data & persistence** — engine, database/schema, tenant isolation, migrations (`db/`), cache, events
486
+ 8. **Configuration** — table: variable, description, type, required, default, varies-by (`environment`|`client`|`tenant`)
487
+ 9. **Secrets** — table: logical name, what it is, source (vault), who provides it, rotation period — **never the value**
488
+ 10. **Runtime & resources** — runtime/version, ports, requests/limits, replicas/HPA
489
+ 11. **Health checks** — liveness; readiness = **hard dependencies only**; startup probe
490
+ 12. **Deployment dependencies** — classified: startup / readiness-critical / hard runtime / soft runtime
491
+ 13. **Observability** — configurable log levels (`TRACE`–`FATAL`), the ConfigMap variable holding `logging_level`, its default, JSON log format
492
+ 14. **GitOps deployment** — location in the config repo, delivery strategy, feature flags
493
+ 15. **Rollback & recovery** — procedure, reversible migrations (N/N-1 compatible), RTO/RPO
494
+ 16. **Post-deploy validation** — smoke tests, contract tests, tenant-isolation tests
495
+ 17. **Troubleshooting** — symptom → cause → diagnosis
227
496
 
228
497
  Rule of completeness: if a variable, secret or dependency is not documented here, it does not exist — CI validates the `service.yaml` schema and the mandatory `README.md` sections as a blocking gate.
229
498
 
499
+ ### Slice README (inside the slice folder)
500
+
501
+ Every slice carries its own `README.md`, generated from the real code — no invented routes, fields or tables. Sections:
502
+
503
+ 1. **Title** — `SERVICE_CLASS_PREFIX`
504
+ 2. **Description** — business purpose and context of the capability
505
+ 3. **Exposed routes** — table: channel (REST/gRPC/Kafka), route or operation, method/topic
506
+ 4. **Input contract** — table: field, type, required, description
507
+ 5. **Output contract** — table: field, type, description
508
+ 6. **Database dependencies** — table: table, use
509
+ 7. **Business errors** — table: error code, HTTP status, when it is raised
510
+ 8. **Usage example** — compact, valid `curl` + JSON response
511
+
230
512
  ### Domain README (repo root)
231
513
 
232
514
  The monorepo root `README.md` is the onboarding doc for the domain, and the owning team keeps it current:
@@ -237,156 +519,163 @@ The monorepo root `README.md` is the onboarding doc for the domain, and the owni
237
519
 
238
520
  It never documents live environment or client configuration — the deployed state per environment/client is managed in the GitOps config repo.
239
521
 
240
- ## Persistence pattern (JDBC + Agroal)
241
-
242
- Full standard (pool config, transactions, batches, jsonb, pagination, SQLException translation) lives in the **quarkus-sql-jdbc-agroal** skill — consult it for any repository work. Summary of the shape:
243
-
244
- Outbound port in `application/port/out`:
245
-
246
- ```java
247
- public interface UserRepository {
248
- Optional<User> findById(UserId id);
249
- void save(User user);
250
- }
251
- ```
252
-
253
- Adapter in `infrastructure/persistence`:
254
-
255
- ```java
256
- @ApplicationScoped
257
- public class JdbcUserRepository implements UserRepository {
258
-
259
- private final AgroalDataSource ds;
260
-
261
- public JdbcUserRepository(AgroalDataSource ds) { this.ds = ds; }
262
-
263
- @Override
264
- public Optional<User> findById(UserId id) {
265
- var sql = "SELECT id, email, status, created_at FROM app_user WHERE id = ?";
266
- try (var con = ds.getConnection(); var ps = con.prepareStatement(sql)) {
267
- ps.setObject(1, id.value());
268
- try (var rs = ps.executeQuery()) {
269
- return rs.next() ? Optional.of(mapRow(rs)) : Optional.empty();
270
- }
271
- } catch (SQLException e) {
272
- throw new PersistenceException("user.find_by_id", e);
273
- }
274
- }
275
- // save(): use explicit transaction control or @Transactional on the use case
276
- }
277
- ```
278
-
279
- Conventions:
280
- - Constructor injection only (no field `@Inject`) — testable and native-friendly.
281
- - SQL as constants or text blocks; never string concatenation with user input.
282
- - Transactions: annotate the **use case** with `@Transactional` (from `quarkus-narayana-jta`) so the unit of work matches the business operation, not the repository call.
283
- - Row mapping by hand or a small private `mapRow(ResultSet)` — no reflection-based mappers.
284
-
285
- ## Domain conventions
286
-
287
- - Value objects = records with validation in a compact constructor.
288
- - Entity identity = typed IDs (`record UserId(UUID value)`), never bare UUID/String in signatures.
289
- - Domain events = sealed interface + records, named in past tense (`UserRegistered`).
290
- - Domain exceptions carry a **stable error code** (used by the error-handling-i18n skill): `throw new UserNotFoundException(id)` where the exception exposes `code() -> "USR-404-001"`.
291
- - Naming per the canonical table above — entities and events carry no suffix, ids are `<Entity>Id`, exceptions are `<Thing><Problem>Exception`.
292
-
293
522
  ## ArchUnit enforcement (mandatory)
294
523
 
295
- `src/test/java/.../ArchitectureTest.java`:
524
+ Conventions that are not enforced decay within two sprints. `src/test/java/.../ArchitectureTest.java` — scope the analysis to your own packages so proto- and Quarkus-generated classes don't trip the rules. Static imports used below: `ArchRuleDefinition.classes/noClasses/methods`, `SlicesRuleDefinition.slices`, `JavaClass.Predicates.resideInAPackage`, `DescribedPredicate.alwaysTrue`.
296
525
 
297
526
  ```java
298
- @AnalyzeClasses(packages = "com.company.bc")
527
+ @AnalyzeClasses(packages = "com.alva.customer", importOptions = ImportOption.DoNotIncludeTests.class)
299
528
  class ArchitectureTest {
300
529
 
530
+ // --- Slice isolation: a slice never imports another slice -------------------
301
531
  @ArchTest
302
- static final ArchRule hexagonal = layeredArchitecture().consideringAllDependencies()
303
- .layer("Domain").definedBy("..domain..")
304
- .layer("Application").definedBy("..application..")
305
- .layer("Infrastructure").definedBy("..infrastructure..")
306
- .whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Infrastructure")
307
- .whereLayer("Application").mayOnlyBeAccessedByLayers("Infrastructure")
308
- .whereLayer("Infrastructure").mayNotBeAccessedByAnyLayer();
532
+ static final ArchRule slicesAreIndependent = slices()
533
+ .matching("com.alva.customer.(*)..")
534
+ .namingSlices("slice $1")
535
+ .should().notDependOnEachOther()
536
+ .ignoreDependency(resideInAPackage("..common.."), alwaysTrue())
537
+ .ignoreDependency(alwaysTrue(), resideInAPackage("..common.."));
538
+
539
+ // --- Hexagon dependency rule inside the slice ------------------------------
540
+ @ArchTest
541
+ static final ArchRule onlyHandlersTouchSql = classes()
542
+ .that().haveSimpleNameEndingWith("Sql")
543
+ .should().onlyBeAccessed().byClassesThat(
544
+ have(simpleNameEndingWith("Handler"))
545
+ // OutboxSql is cross-cutting: its callers are the dispatcher and the relay job,
546
+ // which play the Handler role (they own the Connection) outside any slice.
547
+ .or(resideInAPackage("..common.messaging..")));
309
548
 
310
549
  @ArchTest
311
- static final ArchRule domainIsPure = noClasses().that().resideInAPackage("..domain..")
550
+ static final ArchRule transportHasNoJdbc = noClasses()
551
+ .that().haveSimpleNameEndingWith("Resource")
552
+ .or().haveSimpleNameEndingWith("GrpcService")
553
+ .or().haveSimpleNameEndingWith("Consumer")
554
+ .should().dependOnClassesThat().resideInAnyPackage("java.sql..", "javax.sql..");
555
+
556
+ // A Handler injecting OutboxDispatcher still satisfies this rule: the dispatcher is a
557
+ // common/messaging bean, so no messaging API ever reaches the slice.
558
+ @ArchTest
559
+ static final ArchRule handlerKnowsNoTransport = noClasses()
560
+ .that().haveSimpleNameEndingWith("Handler")
561
+ .and().resideOutsideOfPackage("..common..")
312
562
  .should().dependOnClassesThat().resideInAnyPackage(
313
- "jakarta..", "io.quarkus..", "org.eclipse.microprofile..",
314
- "com.fasterxml.jackson..", "io.smallrye..", "java.sql..");
563
+ "jakarta.ws.rs..", "org.eclipse.microprofile.reactive.messaging..",
564
+ "io.grpc..", "io.quarkus.grpc..");
315
565
 
566
+ // --- Stack rules ------------------------------------------------------------
316
567
  @ArchTest
317
568
  static final ArchRule noOrm = noClasses().should().dependOnClassesThat()
318
569
  .resideInAnyPackage("jakarta.persistence..", "org.hibernate..", "io.quarkus.hibernate..");
570
+
571
+ // Mutiny is allowed ONLY in gRPC server adapters and common/client integration beans
572
+ @ArchTest
573
+ static final ArchRule reactiveIsQuarantined = noClasses()
574
+ .that().haveSimpleNameNotEndingWith("GrpcService")
575
+ .and().resideOutsideOfPackage("..common.client..")
576
+ .should().dependOnClassesThat().resideInAnyPackage(
577
+ "io.smallrye.mutiny..", "io.vertx.mutiny..");
578
+
579
+ // --- Transaction contract ---------------------------------------------------
580
+ @ArchTest
581
+ static final ArchRule transactionalOnlyOnProcess = methods()
582
+ .that().areAnnotatedWith(Transactional.class)
583
+ .and().areDeclaredInClassesThat().resideOutsideOfPackage("..common..")
584
+ .should().beDeclaredInClassesThat().haveSimpleNameEndingWith("Handler")
585
+ .andShould().haveName("process")
586
+ .andShould().bePublic();
587
+
588
+ @ArchTest
589
+ static final ArchRule handlersExposeOnlyProcess = methods()
590
+ .that().arePublic()
591
+ .and().areDeclaredInClassesThat().haveSimpleNameEndingWith("Handler")
592
+ .and().areDeclaredInClassesThat().resideOutsideOfPackage("..common..")
593
+ .should().haveName("process");
319
594
  }
320
595
  ```
321
596
 
322
597
  ### Naming rules (same test class)
323
598
 
324
- Conventions that are not enforced decay within two sprints. Add these alongside the layer rules — `resideInAPackage` is statically imported from `JavaClass.Predicates`:
325
-
326
599
  ```java
327
600
  @ArchTest
328
- static final ArchRule inboundPorts = classes().that().resideInAPackage("..application.port.in..")
329
- .should().beInterfaces()
330
- .andShould().haveSimpleNameEndingWith("UseCase");
331
-
332
- @ArchTest
333
- static final ArchRule outboundPorts = classes().that().resideInAPackage("..application.port.out..")
334
- .should().beInterfaces();
335
-
336
- @ArchTest
337
- static final ArchRule useCaseImpls = classes().that().resideInAPackage("..application.usecase..")
338
- .should().haveSimpleNameEndingWith("Service")
339
- .andShould().implement(resideInAPackage("..application.port.in.."));
601
+ static final ArchRule restResources = classes().that().areAnnotatedWith(Path.class)
602
+ .should().haveSimpleNameEndingWith("Resource");
340
603
 
341
604
  @ArchTest
342
- static final ArchRule serviceSuffixIsReserved = classes().that().haveSimpleNameEndingWith("Service")
343
- .should().resideInAnyPackage("..application.usecase..", "..infrastructure.grpc..");
605
+ static final ArchRule grpcServices = classes().that().areAnnotatedWith(GrpcService.class)
606
+ .should().haveSimpleNameEndingWith("GrpcService");
344
607
 
345
608
  @ArchTest
346
- static final ArchRule restResources = classes().that().areAnnotatedWith(Path.class)
347
- .should().haveSimpleNameEndingWith("Resource")
348
- .andShould().resideInAPackage("..infrastructure.rest..");
609
+ static final ArchRule dtosStayInDtoPackage = classes()
610
+ .that().haveSimpleNameEndingWith("Dto").or().haveSimpleNameEndingWith("Payload")
611
+ .should().resideInAnyPackage("..dto..", "..common.error..");
349
612
 
350
613
  @ArchTest
351
- static final ArchRule jdbcAdapters = classes().that().resideInAPackage("..infrastructure.persistence..")
352
- .and().implement(resideInAPackage("..application.port.out.."))
353
- .should().haveSimpleNameStartingWith("Jdbc");
614
+ static final ArchRule dtosHaveNoLogic = noClasses().that().resideInAPackage("..dto..")
615
+ .should().dependOnClassesThat().resideInAnyPackage("java.sql..", "javax.sql..");
354
616
 
355
617
  @ArchTest
356
- static final ArchRule dtosStayInAdapters = classes().that().haveSimpleNameEndingWith("Dto")
357
- .should().resideInAPackage("..infrastructure..");
618
+ static final ArchRule serviceSuffixIsReserved = classes()
619
+ .that().haveSimpleNameEndingWith("Service")
620
+ .should().haveSimpleNameEndingWith("GrpcService");
358
621
 
359
622
  @ArchTest
360
- static final ArchRule domainExceptions = classes().that().areAssignableTo(DomainException.class)
361
- .should().haveSimpleNameEndingWith("Exception")
362
- .andShould().resideInAPackage("..domain.exception..");
623
+ static final ArchRule businessExceptionIsTheOnlyOne = classes()
624
+ .that().haveSimpleNameEndingWith("Exception")
625
+ .should().resideInAPackage("..common.exception..");
363
626
 
364
627
  @ArchTest
365
628
  static final ArchRule bannedSuffixes = noClasses().should().haveSimpleNameEndingWith("Impl")
366
- .orShould().haveSimpleNameEndingWith("Util")
367
- .orShould().haveSimpleNameEndingWith("Utils")
629
+ .orShould().haveSimpleNameEndingWith("Util") // note: "Utils" is deliberately absent — common/util/StringUtils
368
630
  .orShould().haveSimpleNameEndingWith("Helper")
369
631
  .orShould().haveSimpleNameEndingWith("Manager")
632
+ .orShould().haveSimpleNameEndingWith("Mapper")
633
+ .orShould().haveSimpleNameEndingWith("UseCase")
634
+ .orShould().haveSimpleNameEndingWith("Repository")
635
+ .orShould().haveSimpleNameEndingWith("Dao")
370
636
  .orShould().haveSimpleNameEndingWith("DTO");
371
637
  ```
372
638
 
373
- Scope the analysis to your own packages (`@AnalyzeClasses(packages = "com.alva.bc05")`) so proto- and Quarkus-generated classes don't trip the suffix rules.
639
+ `common/util/StringUtils` and the `Jdbc` namespace helper (sql skill §6) are the sanctioned exceptions to `bannedSuffixes` exclude them by fully-qualified name rather than weakening the rule.
640
+
641
+ Rules the compiler cannot express (enforce in code review and in the generator checklists):
642
+
643
+ - `setAutoCommit` / `.commit()` / `.rollback()` appear nowhere in a slice. A CI `grep` is the cheapest possible gate:
644
+ `! grep -rnE '\.(setAutoCommit|commit|rollback)\(' src/main/java`
645
+ - No hand-built trace strings: `! grep -rnE '"00-"\s*\+' src/main/java` (observability skill).
646
+ - Every `ErrorCatalog` code exists in every locale bundle — write that test once, iterating codes × locales (error-handling skill).
374
647
 
375
648
  ## Native-image survival rules
376
649
 
377
- - Any class serialized by Jackson (DTOs, Kafka payloads) that is NOT touched by Quarkus build-time processing: annotate `@RegisterForReflection`.
378
- - Resources (SQL files, i18n bundles, certs): declare in `quarkus.native.resources.includes` if read at runtime via classpath.
650
+ - Any class serialized by Jackson (DTOs, Kafka payloads, `ErrorDto`) that is NOT touched by Quarkus build-time processing: annotate `@RegisterForReflection`.
651
+ - Resources read at runtime via the classpath: declare in `quarkus.native.resources.includes` `messages/*.properties` is required by the standard bundle layout. `quarkus.locales` must list every supported locale or the locale data is not embedded.
379
652
  - Avoid libraries relying on dynamic proxies/bytecode generation at runtime. Prefer Quarkus extensions over raw libraries — extensions do build-time registration for you.
653
+ - Hand-written row mapping (no reflection mappers) is already native-friendly; keep it that way.
380
654
  - Verify with a native integration test: `@QuarkusIntegrationTest` runs the same tests against the built binary. CI must run `./mvnw verify -Dnative` at least on release branches.
381
655
 
382
656
  ## Testing strategy
383
657
 
384
- | Layer | Test type | Tools |
658
+ | Target | Test type | Tools |
385
659
  |---|---|---|
386
- | domain | plain JUnit 5 unit tests, no Quarkus | JUnit, AssertJ |
387
- | application | unit tests with mocked ports | Mockito |
388
- | infrastructure/persistence | `@QuarkusTest` + Dev Services (Testcontainers Postgres) | REST Assured n/a |
389
- | REST/gRPC adapters | `@QuarkusTest` + REST Assured / gRPC client | |
660
+ | `<Slice>Handler` (business rules) | plain JUnit 5 + Mockito, **no Quarkus context** | JUnit, Mockito, AssertJ |
661
+ | `<Slice>Sql` | `@QuarkusTest` + Dev Services (Testcontainers Postgres) | real SQL against real Postgres, never H2 |
662
+ | `<Resource>Resource` | `@QuarkusTest` + REST Assured | contract, status codes, localized error body |
663
+ | `*GrpcService` / `*Consumer` | `@QuarkusTest` + generated client / in-memory connector | |
390
664
  | native binary | `@QuarkusIntegrationTest` | |
391
665
 
392
- Keep coverage meaningful: mutation testing (PIT) on domain + application layers is the quality gate, not raw line coverage.
666
+ `<Slice>HandlerTest` is mandatory and mirrors the slice. Rules:
667
+
668
+ - Mockito only — `@Mock <Slice>Sql sql;`, `@Mock DataSource dataSource;`, `@Mock Connection connection;`, `@InjectMocks <Slice>Handler handler;`, with `when(dataSource.getConnection()).thenReturn(connection)` in `@BeforeEach`. Never a real database, never `@QuarkusTest` for pure business rules, never both `@InjectMocks` (Mockito) and `@InjectMock` (Quarkus) in one file.
669
+ - Test names: `should<Result>When<Condition>` — `shouldReturnPartyIdWhenRequestIsValid`, `shouldThrowBusinessExceptionWhenGivenNameIsBlank`.
670
+ - Minimum scenarios per public business path:
671
+
672
+ | # | Scenario | How to trigger it | What to assert |
673
+ |---|---|---|---|
674
+ | 1 | Happy path | all `sql` mocks return valid values | the mapped response; the expected `sql` calls in order |
675
+ | 2 | Validation failure | a mandatory field blank/null | `BusinessException` thrown **and `verifyNoInteractions(sql)`** |
676
+ | 3 | Database failure | a `sql` method throws `SQLException` | `BusinessException` propagates out of `process()` (this is what makes the container roll back) |
677
+ | 4 | Error code correctness | inspect the thrown exception | `e.code()` is the expected `<MOD>-<HTTP>-<seq>`; the message is **not** rendered here — localization is asserted in the `Resource` test |
678
+
679
+ Because the container owns the transaction, a Handler unit test **never asserts `verify(connection).commit()` or `verify(connection).rollback()`** — those calls do not exist in the code. The rollback assertion is "a `RuntimeException` escapes `process()`"; verifying the actual rollback belongs to a `@QuarkusTest` that calls `process()` inside a real transaction and then reads the table back.
680
+
681
+ Keep coverage meaningful: mutation testing (PIT) on the `Handler` classes is the quality gate, not raw line coverage.