bmad-method-quarkus 1.0.1 → 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.
Files changed (19) hide show
  1. package/package.json +1 -1
  2. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-error-handling-i18n/SKILL.md +61 -27
  3. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-grpc-services/SKILL.md +60 -41
  4. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-hexagonal-core/SKILL.md +482 -213
  5. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-kafka-messaging/SKILL.md +46 -29
  6. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-observability-otel/SKILL.md +47 -31
  7. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-openapi-tmforum/SKILL.md +42 -25
  8. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-sql-jdbc-agroal/SKILL.md +115 -85
  9. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md +61 -27
  10. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md +60 -41
  11. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md +482 -213
  12. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md +46 -29
  13. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md +47 -31
  14. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md +42 -25
  15. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md +115 -85
  16. package/src/commands/dev-all.md +105 -0
  17. package/src/commands/quarkus-all.md +156 -0
  18. package/tools/installer/ide/_config-driven.js +43 -0
  19. package/tools/installer/ide/platform-codes.yaml +2 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method-quarkus",
4
- "version": "1.0.1",
4
+ "version": "1.0.3",
5
5
  "description": "BMAD Method with Quarkus support",
6
6
  "keywords": [
7
7
  "agile",
@@ -1,6 +1,6 @@
1
1
  ---
2
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 exception class naming and the English-identifiers/localized-messages rule.
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
4
  ---
5
5
 
6
6
  # Unified Exception Handler + i18n Responses (Quarkus, TMF-aligned)
@@ -24,33 +24,42 @@ All error responses use `application/json` with the TMF Error structure (interop
24
24
  }
25
25
  ```
26
26
 
27
- - `code`: stable, machine-readable, defined in the **domain exception**, format `<MOD>-<HTTP>-<seq>` where `<MOD>` is a short module/domain code (the BC code in DDD projects). Never reworded, never localized.
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
28
  - `message`: human-readable, **localized** via bundles.
29
29
  - `referenceError`: link to error catalog docs.
30
30
  - Add `traceId` propagation via response header `X-Trace-Id` (see observability skill), not in the body.
31
31
 
32
- ## Exception hierarchy (domain layer, framework-free)
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:
33
35
 
34
36
  ```java
35
- // domain/exception
36
- public abstract class DomainException extends RuntimeException {
37
+ // common/exception
38
+ public class BusinessException extends RuntimeException {
37
39
  private final String code; // "USR-404-001"
38
- private final Object[] messageArgs; // interpolated into localized message
39
- protected DomainException(String code, Object... args) {
40
- super(code);
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
41
43
  this.code = code;
42
44
  this.messageArgs = args;
43
45
  }
46
+ public BusinessException(String code, Throwable cause, Object... args) { ... }
44
47
  public String code() { return code; }
45
48
  public Object[] messageArgs() { return messageArgs; }
46
49
  }
50
+ ```
47
51
 
48
- public final class UserNotFoundException extends DomainException {
49
- public UserNotFoundException(UserId id) { super("USR-404-001", id.value()); }
50
- }
52
+ Thrown from the slice `Handler` and nowhere else:
53
+
54
+ ```java
55
+ throw new BusinessException("USR-404-001", userId);
51
56
  ```
52
57
 
53
- HTTP status is NOT known by the domain. The mapping code→status lives in the handler via a registry (an enum or properties-driven map in infrastructure).
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.
54
63
 
55
64
  ### Naming
56
65
 
@@ -58,21 +67,24 @@ Canonical rules in the quarkus-hexagonal-core skill; the error/i18n specifics:
58
67
 
59
68
  | Artifact | Convention | Example |
60
69
  |---|---|---|
61
- | Domain exception | `<Thing><Problem>Exception`, in `domain/exception` | `UserNotFoundException`, `DuplicateEmailException` |
62
- | Base class | `DomainException` | |
63
- | Technical exceptions | `PersistenceException`, `TransientPersistenceException`, `StaleVersionException` | |
64
- | Single REST handler | `GlobalExceptionHandler` (the only sanctioned `*Handler`) | |
65
- | gRPC counterpart | `GrpcExceptionInterceptor` | |
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` | |
66
76
  | i18n components | `MessageResolver`, `ErrorCatalog` | |
67
- | Error DTO | `ErrorDto` | |
77
+ | Error DTO | `ErrorDto` (TMF630 Error shape) | |
68
78
  | Bundle files | `messages/errors[_<lang>].properties` | `errors_en.properties` |
69
79
  | Bundle key | the error code itself, never a prose key | `USR-404-001` |
70
80
 
71
- Never name an exception after the HTTP status (`NotFoundException`, `BadRequestException`) the domain does not know about HTTP, and the status is resolved by `ErrorCatalog`. Never shadow a JDK type (`ConcurrentModificationException` `StaleVersionException`).
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.
72
82
 
73
- **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. `UserNotFoundException` with a Spanish translation `El usuario {0} no existe.` in `errors_es.properties` is correct — `UsuarioNoEncontradoException` is not.
83
+ Never name an exception after the HTTP status (`NotFoundException`, `BadRequestException`) the status is resolved by `ErrorCatalog`. Never shadow a JDK type (`ConcurrentModificationException` `StaleVersionException`).
74
84
 
75
- ## The single global handler (infrastructure/rest)
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`)
76
88
 
77
89
  Exactly ONE class. Use `@ServerExceptionMapper` (Quarkus REST) — build-time wired, native-friendly:
78
90
 
@@ -89,13 +101,27 @@ public class GlobalExceptionHandler {
89
101
  }
90
102
 
91
103
  @ServerExceptionMapper
92
- public RestResponse<ErrorDto> map(DomainException e, HttpHeaders headers) {
104
+ public RestResponse<ErrorDto> map(BusinessException e, HttpHeaders headers) {
93
105
  var status = catalog.statusOf(e.code()); // e.g. 404
94
106
  var msg = messages.resolve(e.code(), headers, e.messageArgs());
95
107
  return RestResponse.status(status,
96
108
  ErrorDto.of(e.code(), status, msg));
97
109
  }
98
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
+
99
125
  @ServerExceptionMapper
100
126
  public RestResponse<ErrorDto> map(ConstraintViolationException e, HttpHeaders headers) {
101
127
  // 400, code "GEN-400-001", join violations into localized detail
@@ -114,7 +140,7 @@ public class GlobalExceptionHandler {
114
140
  Rules:
115
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).
116
142
  - Validation errors (Bean Validation) map to 400 with one generic code + per-field details array.
117
- - Resources and use cases must NOT catch-and-format errors themselves — throw domain exceptions and let the handler translate.
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.
118
144
  - `ErrorDto` is `@RegisterForReflection` (native).
119
145
 
120
146
  ## i18n resolution from Accept-Language
@@ -135,17 +161,23 @@ quarkus.default-locale=en
135
161
  ```properties
136
162
  # errors.properties (en, default)
137
163
  USR-404-001=User {0} does not exist.
164
+ PTY-400-001=The ''{0}'' field is required.
138
165
  GEN-400-001=The request contains invalid data.
139
166
  GEN-500-001=Internal error. Contact support with the trace identifier.
140
167
  ```
141
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
+
142
171
  ```properties
143
172
  # errors_es.properties
144
173
  USR-404-001=El usuario {0} no existe.
174
+ PTY-400-001=El campo ''{0}'' es obligatorio.
145
175
  GEN-400-001=La solicitud contiene datos inválidos.
146
176
  GEN-500-001=Error interno. Contacte a soporte con el identificador de traza.
147
177
  ```
148
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
+
149
181
  For native: `quarkus.native.resources.includes=messages/*.properties`.
150
182
 
151
183
  ### Resolver
@@ -175,7 +207,9 @@ Success-path localizable strings (notification texts, report labels) use the sam
175
207
 
176
208
  ## Checklist when adding a new error
177
209
 
178
- 1. Create domain exception extending `DomainException`, named `<Thing><Problem>Exception`, with new code `<MOD>-<HTTP>-<seq>`.
179
- 2. Register code status/reason in `ErrorCatalog`.
180
- 3. Add the message key to ALL locale bundles (build fails a test if a code is missing in any bundle — write that test once: iterate catalog codes × locales).
181
- 4. Document in the error catalog page referenced by `referenceError`.
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,6 +1,6 @@
1
1
  ---
2
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 domain exceptions, 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/mapper naming conventions.
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
4
  ---
5
5
 
6
6
  # gRPC Standard for Inter-Service Communication (Quarkus)
@@ -11,12 +11,12 @@ Extension: `quarkus-grpc` (fully native-compatible; code generation is build-tim
11
11
 
12
12
  ## Proto conventions
13
13
 
14
- Location: `src/main/proto/`. One package per service/module + major version (the module segment is the BC code in DDD projects, the service name otherwise):
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
15
 
16
16
  ```protobuf
17
17
  syntax = "proto3";
18
- package alva.bc05.v1;
19
- option java_package = "com.alva.bc05.grpc.v1";
18
+ package alva.iam.v1;
19
+ option java_package = "com.alva.iam.grpc.v1";
20
20
  option java_multiple_files = true;
21
21
 
22
22
  service UserService {
@@ -36,27 +36,32 @@ Rules:
36
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
37
  - Streaming RPCs only with a justified use case (bulk export, watch); default is unary.
38
38
 
39
- ## Server (inbound adapter, `infrastructure/grpc`)
39
+ ## Server (inbound adapter, in the slice folder)
40
40
 
41
41
  ```java
42
42
  @GrpcService
43
43
  public class UserGrpcService implements UserService { // Mutiny-generated interface
44
44
 
45
- private final GetUserUseCase getUser; // inbound port from application layer
46
-
47
- public UserGrpcService(GetUserUseCase getUser) { this.getUser = getUser; }
45
+ @Inject FindUserHandler findUser; // the slice's Handler, blocking
48
46
 
49
47
  @Override
50
48
  public Uni<GetUserResponse> getUser(GetUserRequest req) {
51
- return Uni.createFrom().item(() -> getUser.byId(new UserId(UUID.fromString(req.getUserId()))))
52
- .runSubscriptionOn(Infrastructure.getDefaultWorkerPool()) // JDBC below → worker thread
53
- .map(UserGrpcMapper::toResponse);
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
54
57
  }
55
58
  }
56
59
  ```
57
60
 
58
- - gRPC service = thin adapter: map proto ↔ command/result, delegate to the use case. No business logic.
59
- - Blocking work (JDBC) either `@Blocking` on the method or explicit worker pool as above.
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.
60
65
 
61
66
  ### Naming
62
67
 
@@ -68,60 +73,73 @@ Canonical rules in the quarkus-hexagonal-core skill; gRPC specifics:
68
73
  | Proto RPC | `<Verb><Entity>` | `GetUser`, `ValidateCredentials` |
69
74
  | Proto messages | `<Rpc>Request` / `<Rpc>Response`, one pair per RPC | `GetUserRequest` |
70
75
  | Proto fields | `snake_case` | `user_id` |
71
- | Server adapter | `<ProtoService>GrpcService` | `UserGrpcService` |
72
- | Client adapter | `Grpc<Port>` port name from `application/port/out` | `GrpcCredentialsValidator` |
73
- | Proto domain mapper | `<Entity>GrpcMapper` | `UserGrpcMapper` |
74
- | Global error interceptor | `GrpcExceptionInterceptor` | |
75
- | Client config key | `<module>-<entity>` kebab-case, matching `@GrpcClient` | `bc05-users` |
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.
76
82
 
77
- `*GrpcService` is the one place outside `application/usecase` where the `*Service` suffix is allowed — it mirrors the proto service name, which is not ours to rename. The client adapter is never called `UserServiceClient`: it implements a domain-named port, and only the `Grpc` prefix reveals the transport.
78
- - 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.
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.
79
84
 
80
- ## Error mapping (domain exception → gRPC status)
85
+ ## Error mapping (`BusinessException` → gRPC status)
81
86
 
82
- One `ExceptionHandlerProvider`-style mapping, symmetric to the REST unified handler:
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:
83
88
 
84
- | Domain situation | gRPC status |
89
+ | Situation (per `ErrorCatalog`) | gRPC status |
85
90
  |---|---|
86
91
  | Not found | `NOT_FOUND` |
87
92
  | Validation / bad argument | `INVALID_ARGUMENT` |
88
93
  | Business rule conflict | `FAILED_PRECONDITION` |
89
- | Duplicate / concurrency | `ALREADY_EXISTS` / `ABORTED` |
94
+ | Duplicate | `ALREADY_EXISTS` |
95
+ | Stale version / concurrency (`StaleVersionException`) | `ABORTED` |
90
96
  | Auth | `UNAUTHENTICATED` / `PERMISSION_DENIED` |
91
97
  | Anything unexpected | `INTERNAL` (no details leaked) |
92
98
 
93
- Attach the stable error code in trailers metadata key `error-code` so callers can map back to their own domain exceptions. Implement once as a global gRPC exception interceptor (`GrpcExceptionInterceptor`, the counterpart of `GlobalExceptionHandler` — see quarkus-error-handling-i18n skill); individual services never build `StatusRuntimeException` by hand.
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`.
94
100
 
95
- ## Client (outbound adapter)
101
+ ## Client (outbound integration bean, `common/client`)
96
102
 
97
- Define an outbound port in `application/port/out` (e.g., `CredentialsValidator`); the gRPC client is its adapter:
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:
98
104
 
99
105
  ```java
106
+ // common/client — named for what it provides, not for how
100
107
  @ApplicationScoped
101
- public class GrpcCredentialsValidator implements CredentialsValidator {
108
+ public class CredentialsValidator {
102
109
 
103
- @GrpcClient("bc05-users") UserService client; // Mutiny stub
110
+ @GrpcClient("iam-users") UserService client; // Mutiny stub, contained here
104
111
 
105
- @Override
106
- public boolean validate(Credentials c) {
107
- return client.validateCredentials(toRequest(c))
112
+ public boolean validate(String username, String password) {
113
+ return client.validateCredentials(
114
+ ValidateCredentialsRequest.newBuilder()
115
+ .setUsername(username).setPassword(password).build())
108
116
  .map(ValidateCredentialsResponse::getValid)
109
- .await().atMost(Duration.ofSeconds(2));
117
+ .await().atMost(Duration.ofSeconds(2)); // deadline also set in config
110
118
  }
111
119
  }
112
120
  ```
113
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
+
114
132
  ```properties
115
- quarkus.grpc.clients.bc05-users.host=bc05-users.internal
116
- quarkus.grpc.clients.bc05-users.port=9000
117
- quarkus.grpc.clients.bc05-users.deadline=2s
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
118
136
  ```
119
137
 
120
138
  Rules:
121
139
  - **Deadline on every client** (config `deadline` or per-call). No unbounded internal calls, ever — cascading hangs kill native pods just as well.
122
140
  - Retries only for idempotent RPCs and only `UNAVAILABLE`/`DEADLINE_EXCEEDED`, with budget — prefer platform/mesh retry policy over hand-rolled loops.
123
141
  - TLS/mTLS per platform standard (`quarkus.grpc.clients.*.ssl.*` / mesh-provided).
124
- - Translate `StatusRuntimeException` back into the caller's domain exceptions at the adapter — gRPC types never cross into application/domain.
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`.
125
143
 
126
144
  ## Health & reflection
127
145
 
@@ -134,8 +152,9 @@ Tracing is automatic when `quarkus-opentelemetry` is present — `traceparent` p
134
152
 
135
153
  ## Checklist for a new RPC
136
154
 
137
- 1. Proto in shared contracts module, package `vN`, dedicated Request/Response.
138
- 2. Server adapter delegating to a use case; blocking handled explicitly.
139
- 3. Client behind an outbound port, deadline configured.
140
- 4. Error mapping covered by the global interceptor (add new code if needed).
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).
141
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.