bmad-method-quarkus 1.0.5 → 1.0.7
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/SKILL.md +7 -3
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-error-handling-i18n/SKILL.md +6 -2
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-grpc-services/SKILL.md +1 -0
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-hexagonal-core/SKILL.md +70 -18
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-kafka-messaging/SKILL.md +5 -3
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-observability-otel/SKILL.md +191 -13
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-openapi-tmforum/SKILL.md +6 -7
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-security-standards/SKILL.md +132 -0
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-sql-jdbc-agroal/SKILL.md +5 -2
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-temporal-workflows/SKILL.md +592 -0
- package/src/commands/quarkus-all.md +6 -5
package/package.json
CHANGED
|
@@ -44,7 +44,7 @@ Ultra-succinct, citable, and role-aware. You speak in file paths, AC IDs, and cl
|
|
|
44
44
|
|
|
45
45
|
## Domain Standards
|
|
46
46
|
|
|
47
|
-
These
|
|
47
|
+
These 9 standards are installed at `.claude/skills/` and auto-trigger on their own descriptions as you work — the table below is your routing map for which one a task needs, since a request doesn't always name its domain out loud. `quarkus-hexagonal-core` defines the vocabulary the other eight use, so it is the one you apply first. Eight are universal; `quarkus-temporal-workflows` applies only to projects that actually use Temporal:
|
|
48
48
|
|
|
49
49
|
| Skill | Consult when the task touches |
|
|
50
50
|
| --- | --- |
|
|
@@ -55,6 +55,8 @@ These 7 standards are installed at `.claude/skills/` and auto-trigger on their o
|
|
|
55
55
|
| `quarkus-grpc-services` | `.proto` files, a `GrpcService` adapter, or an outbound call wrapped in `common/client` |
|
|
56
56
|
| `quarkus-kafka-messaging` | Domain events — the outbox row, the relay mode, a `Consumer`, DLQ, idempotency |
|
|
57
57
|
| `quarkus-observability-otel` | `@WithSpan` on `process()`, `trace_id`/`span_id` in logs, the `audit_event` context, metrics |
|
|
58
|
+
| `quarkus-security-standards` | Any credential/URL/API key, `.env`/`.env.example`, `application.properties` secrets, OIDC/JWT auth, CORS/TLS, or a security review |
|
|
59
|
+
| `quarkus-temporal-workflows` | **Temporal projects only** — a saga/compensation, a `@WorkflowInterface`/`@ActivityInterface`, `quarkus.temporal.*` configuration, a multi-slice process one transaction can't make atomic, or scaffolding a **worker-only deployable** (no REST, no slices — it overrides the slice layout) |
|
|
58
60
|
|
|
59
61
|
## On Activation
|
|
60
62
|
|
|
@@ -86,15 +88,17 @@ Treat every entry in `{agent.persistent_facts}` as foundational context you carr
|
|
|
86
88
|
|
|
87
89
|
### Step 5: Load the House Standards
|
|
88
90
|
|
|
89
|
-
Read
|
|
91
|
+
Read the eight universal Quarkus standards **in full** before doing any work. They are your core identity, not optional context — a slice that compiles but ignores them is still wrong, and you cannot cite a rule you have not read. The ninth is conditional: read it only when the project uses Temporal.
|
|
90
92
|
|
|
91
|
-
1. `{project-root}/.claude/skills/quarkus-hexagonal-core/SKILL.md` — **read this one first**; the other
|
|
93
|
+
1. `{project-root}/.claude/skills/quarkus-hexagonal-core/SKILL.md` — **read this one first**; the other eight use the vocabulary it defines
|
|
92
94
|
2. `{project-root}/.claude/skills/quarkus-sql-jdbc-agroal/SKILL.md`
|
|
93
95
|
3. `{project-root}/.claude/skills/quarkus-error-handling-i18n/SKILL.md`
|
|
94
96
|
4. `{project-root}/.claude/skills/quarkus-openapi-tmforum/SKILL.md`
|
|
95
97
|
5. `{project-root}/.claude/skills/quarkus-grpc-services/SKILL.md`
|
|
96
98
|
6. `{project-root}/.claude/skills/quarkus-kafka-messaging/SKILL.md`
|
|
97
99
|
7. `{project-root}/.claude/skills/quarkus-observability-otel/SKILL.md`
|
|
100
|
+
8. `{project-root}/.claude/skills/quarkus-security-standards/SKILL.md`
|
|
101
|
+
9. `{project-root}/.claude/skills/quarkus-temporal-workflows/SKILL.md` — **only if the project uses Temporal** (`io.quarkiverse.temporal:quarkus-temporal` in a `pom.xml`, a `quarkus.temporal.*` property, or an `orchestration/` package). Skip it otherwise; it governs nothing in a project without workflows.
|
|
98
102
|
|
|
99
103
|
These are **sibling** skills installed flat at `.claude/skills/`, not nested under this one — a bare `skills/<name>/SKILL.md` path resolves from `{skill-root}`, finds nothing, and fails silently. If a file is missing at that path, try `{project-root}/src/bmm-skills/agents/bmad-quarkus-build/skills/<name>/SKILL.md` (pre-install staging layout) and use whichever resolves. If neither resolves, **say so before writing any code** rather than working from memory.
|
|
100
104
|
|
package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-error-handling-i18n/SKILL.md
CHANGED
|
@@ -75,7 +75,7 @@ Canonical rules in the quarkus-hexagonal-core skill; the error/i18n specifics:
|
|
|
75
75
|
| gRPC counterpart | `GrpcExceptionInterceptor`, in `common/error` | |
|
|
76
76
|
| i18n components | `MessageResolver`, `ErrorCatalog` | |
|
|
77
77
|
| Error DTO | `ErrorDto` (TMF630 Error shape) | |
|
|
78
|
-
| Bundle files | `messages/errors[_<lang>].properties` | `
|
|
78
|
+
| Bundle files | `messages/errors[_<lang>].properties` — the default (en) bundle is `errors.properties`, with no suffix | `errors.properties`, `errors_es.properties` |
|
|
79
79
|
| Bundle key | the error code itself, never a prose key | `USR-404-001` |
|
|
80
80
|
|
|
81
81
|
`GlobalExceptionHandler` is no longer "the only sanctioned `*Handler`" — under the slice standard `*Handler` is the business-logic class of every slice (`CreatePartyIndividualHandler`). What stays true is that there is exactly **one** global exception handler per transport, both in `common/error`, and that no `Resource`, `GrpcService` or `Consumer` builds an error response itself.
|
|
@@ -189,7 +189,11 @@ public class MessageResolver {
|
|
|
189
189
|
var locale = headers.getAcceptableLanguages().stream()
|
|
190
190
|
.findFirst().filter(l -> !"*".equals(l.getLanguage()))
|
|
191
191
|
.orElse(Locale.ENGLISH); // default locale = en
|
|
192
|
-
|
|
192
|
+
// getNoFallbackControl is REQUIRED: the default Control falls back to the JVM's
|
|
193
|
+
// default locale before the base bundle, so `Accept-Language: fr` on a pod with
|
|
194
|
+
// LANG=es returns Spanish instead of the English default.
|
|
195
|
+
var bundle = ResourceBundle.getBundle("messages.errors", locale,
|
|
196
|
+
ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_PROPERTIES));
|
|
193
197
|
var pattern = bundle.containsKey(code) ? bundle.getString(code)
|
|
194
198
|
: bundle.getString("GEN-500-001");
|
|
195
199
|
return MessageFormat.format(pattern, args);
|
|
@@ -140,6 +140,7 @@ Rules:
|
|
|
140
140
|
- Retries only for idempotent RPCs and only `UNAVAILABLE`/`DEADLINE_EXCEEDED`, with budget — prefer platform/mesh retry policy over hand-rolled loops.
|
|
141
141
|
- TLS/mTLS per platform standard (`quarkus.grpc.clients.*.ssl.*` / mesh-provided).
|
|
142
142
|
- Translate `StatusRuntimeException` into a `BusinessException` carrying the caller's own error code, inside the integration bean — gRPC types (and `Uni`) never reach a `Handler`. Read the upstream code from the `error-code` trailer and map it through the caller's `ErrorCatalog`.
|
|
143
|
+
- **Exception: a bean called from a Temporal activity rethrows the raw `StatusRuntimeException`.** Flattening it to `BusinessException` destroys the `Status.Code`, and that code is the only thing telling Temporal whether the failure is retryable — collapsed, an `UNAVAILABLE` blip becomes indistinguishable from an `INVALID_ARGUMENT` and would permanently fail the saga. The activity classifies it instead (see quarkus-temporal-workflows skill §9). Keep such a bean's activity-facing method separate from any `Handler`-facing one rather than changing behaviour based on the caller.
|
|
143
144
|
|
|
144
145
|
## Health & reflection
|
|
145
146
|
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: quarkus-hexagonal-core
|
|
3
|
-
description: Foundation for
|
|
3
|
+
description: Foundation for Java + Quarkus backend services compiled to native binaries (GraalVM/Mandrel) with VERTICAL SLICE architecture over a hexagonal core. Use when 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" or "new microservice". Canonical source for the SLICE FILE LAYOUT, CLASS NAMING CONVENTIONS, the domain-monorepo layout, deployable naming (`-ms` with an API, `-worker` without one), the mandatory per-app service.yaml + README standard including API gateway base paths, and the Handler contract (@Transactional only on process(), one Connection per business operation). Enforces Java 25, JDBC-only persistence via Agroal (NO Panache/ORM), ArchUnit slice-isolation + naming tests, and native-image compatibility. Project directives (CLAUDE.md, ADRs) override this where they conflict.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Quarkus Vertical Slice + Hexagonal Core (Native-First)
|
|
7
7
|
|
|
8
|
-
Foundation skill for all backend services. The other skills in this set (sql-jdbc-agroal
|
|
8
|
+
Foundation skill for all backend services. The other eight skills in this set (`quarkus-sql-jdbc-agroal`, `quarkus-error-handling-i18n`, `quarkus-openapi-tmforum`, `quarkus-grpc-services`, `quarkus-kafka-messaging`, `quarkus-observability-otel`, `quarkus-security-standards`, and `quarkus-temporal-workflows` on Temporal projects) build on the structure defined here and use the vocabulary defined here — apply this one first when scaffolding.
|
|
9
9
|
|
|
10
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
11
|
|
|
12
|
+
**One structural exception:** a **Temporal worker-only deployable** (`{module}-{capability}-worker`) — an app that hosts workflows and activities and exposes no API — has no vertical slices, no `Resource`, no `<Slice>Handler`/`<Slice>Sql` pair and usually no datasource. The naming rules, monorepo layout, native build, Dockerfile and the `service.yaml` + README standard below still apply to it in full; the slice layout, the REST adapter rules and the slice-oriented ArchUnit rules do not. See the quarkus-temporal-workflows skill §3 before scaffolding one — do not give it slices because this skill's checklist says so.
|
|
13
|
+
|
|
12
14
|
## The central idea: one feature = one folder
|
|
13
15
|
|
|
14
16
|
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.
|
|
@@ -48,6 +50,7 @@ One monorepo per domain (bounded context in DDD projects) holds **every deployab
|
|
|
48
50
|
│ │ ├── service.yaml # structured metadata — JSON-Schema validated in CI
|
|
49
51
|
│ │ └── README.md # per-app operational doc (see "Per-app documentation")
|
|
50
52
|
│ ├── iam-identity-ms/ # a second backend in the same domain, same internal structure
|
|
53
|
+
│ ├── iam-onboarding-worker/ # Temporal worker — no API, no slices (see temporal skill)
|
|
51
54
|
│ ├── iam-admin-mf/ # microfrontend remote — own build, same service.yaml + README rule
|
|
52
55
|
│ └── iam-shell-module/ # host/shell that composes the remotes
|
|
53
56
|
├── libs/ # code shared *within* this domain — never across domains
|
|
@@ -61,7 +64,7 @@ One monorepo per domain (bounded context in DDD projects) holds **every deployab
|
|
|
61
64
|
└── README.md # domain onboarding doc (see "Domain README")
|
|
62
65
|
```
|
|
63
66
|
|
|
64
|
-
**Scope of this skill inside the monorepo:** everything from "Slice layout" onward governs `-ms` apps only. `-mf` and `-module` apps are first-class residents of the same repo — they share `contracts/`, `libs/web/`, `deploy/` and `docs/`, and they carry the same mandatory `service.yaml` + `README.md` — but their internal structure is not this standard's business. A frontend never reaches a backend except through a published contract in `contracts/`; there is no shared runtime code between an `-ms` and an `-mf`.
|
|
67
|
+
**Scope of this skill inside the monorepo:** everything from "Slice layout" onward governs `-ms` apps only. A `-worker` app is a backend and follows this skill's naming, native build and documentation rules, but it has no slices — its internal structure belongs to the quarkus-temporal-workflows skill. `-mf` and `-module` apps are first-class residents of the same repo — they share `contracts/`, `libs/web/`, `deploy/` and `docs/`, and they carry the same mandatory `service.yaml` + `README.md` — but their internal structure is not this standard's business. A frontend never reaches a backend except through a published contract in `contracts/`; there is no shared runtime code between an `-ms` and an `-mf`.
|
|
65
68
|
|
|
66
69
|
### Deployable app naming (tenant-agnostic)
|
|
67
70
|
|
|
@@ -69,7 +72,9 @@ One monorepo per domain (bounded context in DDD projects) holds **every deployab
|
|
|
69
72
|
{module}-{service or submodule}-{type-suffix}
|
|
70
73
|
```
|
|
71
74
|
|
|
72
|
-
Suffixes: `-ms` (backend microservice —
|
|
75
|
+
Suffixes: `-ms` (backend microservice serving an API), `-worker` (backend deployable that serves **no** API — currently Temporal workflow workers; see the quarkus-temporal-workflows skill), `-mf` (microfrontend remote), `-module` (host/shell). Examples: `iam-management-ms`, `customer-onboarding-worker`.
|
|
76
|
+
|
|
77
|
+
`-ms` vs `-worker` is a deploy-time contract, not a label: an `-ms` gets an ingress, HTTP probes and RPS-based scaling; a `-worker` gets none of those and scales on queue backlog. An app that serves an API *and* hosts a worker is an `-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.
|
|
73
78
|
|
|
74
79
|
## Slice layout (the canonical structure)
|
|
75
80
|
|
|
@@ -179,7 +184,7 @@ Several slices may expose operations under the same TMF base path (`create_party
|
|
|
179
184
|
|
|
180
185
|
| Kind | Convention | Example |
|
|
181
186
|
|---|---|---|
|
|
182
|
-
| Deployable app | `{module}-{service or submodule}-{type}` kebab-case, tenant-agnostic (`-ms` backend) | `iam-management-ms` |
|
|
187
|
+
| Deployable app | `{module}-{service or submodule}-{type}` kebab-case, tenant-agnostic (`-ms` backend with an API, `-worker` backend without one) | `iam-management-ms`, `customer-onboarding-worker` |
|
|
183
188
|
| Slice package | lowercase `snake_case`, verb-first | `com.alva.customer.create_party_individual` |
|
|
184
189
|
| SQL constant | `UPPER_SNAKE_CASE` matching the method | `INSERT_PARTY`, `SELECT_PARTY_BY_ID` |
|
|
185
190
|
| SQL table / column | `snake_case`, singular table, schema-qualified | `customer.party`, `created_at` |
|
|
@@ -228,7 +233,7 @@ private void validate(CreatePartyIndividualRequestDto request) {
|
|
|
228
233
|
|
|
229
234
|
- 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.
|
|
230
235
|
- 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.
|
|
231
|
-
- Catches every checked `SQLException` and rethrows it as `BusinessException`
|
|
236
|
+
- Catches every checked `SQLException` and rethrows it **unchecked** via `SqlStateTranslator` — as a `BusinessException` for the states that mean a business conflict (`23505`, `23503`) and as the `PersistenceException` family for everything else (see the mapping table in quarkus-sql-jdbc-agroal skill §9). An unchecked exception is what triggers the container rollback; a propagated checked one would not.
|
|
232
237
|
- 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.
|
|
233
238
|
- Domain events are published by inserting an outbox row through `OutboxEventRecorder` from `common/messaging`, on this method's `Connection` and therefore 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.
|
|
234
239
|
- **The outbox is cross-cutting, not slice-local: never declare `insertOutboxEvent` on a `<Slice>Sql`.** It is the same table, the same columns and the same envelope in every slice, so the "duplicate twice, extract on the third" rule below does not apply — there is nothing slice-specific to discover. The shared `Connection` is what keeps the write transactional; the class's location is irrelevant to that guarantee. Same for `AuditRecorder` and any other table `common/` owns.
|
|
@@ -321,7 +326,7 @@ Full standard (pool config, batches, jsonb, pagination, keyset paging, SQLState
|
|
|
321
326
|
- 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`.
|
|
322
327
|
- 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()`.
|
|
323
328
|
- **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.
|
|
324
|
-
- Methods `throws SQLException` — they never swallow it and never translate it. Translation
|
|
329
|
+
- Methods `throws SQLException` — they never swallow it and never translate it. Translation happens in the Handler via `SqlStateTranslator`, into `BusinessException` or the `PersistenceException` family depending on the SQLState.
|
|
325
330
|
- **No transaction control here.** No `commit`, no `rollback`, no `setAutoCommit`.
|
|
326
331
|
- Row mapping is hand-written into DTOs (`PartyDto.builder()...`) — no reflection-based mappers, which also keeps native images small.
|
|
327
332
|
|
|
@@ -344,7 +349,7 @@ All three are thin: extract metadata, invoke `handler.process(...)`, format the
|
|
|
344
349
|
- `<Resource>Resource` class with `@Path` + `@Tag`, injecting the slice `Handler`(s).
|
|
345
350
|
- Returns `RestResponse<T>` synchronously. Never `Uni`/`Multi`.
|
|
346
351
|
- `@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 §8).
|
|
347
|
-
-
|
|
352
|
+
- Reads `tenantId` — and any other value that drives authorization, such as `partyId` or `partyRolList` — from the **validated token** (`@Inject JsonWebToken jwt` → `jwt.getClaim(...)`, or `@Claim` — not `SecurityIdentity`, which carries roles and mechanism attributes rather than claims), never from a request header the caller controls (see the Tenancy model rule under "Per-app documentation" and quarkus-security-standards skill §4). `language` and other non-security metadata may come from headers. It sets them on the request DTO before calling `process()`.
|
|
348
353
|
- Full OpenAPI annotations per method: `@Operation` with `operationId`, one `@APIResponse` per relevant status, `@Parameter` for headers and path/query params, `Location` header on create.
|
|
349
354
|
|
|
350
355
|
**gRPC** — full standard in **quarkus-grpc-services**:
|
|
@@ -394,10 +399,15 @@ Code shared across **apps** in the domain goes to `libs/` and uses constructor i
|
|
|
394
399
|
|
|
395
400
|
## Scaffolding checklist
|
|
396
401
|
|
|
397
|
-
When creating a new service
|
|
402
|
+
When creating a new service. A **Temporal worker-only deployable** (quarkus-temporal-workflows skill §3) follows the same steps with a reduced scope — it has no slices and no `Resource`:
|
|
398
403
|
|
|
399
|
-
|
|
400
|
-
|
|
404
|
+
- **Step 2 — add** `io.quarkiverse.temporal:quarkus-temporal` and `quarkus-grpc`; **keep** `quarkus-arc`, `quarkus-smallrye-health` and the three observability extensions; **drop** `quarkus-rest*`, `quarkus-smallrye-openapi`, `quarkus-hibernate-validator` and `quarkus-oidc`, and — unless it owns a table of its own — `quarkus-agroal`, `quarkus-jdbc-*`, `quarkus-narayana-jta` and `quarkus-flyway`.
|
|
405
|
+
- **Step 3 —** keep only the error/exception classes it actually throws.
|
|
406
|
+
- **Step 4 —** drop the datasource and Flyway blocks from the baseline below whenever step 2 dropped those extensions. **Leaving `quarkus.datasource.db-kind=postgresql` in place without a JDBC driver fails the Quarkus build** with "Unable to find a JDBC driver corresponding to the database kind". Add the `quarkus.temporal.*` block that skill's §2 defines instead.
|
|
407
|
+
- **Step 5 —** skip the slice-oriented rules and ship the ArchUnit subset that skill lists.
|
|
408
|
+
|
|
409
|
+
1. App folder `apps/<module>-<service-or-submodule>-ms/` per the monorepo layout above — `-worker` instead of `-ms` for a Temporal worker deployable.
|
|
410
|
+
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-micrometer-registry-prometheus` (mandatory metrics — brings `quarkus-micrometer` transitively) and `quarkus-logging-json` (mandatory structured logs) — the three observability extensions are all required, see that skill — `quarkus-oidc` wherever the app exposes an authenticated API (security skill §4), `quarkus-flyway` (kept even where migrations are disabled — see sql skill §10), plus Lombok and skill-specific extensions as needed.
|
|
401
411
|
3. `common/` package: `BusinessException`, `ErrorCatalog`, `ErrorDto`, `GlobalExceptionHandler`, `MessageResolver`, `SqlStateTranslator`, `StringUtils`.
|
|
402
412
|
4. `application.properties` baseline (below).
|
|
403
413
|
5. ArchUnit test (below) — in the same commit as the first slice, not later.
|
|
@@ -434,8 +444,12 @@ quarkus.flyway.migrate-at-start=false
|
|
|
434
444
|
app.messaging.outbox.relay-mode=debezium
|
|
435
445
|
app.messaging.outbox.poller.every=off
|
|
436
446
|
|
|
447
|
+
# Metrics — Micrometer is mandatory in every service (see quarkus-observability-otel skill)
|
|
448
|
+
quarkus.micrometer.export.prometheus.enabled=true
|
|
449
|
+
quarkus.datasource.metrics.enabled=true
|
|
450
|
+
|
|
437
451
|
# i18n (see error-handling skill) — locales must be declared for native
|
|
438
|
-
quarkus.locales=en,es
|
|
452
|
+
quarkus.locales=en,es,pt
|
|
439
453
|
quarkus.default-locale=en
|
|
440
454
|
quarkus.native.resources.includes=messages/*.properties
|
|
441
455
|
|
|
@@ -484,13 +498,13 @@ Rules:
|
|
|
484
498
|
|
|
485
499
|
Every deployable under `apps/<app>/` carries two files, versioned with the code and updated in the **same PR** that changes the service's behavior:
|
|
486
500
|
|
|
487
|
-
- **`service.yaml`** — structured metadata validated against a JSON Schema in CI: identity (domain, system, component type, lifecycle), ownership (team, SRE, support group), criticality, tenancy (model, data isolation), runtime (type, version, ports), provided APIs with their contract files in `contracts/`, dependencies with `hard`/`soft` requirement, and the observability contract (log format, levels, required fields, data-protection rules). It feeds the service catalog and the
|
|
501
|
+
- **`service.yaml`** — structured metadata validated against a JSON Schema in CI: identity (domain, system, component type, lifecycle), **the API gateway base path(s) under `metadata.gateway`** (see "Gateway routing" below), ownership (team, SRE, support group), criticality, tenancy (model, data isolation), runtime (type, version, ports), provided APIs with their contract files in `contracts/`, dependencies with `hard`/`soft` requirement, and the observability contract (log format, levels, required fields, data-protection rules). It feeds the service catalog, the dependency map and the gateway route configuration.
|
|
488
502
|
- **`README.md`** (per app) — operational doc written so a system administrator who has never seen the code can configure, deploy and recover the service. No `TBD` / "see code" (a justified `N/A` is allowed). No manual changelog section — Git keeps the history.
|
|
489
503
|
|
|
490
504
|
Required `README.md` sections:
|
|
491
505
|
|
|
492
506
|
1. **Identity** — app name, module (`part-of`), type (`ms`), owning team, repository, criticality, part of core yes/no
|
|
493
|
-
2. **Purpose &
|
|
507
|
+
2. **Purpose, responsibility & gateway routes** — description + out of scope, **plus the API gateway base path(s) this service is published under** (see "Gateway routing" below). A `-worker` app declares `N/A — publishes no API`.
|
|
494
508
|
3. **Slice inventory** — table: slice folder, capability, exposed channels (REST/gRPC/Kafka), link to the slice README
|
|
495
509
|
4. **Tenancy model** — `shared` | `per-tenant` | `pool`; tenant identification (validated JWT claim, never a free header); data isolation; per-tenant parameters
|
|
496
510
|
5. **APIs exposed** — table: api/version, protocol, route/topic, contract file, auth, visibility
|
|
@@ -509,6 +523,39 @@ Required `README.md` sections:
|
|
|
509
523
|
|
|
510
524
|
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.
|
|
511
525
|
|
|
526
|
+
### Gateway routing (README §2 + `service.yaml` `metadata.gateway`)
|
|
527
|
+
|
|
528
|
+
Infra configures the API gateway from these two files, so the route a service will be published under is declared **before** it is deployed, in the same PR as the endpoint.
|
|
529
|
+
|
|
530
|
+
**The base path is the module's semantic name**, the same name packages, topics, config keys and metrics already use: `/iam`, `/customer`, `/wallet` — **never the inventory code** (`/bc01` is wrong; the `bcNN` ↔ name mapping lives in governance docs, not in a URL). Every north-bound REST API of every `-ms` in the module therefore hangs off one shared prefix, and the API's own path continues from there per the quarkus-openapi-tmforum skill: `/{module}/{context}/{apiName}/v{major}`, e.g. `/iam/tmf-api/digitalIdentityManagement/v4`.
|
|
531
|
+
|
|
532
|
+
README §2 carries the human-readable table:
|
|
533
|
+
|
|
534
|
+
| Base path | Protocol | Visibility | Purpose |
|
|
535
|
+
|---|---|---|---|
|
|
536
|
+
| `/iam` | REST | public | north-bound TMF identity APIs |
|
|
537
|
+
|
|
538
|
+
`service.yaml` carries the same facts as structured metadata:
|
|
539
|
+
|
|
540
|
+
```yaml
|
|
541
|
+
metadata:
|
|
542
|
+
name: iam-management-ms
|
|
543
|
+
module: iam
|
|
544
|
+
gateway:
|
|
545
|
+
base-paths:
|
|
546
|
+
- path: /iam
|
|
547
|
+
protocol: REST
|
|
548
|
+
visibility: public # public | partner | internal
|
|
549
|
+
purpose: north-bound TMF identity APIs
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
Rules:
|
|
553
|
+
- **`service.yaml` is authoritative; the README renders the same facts for humans.** They must agree — two copies of one fact drift, and here the drift is a misrouted service. CI cross-checks them.
|
|
554
|
+
- **Only north-bound REST appears here.** Internal gRPC never crosses the gateway (internal calls are gRPC, external is REST — see "Baseline constraints"), so a `*GrpcService` contributes no gateway route.
|
|
555
|
+
- **A `-worker` app declares none** — `metadata.gateway.base-paths: []` and `N/A` in the README. It publishes no API by definition (see the quarkus-temporal-workflows skill).
|
|
556
|
+
- List **every** prefix when a service is published under more than one — a legacy alias kept during a migration, or a second public API — each with its own purpose, so nobody has to guess which is being retired.
|
|
557
|
+
- The declared path must match the ingress/gateway configuration in the GitOps repo. A README that disagrees with what is deployed is a defect, not a stale doc: it is the input infra works from.
|
|
558
|
+
|
|
512
559
|
### Slice README (inside the slice folder)
|
|
513
560
|
|
|
514
561
|
Every slice carries its own `README.md`, generated from the real code — no invented routes, fields or tables. Sections:
|
|
@@ -547,7 +594,10 @@ class ArchitectureTest {
|
|
|
547
594
|
.namingSlices("slice $1")
|
|
548
595
|
.should().notDependOnEachOther()
|
|
549
596
|
.ignoreDependency(resideInAPackage("..common.."), alwaysTrue())
|
|
550
|
-
.ignoreDependency(alwaysTrue(), resideInAPackage("..common.."))
|
|
597
|
+
.ignoreDependency(alwaysTrue(), resideInAPackage("..common.."))
|
|
598
|
+
// Temporal only: orchestration/ may reach into slices, never the reverse (deliberately
|
|
599
|
+
// asymmetric — see quarkus-temporal-workflows skill §3). Omit on non-Temporal projects.
|
|
600
|
+
.ignoreDependency(resideInAPackage("..orchestration.."), alwaysTrue());
|
|
551
601
|
|
|
552
602
|
// --- Hexagon dependency rule inside the slice ------------------------------
|
|
553
603
|
@ArchTest
|
|
@@ -646,7 +696,9 @@ static final ArchRule businessExceptionIsTheOnlyOne = classes()
|
|
|
646
696
|
.should().resideInAPackage("..common.exception..");
|
|
647
697
|
|
|
648
698
|
@ArchTest
|
|
649
|
-
static final ArchRule bannedSuffixes = noClasses()
|
|
699
|
+
static final ArchRule bannedSuffixes = noClasses()
|
|
700
|
+
.that().resideOutsideOfPackage("..orchestration..") // Temporal workflow/activity Impl pair — see below
|
|
701
|
+
.should().haveSimpleNameEndingWith("Impl")
|
|
650
702
|
.orShould().haveSimpleNameEndingWith("Util") // note: "Utils" is deliberately absent — common/util/StringUtils
|
|
651
703
|
.orShould().haveSimpleNameEndingWith("Helper")
|
|
652
704
|
.orShould().haveSimpleNameEndingWith("Manager")
|
|
@@ -657,7 +709,7 @@ static final ArchRule bannedSuffixes = noClasses().should().haveSimpleNameEnding
|
|
|
657
709
|
.orShould().haveSimpleNameEndingWith("DTO");
|
|
658
710
|
```
|
|
659
711
|
|
|
660
|
-
`common/util/StringUtils
|
|
712
|
+
`common/util/StringUtils`, the `Jdbc` namespace helper (sql skill §7) and, on Temporal projects, the `*WorkflowImpl`/`*ActivitiesImpl` pair inside `..orchestration..` (whose interface+implementation split the SDK requires — see quarkus-temporal-workflows skill §3) are the sanctioned exceptions to `bannedSuffixes`. Exclude them by package or fully-qualified name, as above, rather than weakening the rule.
|
|
661
713
|
|
|
662
714
|
Rules the compiler cannot express (enforce in code review and in the generator checklists):
|
|
663
715
|
|
|
@@ -686,7 +738,7 @@ Rules the compiler cannot express (enforce in code review and in the generator c
|
|
|
686
738
|
|
|
687
739
|
`<Slice>HandlerTest` is mandatory and mirrors the slice. Rules:
|
|
688
740
|
|
|
689
|
-
- Mockito only — `@Mock <Slice>Sql sql;`, `@Mock DataSource dataSource;`, `@Mock Connection connection;`, `@InjectMocks <Slice>Handler handler
|
|
741
|
+
- Mockito only — `@Mock <Slice>Sql sql;`, `@Mock DataSource dataSource;`, `@Mock Connection connection;`, `@InjectMocks <Slice>Handler handler;`. Stub `when(dataSource.getConnection()).thenReturn(connection)` **inside the tests that reach `execution()`**, not in `@BeforeEach`: the mandatory validation-failure test (scenario 2 below) never opens a connection, so a shared stub fails it with Mockito's `UnnecessaryStubbingException` under the default strict stubs. Use `lenient()` only if you genuinely prefer the shared setup. Never a real database, never `@QuarkusTest` for pure business rules, never both `@InjectMocks` (Mockito) and `@InjectMock` (Quarkus) in one file.
|
|
690
742
|
- Test names: `should<Result>When<Condition>` — `shouldReturnPartyIdWhenRequestIsValid`, `shouldThrowBusinessExceptionWhenGivenNameIsBlank`.
|
|
691
743
|
- Minimum scenarios per public business path:
|
|
692
744
|
|
|
@@ -130,7 +130,9 @@ That is the whole dual-write guarantee — one connection, one transaction, no p
|
|
|
130
130
|
Fire-and-forget technical messages (metrics, notifications with no consistency requirement) MAY use a direct `Emitter` — but it lives in a **`common/` capability bean**, never in a slice. `@Channel`/`Emitter` are `org.eclipse.microprofile.reactive.messaging` types, and the `handlerKnowsNoTransport` ArchUnit rule forbids them in a `Handler`:
|
|
131
131
|
|
|
132
132
|
```java
|
|
133
|
-
// common/audit/
|
|
133
|
+
// common/audit/AuditEventPublisher.java — the Handler injects this bean, not the Emitter.
|
|
134
|
+
// Distinct from AuditRecorder (common/audit), which writes the local audit_event table
|
|
135
|
+
// on the Handler's Connection — see quarkus-observability-otel skill.
|
|
134
136
|
@Inject @Channel("audit-out") Emitter<AuditEventPayload> audit;
|
|
135
137
|
```
|
|
136
138
|
|
|
@@ -314,12 +316,12 @@ mp.messaging.incoming.user-registered-in.connector=smallrye-kafka
|
|
|
314
316
|
mp.messaging.incoming.user-registered-in.topic=alva.iam.user.registered.v1
|
|
315
317
|
mp.messaging.incoming.user-registered-in.group.id=${quarkus.application.name}
|
|
316
318
|
mp.messaging.incoming.user-registered-in.auto.offset.reset=earliest
|
|
317
|
-
mp.messaging.incoming.user-registered-in.failure-strategy=
|
|
319
|
+
mp.messaging.incoming.user-registered-in.failure-strategy=dead-letter-queue
|
|
318
320
|
mp.messaging.incoming.user-registered-in.dead-letter-queue.topic=alva.iam.user.registered.v1.dlq
|
|
319
321
|
```
|
|
320
322
|
|
|
321
323
|
Rules:
|
|
322
|
-
- **Idempotency is mandatory**: `processed_event(consumer_group, event_id)` checked/inserted through the
|
|
324
|
+
- **Idempotency is mandatory**: `processed_event(consumer_group, event_id)` checked/inserted through the cross-cutting `ProcessedEventSql` in `common/messaging` — never a per-slice copy, it is the same table for every slice (see quarkus-sql-jdbc-agroal skill §6) — on the `Connection` the `Handler` already owns, in the same transaction as the side effects. Kafka is at-least-once; duplicates WILL happen.
|
|
323
325
|
- Consumer group = service name; stable across deployments.
|
|
324
326
|
- Failure strategy: bounded retries with delay, then DLQ topic `<topic>.dlq`. DLQ messages keep original headers plus `dead-letter-reason`. A DLQ must have an owner and an alert — never a silent graveyard.
|
|
325
327
|
- Poison-pill safety: deserialization failures also route to DLQ (`...deserialization-failure-handler` or failure strategy), never block the partition.
|