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.
- package/package.json +1 -1
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-error-handling-i18n/SKILL.md +61 -27
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-grpc-services/SKILL.md +60 -41
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-hexagonal-core/SKILL.md +482 -213
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-kafka-messaging/SKILL.md +46 -29
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-observability-otel/SKILL.md +47 -31
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-openapi-tmforum/SKILL.md +42 -25
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-sql-jdbc-agroal/SKILL.md +115 -85
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md +61 -27
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md +60 -41
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md +482 -213
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md +46 -29
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md +47 -31
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md +42 -25
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md +115 -85
- package/src/commands/dev-all.md +105 -0
- package/src/commands/quarkus-all.md +156 -0
- package/tools/installer/ide/_config-driven.js +43 -0
- package/tools/installer/ide/platform-codes.yaml +2 -0
package/package.json
CHANGED
package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-error-handling-i18n/SKILL.md
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
##
|
|
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
|
-
//
|
|
36
|
-
public
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
62
|
-
|
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
-
|
|
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.
|
|
179
|
-
2.
|
|
180
|
-
3.
|
|
181
|
-
4.
|
|
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
|
|
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
|
|
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.
|
|
19
|
-
option java_package = "com.alva.
|
|
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,
|
|
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
|
-
|
|
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(() ->
|
|
52
|
-
|
|
53
|
-
|
|
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:
|
|
59
|
-
-
|
|
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
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
|
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
|
|
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
|
|
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
|
|
101
|
+
## Client (outbound integration bean, `common/client`)
|
|
96
102
|
|
|
97
|
-
|
|
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
|
|
108
|
+
public class CredentialsValidator {
|
|
102
109
|
|
|
103
|
-
@GrpcClient("
|
|
110
|
+
@GrpcClient("iam-users") UserService client; // Mutiny stub, contained here
|
|
104
111
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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.
|
|
116
|
-
quarkus.grpc.clients.
|
|
117
|
-
quarkus.grpc.clients.
|
|
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`
|
|
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
|
|
138
|
-
2.
|
|
139
|
-
3.
|
|
140
|
-
4. Error mapping covered by the global interceptor (
|
|
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.
|