bmad-method-quarkus 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/package.json +1 -1
  2. package/removals.txt +10 -0
  3. package/src/bmm-skills/agents/bmad-quarkus-build/SKILL.md +19 -16
  4. package/src/bmm-skills/agents/bmad-quarkus-build/customize.toml +7 -5
  5. package/src/bmm-skills/agents/bmad-quarkus-build/skills/bqa-setup/assets/module-help.csv +0 -1
  6. package/src/bmm-skills/agents/bmad-quarkus-build/skills/bqa-setup/assets/module.yaml +2 -9
  7. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-hexagonal-core/SKILL.md +30 -10
  8. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-kafka-messaging/SKILL.md +163 -15
  9. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-sql-jdbc-agroal/SKILL.md +37 -4
  10. package/src/bmm-skills/module.yaml +0 -7
  11. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/SKILL.md +0 -108
  12. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/customize.toml +0 -62
  13. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/references/architecture-review.md +0 -17
  14. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/references/prompt-quality-canon.md +0 -79
  15. package/src/bmm-skills/agents/bmad-quarkus-dev/.memlog.md +0 -12
  16. package/src/bmm-skills/agents/bmad-quarkus-dev/.memlog.md:Zone.Identifier +0 -0
  17. package/src/bmm-skills/agents/bmad-quarkus-dev/SKILL.md +0 -86
  18. package/src/bmm-skills/agents/bmad-quarkus-dev/SKILL.md:Zone.Identifier +0 -0
  19. package/src/bmm-skills/agents/bmad-quarkus-dev/customize.toml +0 -37
  20. package/src/bmm-skills/agents/bmad-quarkus-dev/customize.toml:Zone.Identifier +0 -0
  21. package/src/bmm-skills/agents/bmad-quarkus-dev/references/enrich-stories.md +0 -19
  22. package/src/bmm-skills/agents/bmad-quarkus-dev/references/enrich-stories.md:Zone.Identifier +0 -0
  23. package/src/bmm-skills/agents/bmad-quarkus-dev/references/prompt-quality-canon.md +0 -79
  24. package/src/bmm-skills/agents/bmad-quarkus-dev/references/prompt-quality-canon.md:Zone.Identifier +0 -0
  25. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/SKILL.md +0 -80
  26. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/SKILL.md:Zone.Identifier +0 -0
  27. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module-help.csv +0 -9
  28. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module-help.csv:Zone.Identifier +0 -0
  29. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module.yaml +0 -16
  30. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module.yaml:Zone.Identifier +0 -0
  31. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/cleanup-legacy.py +0 -287
  32. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/cleanup-legacy.py:Zone.Identifier +0 -0
  33. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-config.py +0 -441
  34. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-config.py:Zone.Identifier +0 -0
  35. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-help-csv.py +0 -246
  36. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-help-csv.py:Zone.Identifier +0 -0
  37. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md +0 -215
  38. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md:Zone.Identifier +0 -0
  39. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md +0 -160
  40. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md:Zone.Identifier +0 -0
  41. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md +0 -661
  42. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md:Zone.Identifier +0 -0
  43. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md +0 -165
  44. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md:Zone.Identifier +0 -0
  45. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md +0 -196
  46. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md:Zone.Identifier +0 -0
  47. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md +0 -145
  48. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md:Zone.Identifier +0 -0
  49. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md +0 -275
  50. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md:Zone.Identifier +0 -0
