bmad-method-quarkus 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.