bmad-method-quarkus 1.0.2 → 1.0.3
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.
- package/package.json +1 -1
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-error-handling-i18n/SKILL.md +61 -27
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-grpc-services/SKILL.md +60 -41
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-hexagonal-core/SKILL.md +482 -213
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-kafka-messaging/SKILL.md +46 -29
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-observability-otel/SKILL.md +47 -31
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-openapi-tmforum/SKILL.md +42 -25
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-sql-jdbc-agroal/SKILL.md +115 -85
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md +61 -27
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md +60 -41
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md +482 -213
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md +46 -29
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md +47 -31
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md +42 -25
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md +115 -85
|
@@ -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
|
|
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.
|
|
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
|
|
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
|
|
17
|
-
4. **
|
|
18
|
-
5. **
|
|
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
|
|
|
@@ -46,129 +63,342 @@ 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
|
-
##
|
|
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
|
|
53
|
-
├──
|
|
54
|
-
│ ├──
|
|
55
|
-
│ ├──
|
|
56
|
-
│
|
|
57
|
-
├──
|
|
58
|
-
│ ├──
|
|
59
|
-
│
|
|
60
|
-
│
|
|
61
|
-
│
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
## Class naming conventions (canonical)
|
|
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.
|
|
73
111
|
|
|
74
|
-
|
|
112
|
+
### Slice naming variables
|
|
75
113
|
|
|
76
|
-
|
|
114
|
+
Every generator and every review uses these three names consistently:
|
|
77
115
|
|
|
78
|
-
|
|
|
116
|
+
| Variable | Meaning | Example |
|
|
79
117
|
|---|---|---|
|
|
80
|
-
|
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
|
|
84
|
-
|
|
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` |
|
|
121
|
+
|
|
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.
|
|
88
123
|
|
|
89
|
-
|
|
124
|
+
## Class naming conventions (canonical)
|
|
90
125
|
|
|
91
|
-
|
|
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
|
-
|
|
|
96
|
-
|
|
|
97
|
-
|
|
|
98
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
|
|
|
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
|
-
|
|
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
|
-
###
|
|
152
|
+
### Two slices, one TMF resource path
|
|
106
153
|
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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.
|
|
158
|
+
|
|
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.
|
|
115
168
|
|
|
116
|
-
|
|
169
|
+
### Other identifiers (for reference)
|
|
117
170
|
|
|
118
|
-
|
|
|
171
|
+
| Kind | Convention | Example |
|
|
119
172
|
|---|---|---|
|
|
120
|
-
|
|
|
121
|
-
|
|
|
122
|
-
|
|
|
123
|
-
|
|
|
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
|
|
182
|
+
## The Handler — logic and transaction contract
|
|
126
183
|
|
|
127
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
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>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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).
|
|
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
|
+
sql.insertOutboxEvent(conn, partyId, "party", "com.alva.customer.party.created.v1",
|
|
238
|
+
payloadJson(partyId, request), traceparent()); // traceparent captured from Span.current() (see kafka/observability skills)
|
|
239
|
+
return partyId;
|
|
240
|
+
} catch (SQLException e) {
|
|
241
|
+
throw SqlStateTranslator.translate("PTY-500-001", e);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### `getResult()` — response mapping
|
|
247
|
+
|
|
248
|
+
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.
|
|
249
|
+
|
|
250
|
+
```java
|
|
251
|
+
private CreatePartyIndividualResponseDto getResult(UUID partyId) {
|
|
252
|
+
return CreatePartyIndividualResponseDto.builder()
|
|
253
|
+
.partyId(partyId.toString())
|
|
254
|
+
.build();
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### `process()` — the single entry point
|
|
259
|
+
|
|
260
|
+
```java
|
|
261
|
+
@Transactional
|
|
262
|
+
@WithSpan("usecase.createPartyIndividual")
|
|
263
|
+
public CreatePartyIndividualResponseDto process(CreatePartyIndividualRequestDto request) {
|
|
264
|
+
validate(request);
|
|
265
|
+
UUID partyId = execution(request);
|
|
266
|
+
return getResult(partyId);
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
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).
|
|
271
|
+
|
|
272
|
+
### Transaction rules — no exceptions
|
|
273
|
+
|
|
274
|
+
The container (Quarkus + Narayana JTA) owns the transaction: it **commits** when `process()` returns normally and **rolls back** on any propagated `RuntimeException`, `BusinessException` included.
|
|
275
|
+
|
|
276
|
+
- **`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)`.
|
|
277
|
+
- Closing the `Connection` (try-with-resources) returns it to the pool; it does **not** end the JTA transaction.
|
|
278
|
+
- `@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.
|
|
279
|
+
- **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.
|
|
280
|
+
- 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).
|
|
281
|
+
|
|
282
|
+
**Pre-delivery checklist for every Handler:**
|
|
283
|
+
|
|
284
|
+
- [ ] `setAutoCommit` does not appear in the file.
|
|
285
|
+
- [ ] `.commit()` does not appear in the file.
|
|
286
|
+
- [ ] `.rollback()` does not appear in the file.
|
|
287
|
+
- [ ] `@Transactional` appears at most once, on `process()` — and is absent on a read-only slice.
|
|
288
|
+
- [ ] Exactly one `dataSource.getConnection()`, passed to every `Sql` call.
|
|
289
|
+
- [ ] Every `Sql` call matches the real signature (name, order, arity, types).
|
|
290
|
+
- [ ] `BusinessException` is thrown with a code + args, never with a pre-rendered message.
|
|
291
|
+
- [ ] No `Uni`/`Multi`/`PgPool`, no `@GrpcClient` stub, no transport type, no DTO redeclared as a nested class.
|
|
292
|
+
|
|
293
|
+
### When to add an interface back
|
|
294
|
+
|
|
295
|
+
Reintroduce a port interface (and only then) when one of these is true, and say so in the slice README:
|
|
296
|
+
|
|
297
|
+
- A **second real implementation** exists in production code (two data sources, a stub for a partner API, a feature-flagged alternative).
|
|
298
|
+
- The implementation lives in another deployable and is injected across a module boundary.
|
|
299
|
+
- The consumer is `common/`/`libs/` code that must not depend on a slice.
|
|
300
|
+
|
|
301
|
+
"Mocking is easier" is not a reason — Mockito mocks the concrete `Sql` class fine.
|
|
302
|
+
|
|
303
|
+
## The Sql class — data adapter
|
|
304
|
+
|
|
305
|
+
Full standard (pool config, batches, jsonb, pagination, keyset paging, SQLState translation) lives in the **quarkus-sql-jdbc-agroal** skill. Slice-specific shape:
|
|
306
|
+
|
|
307
|
+
- `@ApplicationScoped`, `@Inject DataSource dataSource;`, then `private static final String` query constants, then one public method per query.
|
|
308
|
+
- 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`.
|
|
309
|
+
- 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()`.
|
|
310
|
+
- **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.
|
|
311
|
+
- Methods `throws SQLException` — they never swallow it and never translate it. Translation to `BusinessException` happens in the Handler via `SqlStateTranslator`.
|
|
312
|
+
- **No transaction control here.** No `commit`, no `rollback`, no `setAutoCommit`.
|
|
313
|
+
- Row mapping is hand-written into DTOs (`PartyDto.builder()...`) — no reflection-based mappers, which also keeps native images small.
|
|
314
|
+
|
|
315
|
+
## DTOs
|
|
316
|
+
|
|
317
|
+
- Live in `${SLICE_PACKAGE}.dto`, one file per DTO **including nested ones** — never a `static class` or local `record`.
|
|
318
|
+
- Lombok: `@Data`, `@NoArgsConstructor`, `@AllArgsConstructor` on every serializable DTO; `@Builder` on responses and on anything the Handler builds in parts.
|
|
319
|
+
- **Date fields are always `String`** in a DTO (request and response), ISO-8601. Conversion happens exclusively in the Handler.
|
|
320
|
+
- Form validation only (`jakarta.validation.constraints.*`); business rules belong in `validate()`.
|
|
321
|
+
- 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.
|
|
322
|
+
- `@RegisterForReflection` on anything Jackson touches that Quarkus does not process at build time — Kafka payloads especially.
|
|
323
|
+
- No business logic inside a DTO.
|
|
324
|
+
|
|
325
|
+
## Transport adapters
|
|
326
|
+
|
|
327
|
+
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.
|
|
328
|
+
|
|
329
|
+
**REST** — full standard in **quarkus-openapi-tmforum** and **quarkus-error-handling-i18n**:
|
|
330
|
+
|
|
331
|
+
- `<Resource>Resource` class with `@Path` + `@Tag`, injecting the slice `Handler`(s).
|
|
332
|
+
- Returns `RestResponse<T>` synchronously. Never `Uni`/`Multi`.
|
|
333
|
+
- `@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).
|
|
334
|
+
- Extracts `tenantId`, `partyId`, `partyRolList`, `language` from headers when the contract requires them, and sets them on the request DTO before calling `process()`.
|
|
335
|
+
- Full OpenAPI annotations per method: `@Operation` with `operationId`, one `@APIResponse` per relevant status, `@Parameter` for headers and path/query params, `Location` header on create.
|
|
336
|
+
|
|
337
|
+
**gRPC** — full standard in **quarkus-grpc-services**:
|
|
338
|
+
|
|
339
|
+
- Proto in `src/main/proto/<module>/v<major>/`, published to `contracts/`, package `<org>.<module>.v1`, `snake_case` fields.
|
|
340
|
+
- `@GrpcService` class named after the proto service, injecting the slice `Handler`.
|
|
341
|
+
- One of the two sanctioned Mutiny sites: `Uni.createFrom().item(() -> {...}).runSubscriptionOn(Infrastructure.getDefaultWorkerPool())`, with proto ↔ DTO conversion inside the lambda.
|
|
342
|
+
|
|
343
|
+
**Kafka** — full standard in **quarkus-kafka-messaging**:
|
|
344
|
+
|
|
345
|
+
- `<Event>Consumer` class, `@ApplicationScoped`, injecting the slice `Handler`.
|
|
346
|
+
- `@Incoming` methods are `@Blocking` (JDBC below). Idempotency check + `process()` + DLQ strategy per the kafka skill.
|
|
347
|
+
- Producing domain events is **not** done here — the Handler inserts an outbox row in its transaction and `OutboxRelayJob` relays it.
|
|
348
|
+
|
|
349
|
+
## Errors and i18n
|
|
350
|
+
|
|
351
|
+
Governed by **quarkus-error-handling-i18n**; the slice-relevant rules:
|
|
352
|
+
|
|
353
|
+
- 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.
|
|
354
|
+
- Technical failures keep the `PersistenceException` family; `SqlStateTranslator` maps SQLState → error code (`23505` → conflict, `40001` → transient, `57014` → timeout).
|
|
355
|
+
- 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).
|
|
356
|
+
- `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`.
|
|
357
|
+
- **Message resolution happens at the edge, never in the Handler.** `GlobalExceptionHandler` (REST) and `GrpcExceptionInterceptor` (gRPC) are the only places a message string is produced.
|
|
358
|
+
- Native: `quarkus.locales=en,es,pt` and `quarkus.native.resources.includes=messages/*.properties`.
|
|
359
|
+
|
|
360
|
+
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()`.
|
|
361
|
+
|
|
362
|
+
## Observability
|
|
363
|
+
|
|
364
|
+
Governed by **quarkus-observability-otel**; the slice-relevant rules:
|
|
365
|
+
|
|
366
|
+
- `@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.
|
|
367
|
+
- Logger per class via `Log`/`Logger.getLogger(X.class)`; the Handler's class name already carries the slice. Never `System.out`.
|
|
368
|
+
- Trace ids come only from `Span.current().getSpanContext()` — never hand-built strings. Outbox rows carry `traceparent` + a `traceContext` block in the payload.
|
|
369
|
+
- Audit writes go through the `AuditRecorder` bean in `common/audit`, which captures the span itself.
|
|
370
|
+
- No PII or secrets in spans, attributes or logs.
|
|
371
|
+
|
|
372
|
+
## Shared code
|
|
373
|
+
|
|
374
|
+
`common/` holds only what **two or more slices** genuinely share, or what is structurally cross-cutting: the exception/error/i18n machinery, integration beans, `OutboxRelayJob`, `AuditRecorder`, `@ConfigMapping` interfaces, `StringUtils`.
|
|
375
|
+
|
|
376
|
+
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.
|
|
377
|
+
|
|
378
|
+
Code shared across **apps** in the domain goes to `libs/` and uses constructor injection.
|
|
160
379
|
|
|
161
380
|
## Scaffolding checklist
|
|
162
381
|
|
|
163
382
|
When creating a new service:
|
|
164
383
|
|
|
165
384
|
1. App folder `apps/<module>-<service>-ms/` per the monorepo layout above.
|
|
166
|
-
2. `pom.xml` with BOM `io.quarkus.platform:quarkus-bom` (
|
|
167
|
-
3. `
|
|
168
|
-
4.
|
|
169
|
-
5.
|
|
170
|
-
6.
|
|
171
|
-
7. `
|
|
385
|
+
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`, plus Lombok and skill-specific extensions as needed.
|
|
386
|
+
3. `common/` package: `BusinessException`, `ErrorCatalog`, `ErrorDto`, `GlobalExceptionHandler`, `MessageResolver`, `SqlStateTranslator`, `StringUtils`.
|
|
387
|
+
4. `application.properties` baseline (below).
|
|
388
|
+
5. ArchUnit test (below) — in the same commit as the first slice, not later.
|
|
389
|
+
6. Native profile using the Mandrel container build.
|
|
390
|
+
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.
|
|
391
|
+
8. `service.yaml` + per-app `README.md` (below) — in the same PR as the first endpoint; CI blocks merges without them.
|
|
392
|
+
|
|
393
|
+
When creating a new slice, in this order (each step reads the previous file's real signatures):
|
|
394
|
+
|
|
395
|
+
1. `dto/` — request, response, nested DTOs.
|
|
396
|
+
2. `<Slice>Sql.java` — query constants + one method per query, `Connection` first.
|
|
397
|
+
3. `<Slice>Handler.java` — `validate` → `execution` → `getResult` → `process`.
|
|
398
|
+
4. Transport class(es): `Resource` always; `GrpcService`/`Consumer` only if the plan requires them (+ the `.proto` / channel config).
|
|
399
|
+
5. New error codes registered in `ErrorCatalog` and added to **every** locale bundle.
|
|
400
|
+
6. `<Slice>HandlerTest.java`.
|
|
401
|
+
7. Slice `README.md`.
|
|
172
402
|
|
|
173
403
|
### application.properties baseline
|
|
174
404
|
|
|
@@ -177,6 +407,11 @@ quarkus.datasource.db-kind=postgresql
|
|
|
177
407
|
quarkus.datasource.jdbc.max-size=16
|
|
178
408
|
quarkus.flyway.migrate-at-start=true
|
|
179
409
|
|
|
410
|
+
# i18n (see error-handling skill) — locales must be declared for native
|
|
411
|
+
quarkus.locales=en,es
|
|
412
|
+
quarkus.default-locale=en
|
|
413
|
+
quarkus.native.resources.includes=messages/*.properties
|
|
414
|
+
|
|
180
415
|
# Native build via Mandrel container (no local GraalVM needed)
|
|
181
416
|
quarkus.native.container-build=true
|
|
182
417
|
quarkus.native.builder-image=mandrel
|
|
@@ -199,6 +434,25 @@ quarkus.http.port=8080
|
|
|
199
434
|
|
|
200
435
|
Build: `./mvnw package -Dnative` → binary in `target/*-runner`.
|
|
201
436
|
|
|
437
|
+
### Dockerfile (native image — mandatory)
|
|
438
|
+
|
|
439
|
+
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:
|
|
440
|
+
|
|
441
|
+
```dockerfile
|
|
442
|
+
FROM quay.io/quarkus/quarkus-micro-image:2.0
|
|
443
|
+
WORKDIR /work/
|
|
444
|
+
COPY --chown=1001:root target/*-runner /work/application
|
|
445
|
+
EXPOSE 8080
|
|
446
|
+
USER 1001
|
|
447
|
+
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Rules:
|
|
451
|
+
- Base image: `quay.io/quarkus/quarkus-micro-image` (or a distroless equivalent per platform standard) — no JDK/JRE layer in the final image.
|
|
452
|
+
- Non-root user (`USER 1001`), binary copied as `application`, port matching `quarkus.http.port`.
|
|
453
|
+
- 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.
|
|
454
|
+
- A `Dockerfile.jvm` may exist for local debugging only; it is never what CI publishes.
|
|
455
|
+
|
|
202
456
|
## Per-app documentation (mandatory, blocking)
|
|
203
457
|
|
|
204
458
|
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 +464,37 @@ Required `README.md` sections:
|
|
|
210
464
|
|
|
211
465
|
1. **Identity** — app name, module (`part-of`), type (`ms`), owning team, repository, criticality, part of core yes/no
|
|
212
466
|
2. **Purpose & responsibility** — description + out of scope
|
|
213
|
-
3. **
|
|
214
|
-
4. **
|
|
215
|
-
5. **
|
|
216
|
-
6. **
|
|
217
|
-
7. **
|
|
218
|
-
8. **
|
|
219
|
-
9. **
|
|
220
|
-
10. **
|
|
221
|
-
11. **
|
|
222
|
-
12. **
|
|
223
|
-
13. **
|
|
224
|
-
14. **
|
|
225
|
-
15. **
|
|
226
|
-
16. **
|
|
467
|
+
3. **Slice inventory** — table: slice folder, capability, exposed channels (REST/gRPC/Kafka), link to the slice README
|
|
468
|
+
4. **Tenancy model** — `shared` | `per-tenant` | `pool`; tenant identification (validated JWT claim, never a free header); data isolation; per-tenant parameters
|
|
469
|
+
5. **APIs exposed** — table: api/version, protocol, route/topic, contract file, auth, visibility
|
|
470
|
+
6. **Dependencies** — table: name, type, criticality (`hard`|`soft`), timeout/retry, behavior if down
|
|
471
|
+
7. **Data & persistence** — engine, database/schema, tenant isolation, migrations (`db/`), cache, events
|
|
472
|
+
8. **Configuration** — table: variable, description, type, required, default, varies-by (`environment`|`client`|`tenant`)
|
|
473
|
+
9. **Secrets** — table: logical name, what it is, source (vault), who provides it, rotation period — **never the value**
|
|
474
|
+
10. **Runtime & resources** — runtime/version, ports, requests/limits, replicas/HPA
|
|
475
|
+
11. **Health checks** — liveness; readiness = **hard dependencies only**; startup probe
|
|
476
|
+
12. **Deployment dependencies** — classified: startup / readiness-critical / hard runtime / soft runtime
|
|
477
|
+
13. **Observability** — configurable log levels (`TRACE`–`FATAL`), the ConfigMap variable holding `logging_level`, its default, JSON log format
|
|
478
|
+
14. **GitOps deployment** — location in the config repo, delivery strategy, feature flags
|
|
479
|
+
15. **Rollback & recovery** — procedure, reversible migrations (N/N-1 compatible), RTO/RPO
|
|
480
|
+
16. **Post-deploy validation** — smoke tests, contract tests, tenant-isolation tests
|
|
481
|
+
17. **Troubleshooting** — symptom → cause → diagnosis
|
|
227
482
|
|
|
228
483
|
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
484
|
|
|
485
|
+
### Slice README (inside the slice folder)
|
|
486
|
+
|
|
487
|
+
Every slice carries its own `README.md`, generated from the real code — no invented routes, fields or tables. Sections:
|
|
488
|
+
|
|
489
|
+
1. **Title** — `SERVICE_CLASS_PREFIX`
|
|
490
|
+
2. **Description** — business purpose and context of the capability
|
|
491
|
+
3. **Exposed routes** — table: channel (REST/gRPC/Kafka), route or operation, method/topic
|
|
492
|
+
4. **Input contract** — table: field, type, required, description
|
|
493
|
+
5. **Output contract** — table: field, type, description
|
|
494
|
+
6. **Database dependencies** — table: table, use
|
|
495
|
+
7. **Business errors** — table: error code, HTTP status, when it is raised
|
|
496
|
+
8. **Usage example** — compact, valid `curl` + JSON response
|
|
497
|
+
|
|
230
498
|
### Domain README (repo root)
|
|
231
499
|
|
|
232
500
|
The monorepo root `README.md` is the onboarding doc for the domain, and the owning team keeps it current:
|
|
@@ -237,156 +505,157 @@ The monorepo root `README.md` is the onboarding doc for the domain, and the owni
|
|
|
237
505
|
|
|
238
506
|
It never documents live environment or client configuration — the deployed state per environment/client is managed in the GitOps config repo.
|
|
239
507
|
|
|
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
508
|
## ArchUnit enforcement (mandatory)
|
|
294
509
|
|
|
295
|
-
`src/test/java/.../ArchitectureTest.java
|
|
510
|
+
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
511
|
|
|
297
512
|
```java
|
|
298
|
-
@AnalyzeClasses(packages = "com.
|
|
513
|
+
@AnalyzeClasses(packages = "com.alva.customer", importOptions = ImportOption.DoNotIncludeTests.class)
|
|
299
514
|
class ArchitectureTest {
|
|
300
515
|
|
|
516
|
+
// --- Slice isolation: a slice never imports another slice -------------------
|
|
517
|
+
@ArchTest
|
|
518
|
+
static final ArchRule slicesAreIndependent = slices()
|
|
519
|
+
.matching("com.alva.customer.(*)..")
|
|
520
|
+
.namingSlices("slice $1")
|
|
521
|
+
.should().notDependOnEachOther()
|
|
522
|
+
.ignoreDependency(resideInAPackage("..common.."), alwaysTrue())
|
|
523
|
+
.ignoreDependency(alwaysTrue(), resideInAPackage("..common.."));
|
|
524
|
+
|
|
525
|
+
// --- Hexagon dependency rule inside the slice ------------------------------
|
|
301
526
|
@ArchTest
|
|
302
|
-
static final ArchRule
|
|
303
|
-
.
|
|
304
|
-
.
|
|
305
|
-
.layer("Infrastructure").definedBy("..infrastructure..")
|
|
306
|
-
.whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Infrastructure")
|
|
307
|
-
.whereLayer("Application").mayOnlyBeAccessedByLayers("Infrastructure")
|
|
308
|
-
.whereLayer("Infrastructure").mayNotBeAccessedByAnyLayer();
|
|
527
|
+
static final ArchRule onlyHandlersTouchSql = classes()
|
|
528
|
+
.that().haveSimpleNameEndingWith("Sql")
|
|
529
|
+
.should().onlyBeAccessed().byClassesThat().haveSimpleNameEndingWith("Handler");
|
|
309
530
|
|
|
310
531
|
@ArchTest
|
|
311
|
-
static final ArchRule
|
|
532
|
+
static final ArchRule transportHasNoJdbc = noClasses()
|
|
533
|
+
.that().haveSimpleNameEndingWith("Resource")
|
|
534
|
+
.or().haveSimpleNameEndingWith("GrpcService")
|
|
535
|
+
.or().haveSimpleNameEndingWith("Consumer")
|
|
536
|
+
.should().dependOnClassesThat().resideInAnyPackage("java.sql..", "javax.sql..");
|
|
537
|
+
|
|
538
|
+
@ArchTest
|
|
539
|
+
static final ArchRule handlerKnowsNoTransport = noClasses()
|
|
540
|
+
.that().haveSimpleNameEndingWith("Handler")
|
|
541
|
+
.and().resideOutsideOfPackage("..common..")
|
|
312
542
|
.should().dependOnClassesThat().resideInAnyPackage(
|
|
313
|
-
"jakarta
|
|
314
|
-
"
|
|
543
|
+
"jakarta.ws.rs..", "org.eclipse.microprofile.reactive.messaging..",
|
|
544
|
+
"io.grpc..", "io.quarkus.grpc..");
|
|
315
545
|
|
|
546
|
+
// --- Stack rules ------------------------------------------------------------
|
|
316
547
|
@ArchTest
|
|
317
548
|
static final ArchRule noOrm = noClasses().should().dependOnClassesThat()
|
|
318
549
|
.resideInAnyPackage("jakarta.persistence..", "org.hibernate..", "io.quarkus.hibernate..");
|
|
550
|
+
|
|
551
|
+
// Mutiny is allowed ONLY in gRPC server adapters and common/client integration beans
|
|
552
|
+
@ArchTest
|
|
553
|
+
static final ArchRule reactiveIsQuarantined = noClasses()
|
|
554
|
+
.that().haveSimpleNameNotEndingWith("GrpcService")
|
|
555
|
+
.and().resideOutsideOfPackage("..common.client..")
|
|
556
|
+
.should().dependOnClassesThat().resideInAnyPackage(
|
|
557
|
+
"io.smallrye.mutiny..", "io.vertx.mutiny..");
|
|
558
|
+
|
|
559
|
+
// --- Transaction contract ---------------------------------------------------
|
|
560
|
+
@ArchTest
|
|
561
|
+
static final ArchRule transactionalOnlyOnProcess = methods()
|
|
562
|
+
.that().areAnnotatedWith(Transactional.class)
|
|
563
|
+
.and().areDeclaredInClassesThat().resideOutsideOfPackage("..common..")
|
|
564
|
+
.should().beDeclaredInClassesThat().haveSimpleNameEndingWith("Handler")
|
|
565
|
+
.andShould().haveName("process")
|
|
566
|
+
.andShould().bePublic();
|
|
567
|
+
|
|
568
|
+
@ArchTest
|
|
569
|
+
static final ArchRule handlersExposeOnlyProcess = methods()
|
|
570
|
+
.that().arePublic()
|
|
571
|
+
.and().areDeclaredInClassesThat().haveSimpleNameEndingWith("Handler")
|
|
572
|
+
.and().areDeclaredInClassesThat().resideOutsideOfPackage("..common..")
|
|
573
|
+
.should().haveName("process");
|
|
319
574
|
}
|
|
320
575
|
```
|
|
321
576
|
|
|
322
577
|
### Naming rules (same test class)
|
|
323
578
|
|
|
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
579
|
```java
|
|
327
580
|
@ArchTest
|
|
328
|
-
static final ArchRule
|
|
329
|
-
.should().
|
|
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.."));
|
|
581
|
+
static final ArchRule restResources = classes().that().areAnnotatedWith(Path.class)
|
|
582
|
+
.should().haveSimpleNameEndingWith("Resource");
|
|
340
583
|
|
|
341
584
|
@ArchTest
|
|
342
|
-
static final ArchRule
|
|
343
|
-
.should().
|
|
585
|
+
static final ArchRule grpcServices = classes().that().areAnnotatedWith(GrpcService.class)
|
|
586
|
+
.should().haveSimpleNameEndingWith("GrpcService");
|
|
344
587
|
|
|
345
588
|
@ArchTest
|
|
346
|
-
static final ArchRule
|
|
347
|
-
.
|
|
348
|
-
.
|
|
589
|
+
static final ArchRule dtosStayInDtoPackage = classes()
|
|
590
|
+
.that().haveSimpleNameEndingWith("Dto").or().haveSimpleNameEndingWith("Payload")
|
|
591
|
+
.should().resideInAnyPackage("..dto..", "..common.error..");
|
|
349
592
|
|
|
350
593
|
@ArchTest
|
|
351
|
-
static final ArchRule
|
|
352
|
-
.
|
|
353
|
-
.should().haveSimpleNameStartingWith("Jdbc");
|
|
594
|
+
static final ArchRule dtosHaveNoLogic = noClasses().that().resideInAPackage("..dto..")
|
|
595
|
+
.should().dependOnClassesThat().resideInAnyPackage("java.sql..", "javax.sql..");
|
|
354
596
|
|
|
355
597
|
@ArchTest
|
|
356
|
-
static final ArchRule
|
|
357
|
-
.
|
|
598
|
+
static final ArchRule serviceSuffixIsReserved = classes()
|
|
599
|
+
.that().haveSimpleNameEndingWith("Service")
|
|
600
|
+
.should().haveSimpleNameEndingWith("GrpcService");
|
|
358
601
|
|
|
359
602
|
@ArchTest
|
|
360
|
-
static final ArchRule
|
|
361
|
-
.
|
|
362
|
-
.
|
|
603
|
+
static final ArchRule businessExceptionIsTheOnlyOne = classes()
|
|
604
|
+
.that().haveSimpleNameEndingWith("Exception")
|
|
605
|
+
.should().resideInAPackage("..common.exception..");
|
|
363
606
|
|
|
364
607
|
@ArchTest
|
|
365
608
|
static final ArchRule bannedSuffixes = noClasses().should().haveSimpleNameEndingWith("Impl")
|
|
366
|
-
.orShould().haveSimpleNameEndingWith("Util")
|
|
367
|
-
.orShould().haveSimpleNameEndingWith("Utils")
|
|
609
|
+
.orShould().haveSimpleNameEndingWith("Util") // note: "Utils" is deliberately absent — common/util/StringUtils
|
|
368
610
|
.orShould().haveSimpleNameEndingWith("Helper")
|
|
369
611
|
.orShould().haveSimpleNameEndingWith("Manager")
|
|
612
|
+
.orShould().haveSimpleNameEndingWith("Mapper")
|
|
613
|
+
.orShould().haveSimpleNameEndingWith("UseCase")
|
|
614
|
+
.orShould().haveSimpleNameEndingWith("Repository")
|
|
615
|
+
.orShould().haveSimpleNameEndingWith("Dao")
|
|
370
616
|
.orShould().haveSimpleNameEndingWith("DTO");
|
|
371
617
|
```
|
|
372
618
|
|
|
373
|
-
|
|
619
|
+
`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.
|
|
620
|
+
|
|
621
|
+
Rules the compiler cannot express (enforce in code review and in the generator checklists):
|
|
622
|
+
|
|
623
|
+
- `setAutoCommit` / `.commit()` / `.rollback()` appear nowhere in a slice. A CI `grep` is the cheapest possible gate:
|
|
624
|
+
`! grep -rnE '\.(setAutoCommit|commit|rollback)\(' src/main/java`
|
|
625
|
+
- No hand-built trace strings: `! grep -rnE '"00-"\s*\+' src/main/java` (observability skill).
|
|
626
|
+
- Every `ErrorCatalog` code exists in every locale bundle — write that test once, iterating codes × locales (error-handling skill).
|
|
374
627
|
|
|
375
628
|
## Native-image survival rules
|
|
376
629
|
|
|
377
|
-
- Any class serialized by Jackson (DTOs, Kafka payloads) that is NOT touched by Quarkus build-time processing: annotate `@RegisterForReflection`.
|
|
378
|
-
- Resources
|
|
630
|
+
- Any class serialized by Jackson (DTOs, Kafka payloads, `ErrorDto`) that is NOT touched by Quarkus build-time processing: annotate `@RegisterForReflection`.
|
|
631
|
+
- 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
632
|
- Avoid libraries relying on dynamic proxies/bytecode generation at runtime. Prefer Quarkus extensions over raw libraries — extensions do build-time registration for you.
|
|
633
|
+
- Hand-written row mapping (no reflection mappers) is already native-friendly; keep it that way.
|
|
380
634
|
- 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
635
|
|
|
382
636
|
## Testing strategy
|
|
383
637
|
|
|
384
|
-
|
|
|
638
|
+
| Target | Test type | Tools |
|
|
385
639
|
|---|---|---|
|
|
386
|
-
|
|
|
387
|
-
|
|
|
388
|
-
|
|
|
389
|
-
|
|
|
640
|
+
| `<Slice>Handler` (business rules) | plain JUnit 5 + Mockito, **no Quarkus context** | JUnit, Mockito, AssertJ |
|
|
641
|
+
| `<Slice>Sql` | `@QuarkusTest` + Dev Services (Testcontainers Postgres) | real SQL against real Postgres, never H2 |
|
|
642
|
+
| `<Resource>Resource` | `@QuarkusTest` + REST Assured | contract, status codes, localized error body |
|
|
643
|
+
| `*GrpcService` / `*Consumer` | `@QuarkusTest` + generated client / in-memory connector | |
|
|
390
644
|
| native binary | `@QuarkusIntegrationTest` | |
|
|
391
645
|
|
|
392
|
-
|
|
646
|
+
`<Slice>HandlerTest` is mandatory and mirrors the slice. Rules:
|
|
647
|
+
|
|
648
|
+
- 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.
|
|
649
|
+
- Test names: `should<Result>When<Condition>` — `shouldReturnPartyIdWhenRequestIsValid`, `shouldThrowBusinessExceptionWhenGivenNameIsBlank`.
|
|
650
|
+
- Minimum scenarios per public business path:
|
|
651
|
+
|
|
652
|
+
| # | Scenario | How to trigger it | What to assert |
|
|
653
|
+
|---|---|---|---|
|
|
654
|
+
| 1 | Happy path | all `sql` mocks return valid values | the mapped response; the expected `sql` calls in order |
|
|
655
|
+
| 2 | Validation failure | a mandatory field blank/null | `BusinessException` thrown **and `verifyNoInteractions(sql)`** |
|
|
656
|
+
| 3 | Database failure | a `sql` method throws `SQLException` | `BusinessException` propagates out of `process()` (this is what makes the container roll back) |
|
|
657
|
+
| 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 |
|
|
658
|
+
|
|
659
|
+
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.
|
|
660
|
+
|
|
661
|
+
Keep coverage meaningful: mutation testing (PIT) on the `Handler` classes is the quality gate, not raw line coverage.
|