@@ -1,215 +0,0 @@
1
- ---
2
- name: quarkus-error-handling-i18n
3
- description: Unified (single) global exception handler and internationalized (i18n) API responses for Quarkus backend services, aligned to TM Forum error schema (TMF630). Use this skill whenever the user mentions exception handling, error responses, ExceptionMapper, error codes, Problem Details, localization, i18n, translations, Accept-Language, message bundles, or when creating/reviewing ANY REST endpoint that can fail — errors and locale handling must always go through this standard, never ad-hoc try/catch in resources. Also defines the single BusinessException type (code + args, never a pre-rendered message), the ErrorCatalog/MessageResolver machinery in common/, and the English-identifiers/localized-messages rule.
4
- ---
5
-
6
- # Unified Exception Handler + i18n Responses (Quarkus, TMF-aligned)
7
-
8
- One handler, one error contract, localized messages resolved from the `Accept-Language` header. No endpoint ever builds its own error response.
9
-
10
- Applies to any Quarkus backend project. Defaults below (locale set, error-code format) are overridable by project directives (CLAUDE.md, ADRs, explicit instructions) — the single-handler and stable-code principles are the part to defend.
11
-
12
- ## Error contract (TM Forum TMF630 Error schema)
13
-
14
- All error responses use `application/json` with the TMF Error structure (interoperable with TMF Open APIs; if the project is not TMF-facing, RFC 9457 `application/problem+json` is the fallback — same handler, different DTO):
15
-
16
- ```json
17
- {
18
- "code": "USR-404-001",
19
- "reason": "Not Found",
20
- "message": "User 8f14e45f does not exist.",
21
- "status": "404",
22
- "referenceError": "https://docs.company.com/errors/USR-404-001",
23
- "@type": "Error"
24
- }
25
- ```
26
-
27
- - `code`: stable, machine-readable, carried by the thrown `BusinessException`, format `<MOD>-<HTTP>-<seq>` where `<MOD>` is a short uppercase code derived from the semantic module name or its main entity (`IAM`, `USR`, `PTY` — never a `bcNN` inventory code; see quarkus-hexagonal-core skill). It is also the **bundle key**. Never reworded, never localized.
28
- - `message`: human-readable, **localized** via bundles.
29
- - `referenceError`: link to error catalog docs.
30
- - Add `traceId` propagation via response header `X-Trace-Id` (see observability skill), not in the body.
31
-
32
- ## One business exception (`common/exception`)
33
-
34
- The vertical-slice standard uses a **single** business exception carrying a stable code plus interpolation arguments — not a subclass per error. The subclass hierarchy bought nothing that the code does not already express, and cost one file per error:
35
-
36
- ```java
37
- // common/exception
38
- public class BusinessException extends RuntimeException {
39
- private final String code; // "USR-404-001"
40
- private final Object[] messageArgs; // interpolated into the localized message
41
- public BusinessException(String code, Object... args) {
42
- super(code); // super message = the code; the human text is resolved at the edge
43
- this.code = code;
44
- this.messageArgs = args;
45
- }
46
- public BusinessException(String code, Throwable cause, Object... args) { ... }
47
- public String code() { return code; }
48
- public Object[] messageArgs() { return messageArgs; }
49
- }
50
- ```
51
-
52
- Thrown from the slice `Handler` and nowhere else:
53
-
54
- ```java
55
- throw new BusinessException("USR-404-001", userId);
56
- ```
57
-
58
- **The thrower never formats or translates the message.** A `Handler` that injects a `ResourceBundle` and passes a rendered string silently pins every response to one language, defeating `Accept-Language`. Resolution happens once, at the edge, in `GlobalExceptionHandler` / `GrpcExceptionInterceptor`.
59
-
60
- HTTP status is not known by the `Handler` either. The mapping code→status lives in `ErrorCatalog` (`common/error`).
61
-
62
- One sibling, not a subclass: `StaleVersionException` (optimistic-lock conflict) is the only other code-carrying exception. It is NOT a `BusinessException` subtype and NOT part of the `PersistenceException` family — a version conflict is a client-actionable 409, not an infrastructure failure hidden behind a 500. It is thrown by the `Handler` when a versioned `UPDATE` returns 0 rows (never by `SqlStateTranslator` — there is no `SQLException` involved) and mapped by its own dedicated mapper below.
63
-
64
- ### Naming
65
-
66
- Canonical rules in the quarkus-hexagonal-core skill; the error/i18n specifics:
67
-
68
- | Artifact | Convention | Example |
69
- |---|---|---|
70
- | Business exception | `BusinessException` — one class, in `common/exception` | thrown with `("USR-404-001", id)` |
71
- | Technical exceptions | `PersistenceException`, `TransientPersistenceException`, `PersistenceTimeoutException`, in `common/exception` | |
72
- | Concurrency-conflict exception | `StaleVersionException` — standalone, code-carrying (`code + args`, like `BusinessException`), in `common/exception` | thrown with `("PTY-409-001", id)` |
73
- | SQLState translator | `SqlStateTranslator`, in `common/error` | |
74
- | Single REST handler | `GlobalExceptionHandler`, in `common/error` | |
75
- | gRPC counterpart | `GrpcExceptionInterceptor`, in `common/error` | |
76
- | i18n components | `MessageResolver`, `ErrorCatalog` | |
77
- | Error DTO | `ErrorDto` (TMF630 Error shape) | |
78
- | Bundle files | `messages/errors[_<lang>].properties` | `errors_en.properties` |
79
- | Bundle key | the error code itself, never a prose key | `USR-404-001` |
80
-
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.
82
-
83
- Never name an exception after the HTTP status (`NotFoundException`, `BadRequestException`) — the status is resolved by `ErrorCatalog`. Never shadow a JDK type (`ConcurrentModificationException` → `StaleVersionException`).
84
-
85
- **Language rule:** class, method and field names are English; the *messages* are localized per bundle, with **English as the default locale** and other languages (es, pt, ...) added per project need. A slice named `create_party_individual` throwing `PTY-400-001`, translated as `El campo ''{0}'' es obligatorio.` in `errors_es.properties`, is correct — a class or key named `crear_persona.campo_obligatorio` is not.
86
-
87
- ## The single global handler (`common/error`)
88
-
89
- Exactly ONE class. Use `@ServerExceptionMapper` (Quarkus REST) — build-time wired, native-friendly:
90
-
91
- ```java
92
- @ApplicationScoped // required for constructor injection into the mapper class
93
- public class GlobalExceptionHandler {
94
-
95
- private final MessageResolver messages; // i18n component below
96
- private final ErrorCatalog catalog; // code -> HTTP status + reason
97
-
98
- public GlobalExceptionHandler(MessageResolver messages, ErrorCatalog catalog) {
99
- this.messages = messages;
100
- this.catalog = catalog;
101
- }
102
-
103
- @ServerExceptionMapper
104
- public RestResponse<ErrorDto> map(BusinessException e, HttpHeaders headers) {
105
- var status = catalog.statusOf(e.code()); // e.g. 404
106
- var msg = messages.resolve(e.code(), headers, e.messageArgs());
107
- return RestResponse.status(status,
108
- ErrorDto.of(e.code(), status, msg));
109
- }
110
-
111
- @ServerExceptionMapper
112
- public RestResponse<ErrorDto> map(StaleVersionException e, HttpHeaders headers) {
113
- // optimistic-lock conflict: 409 per catalog, client-visible and localized like a business error
114
- var status = catalog.statusOf(e.code()); // 409
115
- var msg = messages.resolve(e.code(), headers, e.messageArgs());
116
- return RestResponse.status(status, ErrorDto.of(e.code(), status, msg));
117
- }
118
-
119
- @ServerExceptionMapper
120
- public RestResponse<ErrorDto> map(PersistenceException e, HttpHeaders headers) {
121
- // technical failures: 500/503 per catalog, stack trace to logs only
122
- ...
123
- }
124
-
125
- @ServerExceptionMapper
126
- public RestResponse<ErrorDto> map(ConstraintViolationException e, HttpHeaders headers) {
127
- // 400, code "GEN-400-001", join violations into localized detail
128
- ...
129
- }
130
-
131
- @ServerExceptionMapper
132
- public RestResponse<ErrorDto> map(Exception e, HttpHeaders headers) {
133
- Log.error("Unhandled exception", e); // full stack only in logs
134
- var msg = messages.resolve("GEN-500-001", headers);
135
- return RestResponse.status(500, ErrorDto.of("GEN-500-001", 500, msg));
136
- }
137
- }
138
- ```
139
-
140
- Rules:
141
- - Catch-all `Exception` mapper is mandatory: internal details/stack traces NEVER reach the client. Log with full context (trace_id lands in the log automatically via MDC).
142
- - Validation errors (Bean Validation) map to 400 with one generic code + per-field details array.
143
- - `Resource`, `GrpcService`, `Consumer` and `Handler` classes must NOT catch-and-format errors themselves — throw `BusinessException` with a code and let the handler translate. A `try { ... } catch (BusinessException e) { return Response.status(400)... }` inside a `Resource` is the anti-pattern this skill exists to prevent: it bypasses the catalog, the locale and the trace header.
144
- - `ErrorDto` is `@RegisterForReflection` (native).
145
-
146
- ## i18n resolution from Accept-Language
147
-
148
- ### Configuration
149
-
150
- ```properties
151
- quarkus.locales=en,es,pt
152
- quarkus.default-locale=en
153
- ```
154
-
155
- `quarkus.locales` also tells the native image which locale data to embed — required for native builds.
156
-
157
- ### Message bundles
158
-
159
- `src/main/resources/messages/errors.properties` (default = **en**), `errors_es.properties`, `errors_pt.properties`:
160
-
161
- ```properties
162
- # errors.properties (en, default)
163
- USR-404-001=User {0} does not exist.
164
- PTY-400-001=The ''{0}'' field is required.
165
- GEN-400-001=The request contains invalid data.
166
- GEN-500-001=Internal error. Contact support with the trace identifier.
167
- ```
168
-
169
- Bundles are **per application, keyed by error code** — not per slice, not keyed by prose. A slice contributes rows to the same files, which is what keeps "every code exists in every locale" a single testable invariant. (`''` escapes a literal apostrophe for `MessageFormat`.)
170
-
171
- ```properties
172
- # errors_es.properties
173
- USR-404-001=El usuario {0} no existe.
174
- PTY-400-001=El campo ''{0}'' es obligatorio.
175
- GEN-400-001=La solicitud contiene datos inválidos.
176
- GEN-500-001=Error interno. Contacte a soporte con el identificador de traza.
177
- ```
178
-
179
- Quality bar per language — Spanish: formal or infinitive, precise, no vague `Error de validación`. English: direct declarative, no vague `something went wrong`. Same key order in every file so diffs stay readable.
180
-
181
- For native: `quarkus.native.resources.includes=messages/*.properties`.
182
-
183
- ### Resolver
184
-
185
- ```java
186
- @ApplicationScoped
187
- public class MessageResolver {
188
- public String resolve(String code, HttpHeaders headers, Object... args) {
189
- var locale = headers.getAcceptableLanguages().stream()
190
- .findFirst().filter(l -> !"*".equals(l.getLanguage()))
191
- .orElse(Locale.ENGLISH); // default locale = en
192
- var bundle = ResourceBundle.getBundle("messages.errors", locale);
193
- var pattern = bundle.containsKey(code) ? bundle.getString(code)
194
- : bundle.getString("GEN-500-001");
195
- return MessageFormat.format(pattern, args);
196
- }
197
- }
198
- ```
199
-
200
- `getAcceptableLanguages()` already returns the header parsed and sorted by q-value — take the first supported one. Unknown locales fall back to the default silently (never 406 for language).
201
-
202
- Alternative: `quarkus-messages` / SmallRye localized message bundles (`@MessageBundle`) give compile-time checked bundles — prefer it in greenfield projects; the `ResourceBundle` approach above is the portable baseline.
203
-
204
- ## i18n beyond errors
205
-
206
- Success-path localizable strings (notification texts, report labels) use the same resolver and bundles (`messages/app*.properties`). Data itself (entity fields) is NOT localized by this mechanism — that is a modeling concern.
207
-
208
- ## Checklist when adding a new error
209
-
210
- 1. Pick the next code `<MOD>-<HTTP>-<seq>` — no new exception class.
211
- 2. Throw `new BusinessException(code, args...)` from the slice `Handler` (`validate()` or `execution()`), with the interpolation args and no rendered text.
212
- 3. Register code → status/reason in `ErrorCatalog`.
213
- 4. Add the message key to ALL locale bundles (a test fails the build if a code is missing in any bundle — write it once: iterate catalog codes × locales).
214
- 5. Document in the error catalog page referenced by `referenceError`, and add the row to the slice `README.md` "Business errors" table.
215
- 6. Assert the code (not the message) in the `HandlerTest`; assert the localized body in the `Resource` `@QuarkusTest`.
@@ -1,160 +0,0 @@
1
- ---
2
- name: quarkus-grpc-services
3
- description: Standard for internal microservice-to-microservice communication using gRPC in Quarkus native services — proto conventions, Mutiny services, clients, deadlines, error mapping to BusinessException codes, health checks, and native-image setup. Use this skill whenever the user mentions gRPC, protobuf, .proto files, internal service calls, synchronous inter-service communication, service clients, or "call service X from service Y" — internal synchronous calls are ALWAYS gRPC, never internal REST. Includes proto and gRPC adapter/client naming conventions for the vertical-slice layout (the GrpcService lives in the slice folder and delegates to the slice Handler; outbound stubs are wrapped by capability-named beans in common/client).
4
- ---
5
-
6
- # gRPC Standard for Inter-Service Communication (Quarkus)
7
-
8
- Rule of the road: **external/north-bound = REST (TMF)**, **internal synchronous service-to-service = gRPC**, **internal asynchronous = Kafka**. If someone proposes an internal REST client, redirect to this skill. Applies to any Quarkus backend project; project directives (CLAUDE.md, ADRs, explicit instructions) override these defaults.
9
-
10
- Extension: `quarkus-grpc` (fully native-compatible; code generation is build-time).
11
-
12
- ## Proto conventions
13
-
14
- Location: `src/main/proto/<module>/v<major>/`, one file per capability group, published to the monorepo `contracts/` folder. One proto package per service/module + major version (the module segment is the module's semantic name — `iam`, `customer` — see quarkus-hexagonal-core skill, or the service name):
15
-
16
- ```protobuf
17
- syntax = "proto3";
18
- package alva.iam.v1;
19
- option java_package = "com.alva.iam.grpc.v1";
20
- option java_multiple_files = true;
21
-
22
- service UserService {
23
- rpc GetUser (GetUserRequest) returns (GetUserResponse);
24
- rpc ValidateCredentials (ValidateCredentialsRequest) returns (ValidateCredentialsResponse);
25
- }
26
-
27
- message GetUserRequest { string user_id = 1; }
28
- message GetUserResponse { User user = 1; }
29
- ```
30
-
31
- Rules:
32
- - Package version `v1`, `v2` — breaking change = new package version, run both during migration.
33
- - `snake_case` fields, `PascalCase` messages/services, one `XxxRequest`/`XxxResponse` pair per RPC (never reuse messages across RPCs — they evolve independently).
34
- - Never reuse/renumber field tags; use `reserved` for removed fields.
35
- - Timestamps: `google.protobuf.Timestamp`; optional scalars needing presence: `optional` keyword or wrappers.
36
- - Proto files are contracts: keep them in a shared `contracts/` module (or a dedicated repo) consumed by both sides; do NOT copy-paste protos between services.
37
- - Streaming RPCs only with a justified use case (bulk export, watch); default is unary.
38
-
39
- ## Server (inbound adapter, in the slice folder)
40
-
41
- ```java
42
- @GrpcService
43
- public class UserGrpcService implements UserService { // Mutiny-generated interface
44
-
45
- @Inject FindUserHandler findUser; // the slice's Handler, blocking
46
-
47
- @Override
48
- public Uni<GetUserResponse> getUser(GetUserRequest req) {
49
- return Uni.createFrom().item(() -> {
50
- var request = FindUserRequestDto.builder().userId(req.getUserId()).build();
51
- var result = findUser.process(request); // blocking JDBC
52
- return GetUserResponse.newBuilder()
53
- .setUser(User.newBuilder().setId(result.getId()).setEmail(result.getEmail()))
54
- .build();
55
- })
56
- .runSubscriptionOn(Infrastructure.getDefaultWorkerPool()); // JDBC → worker thread
57
- }
58
- }
59
- ```
60
-
61
- - gRPC service = thin adapter: convert proto ↔ DTO, delegate to the slice `Handler`. No business logic, no JDBC, no `Connection`.
62
- - **Proto ↔ DTO conversion happens inline, inside the `item(() -> ...)` lambda.** There is no `*GrpcMapper` class — `*Mapper` is a banned suffix (see quarkus-hexagonal-core skill).
63
- - This class and the integration beans in `common/client` are the **only** two places Mutiny is allowed; the ArchUnit rule `reactiveIsQuarantined` enforces it. Never let `Uni`/`Multi` reach a `Handler` or `Sql`.
64
- - Blocking work (JDBC) either `@Blocking` on the method or the explicit worker pool as above.
65
-
66
- ### Naming
67
-
68
- Canonical rules in the quarkus-hexagonal-core skill; gRPC specifics:
69
-
70
- | Artifact | Convention | Example |
71
- |---|---|---|
72
- | Proto service | `<Entity>Service` (PascalCase) | `UserService` |
73
- | Proto RPC | `<Verb><Entity>` | `GetUser`, `ValidateCredentials` |
74
- | Proto messages | `<Rpc>Request` / `<Rpc>Response`, one pair per RPC | `GetUserRequest` |
75
- | Proto fields | `snake_case` | `user_id` |
76
- | Server adapter | `<ProtoService>GrpcService`, in the slice folder | `UserGrpcService` |
77
- | Outbound integration bean | capability noun, **no technology**, in `common/client` | `CredentialsValidator` |
78
- | Global error interceptor | `GrpcExceptionInterceptor`, in `common/error` | |
79
- | Client config key | `<module>-<entity>` kebab-case, matching `@GrpcClient` | `iam-users` |
80
-
81
- `*GrpcService` is the **only** class allowed to end in `Service` under the slice standard — it mirrors the proto service name, which is not ours to rename (ArchUnit rule `serviceSuffixIsReserved`). There is no `Grpc<Port>` client adapter and no `*GrpcMapper`: the outbound bean is named after the capability it provides, and only its internals reveal that gRPC is the transport.
82
-
83
- Separate gRPC server port (default `9000`) or unified with HTTP via `quarkus.grpc.server.use-separate-server=false` — pick one per platform and keep it consistent.
84
-
85
- ## Error mapping (`BusinessException` → gRPC status)
86
-
87
- One `ExceptionHandlerProvider`-style mapping, symmetric to the REST unified handler. The status is derived from the code's entry in `ErrorCatalog`, not from the exception class:
88
-
89
- | Situation (per `ErrorCatalog`) | gRPC status |
90
- |---|---|
91
- | Not found | `NOT_FOUND` |
92
- | Validation / bad argument | `INVALID_ARGUMENT` |
93
- | Business rule conflict | `FAILED_PRECONDITION` |
94
- | Duplicate | `ALREADY_EXISTS` |
95
- | Stale version / concurrency (`StaleVersionException`) | `ABORTED` |
96
- | Auth | `UNAUTHENTICATED` / `PERMISSION_DENIED` |
97
- | Anything unexpected | `INTERNAL` (no details leaked) |
98
-
99
- Attach the stable error code in trailers metadata key `error-code` so callers can map it back through their own `ErrorCatalog`. Implement once as a global gRPC exception interceptor (`GrpcExceptionInterceptor` in `common/error`, the counterpart of `GlobalExceptionHandler` — see quarkus-error-handling-i18n skill); it resolves the localized message the same way, from the request's locale metadata. Individual services never build `StatusRuntimeException` by hand, and no `GrpcService` catches `BusinessException`.
100
-
101
- ## Client (outbound integration bean, `common/client`)
102
-
103
- A slice `Handler` must never hold a `@GrpcClient` stub: the generated stub returns `Uni`, which would drag Mutiny into the business logic and break the `reactiveIsQuarantined` rule. Wrap it in a capability-named bean that `await()`s internally and exposes plain types:
104
-
105
- ```java
106
- // common/client — named for what it provides, not for how
107
- @ApplicationScoped
108
- public class CredentialsValidator {
109
-
110
- @GrpcClient("iam-users") UserService client; // Mutiny stub, contained here
111
-
112
- public boolean validate(String username, String password) {
113
- return client.validateCredentials(
114
- ValidateCredentialsRequest.newBuilder()
115
- .setUsername(username).setPassword(password).build())
116
- .map(ValidateCredentialsResponse::getValid)
117
- .await().atMost(Duration.ofSeconds(2)); // deadline also set in config
118
- }
119
- }
120
- ```
121
-
122
- The `Handler` then injects it like any other collaborator:
123
-
124
- ```java
125
- @Inject CredentialsValidator credentials; // returns boolean, knows nothing about gRPC
126
- ```
127
-
128
- The bean always lives in `common/client`, even when only one slice calls it today — that is the package the `reactiveIsQuarantined` ArchUnit rule exempts (see quarkus-hexagonal-core skill), and it saves relocating the bean the day a second slice needs it. It is mocked in `HandlerTest` exactly like the `Sql` class.
129
-
130
- An interface is warranted only under the core skill's "when to add an interface back" rule — a second real implementation, a cross-module boundary, or a `common/` consumer that must not see the slice.
131
-
132
- ```properties
133
- quarkus.grpc.clients.iam-users.host=iam-users.internal
134
- quarkus.grpc.clients.iam-users.port=9000
135
- quarkus.grpc.clients.iam-users.deadline=2s
136
- ```
137
-
138
- Rules:
139
- - **Deadline on every client** (config `deadline` or per-call). No unbounded internal calls, ever — cascading hangs kill native pods just as well.
140
- - Retries only for idempotent RPCs and only `UNAVAILABLE`/`DEADLINE_EXCEEDED`, with budget — prefer platform/mesh retry policy over hand-rolled loops.
141
- - TLS/mTLS per platform standard (`quarkus.grpc.clients.*.ssl.*` / mesh-provided).
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
-
144
- ## Health & reflection
145
-
146
- - `quarkus-smallrye-health` + gRPC health service enabled (`quarkus.grpc.server.health.enabled=true`) → Kubernetes gRPC probes.
147
- - Server reflection (`quarkus.grpc.server.enable-reflection-service=true`) in **dev/test only** (grpcurl debugging); off in prod, same policy as Swagger UI.
148
-
149
- ## Observability
150
-
151
- Tracing is automatic when `quarkus-opentelemetry` is present — `traceparent` propagates via gRPC metadata on both server and client. No manual work; see quarkus-observability-otel skill.
152
-
153
- ## Checklist for a new RPC
154
-
155
- 1. Proto in `src/main/proto/<module>/v<major>/`, published to `contracts/`, dedicated Request/Response pair.
156
- 2. `<ProtoService>GrpcService` in the slice folder delegating to the slice `Handler`; proto ↔ DTO conversion inline; blocking handled by `runSubscriptionOn` or `@Blocking`.
157
- 3. Outbound calls wrapped in a capability-named bean in `common/client` that `await()`s and returns plain types; deadline configured.
158
- 4. Error mapping covered by the global interceptor (register any new code in `ErrorCatalog` + all locale bundles).
159
- 5. Contract check in CI (buf breaking-change detection or equivalent).
160
- 6. No `Uni` outside the `GrpcService` and the integration bean — `reactiveIsQuarantined` proves it.