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-kafka-messaging
3
- description: Standard for asynchronous event-driven messaging with Apache Kafka in Quarkus native services — CloudEvents envelope, topic naming, transactional outbox, idempotent consumers, DLQ, and W3C trace context propagation. Use this skill whenever the user mentions Kafka, events, messaging, publish/subscribe, producers, consumers, outbox, domain events between services, SmallRye Reactive Messaging, or asynchronous integration between bounded contexts — even if they just say "emit an event" or "listen to a topic". Includes event, payload, consumer, publisher and channel naming conventions.
3
+ description: Standard for asynchronous event-driven messaging with Apache Kafka in Quarkus native services — CloudEvents envelope, topic naming, transactional outbox, idempotent consumers, DLQ, and W3C trace context propagation. Use this skill whenever the user mentions Kafka, events, messaging, publish/subscribe, producers, consumers, outbox, domain events between services, SmallRye Reactive Messaging, or asynchronous integration between bounded contexts — even if they just say "emit an event" or "listen to a topic". Includes payload, consumer, relay-job and channel naming conventions for the vertical-slice layout (the Consumer lives in the slice folder and delegates to the slice Handler; the outbox row is written by the slice's own Sql inside the Handler transaction).
4
4
  ---
5
5
 
6
6
  # Kafka Messaging Standard (Quarkus + SmallRye Reactive Messaging)
@@ -12,9 +12,9 @@ Extension: `quarkus-messaging-kafka` (SmallRye Reactive Messaging). Native-compa
12
12
  ## Topic naming
13
13
 
14
14
  ```
15
- <org>.<module>.<entity>.<event-type>.v<major> # <module> = BC code or service name
16
- alva.bc05.user.registered.v1
17
- alva.bc01.subscription.status-changed.v2
15
+ <org>.<module>.<entity>.<event-type>.v<major> # <module> = semantic module name (see quarkus-hexagonal-core skill)
16
+ alva.iam.user.registered.v1
17
+ alva.tenant.subscription.status-changed.v2
18
18
  ```
19
19
 
20
20
  - Kebab-case segments, past-tense event types.
@@ -27,10 +27,10 @@ Producers emit CloudEvents in **binary mode** (attributes as Kafka headers `ce_*
27
27
 
28
28
  ```properties
29
29
  mp.messaging.outgoing.user-registered.connector=smallrye-kafka
30
- mp.messaging.outgoing.user-registered.topic=alva.bc05.user.registered.v1
30
+ mp.messaging.outgoing.user-registered.topic=alva.iam.user.registered.v1
31
31
  mp.messaging.outgoing.user-registered.cloud-events=true
32
- mp.messaging.outgoing.user-registered.cloud-events-source=//alva/bc05
33
- mp.messaging.outgoing.user-registered.cloud-events-type=com.alva.bc05.user.registered.v1
32
+ mp.messaging.outgoing.user-registered.cloud-events-source=//alva/iam
33
+ mp.messaging.outgoing.user-registered.cloud-events-type=com.alva.iam.user.registered.v1
34
34
  mp.messaging.outgoing.user-registered.value.serializer=io.quarkus.kafka.client.serialization.ObjectMapperSerializer
35
35
  ```
36
36
 
@@ -42,9 +42,9 @@ Payload schema:
42
42
 
43
43
  ## Producing: transactional outbox (mandatory for domain events)
44
44
 
45
- Never publish directly from a use case in the same breath as a DB write — dual-write problem. Standard:
45
+ Never publish to Kafka in the same breath as a DB write — dual-write problem. Standard:
46
46
 
47
- 1. Use case (in the SAME JDBC transaction as the state change) inserts into the outbox table.
47
+ 1. The slice `Handler` (in the SAME JDBC transaction as the state change) inserts into the outbox table.
48
48
  2. A relay publishes to Kafka (Debezium outbox connector preferred; scheduled poller as fallback for environments without Kafka Connect).
49
49
 
50
50
  Standard outbox table (one per service):
@@ -54,7 +54,7 @@ CREATE TABLE outbox_event (
54
54
  id UUID PRIMARY KEY,
55
55
  aggregate_type VARCHAR(64) NOT NULL, -- "user"
56
56
  aggregate_id VARCHAR(64) NOT NULL, -- partition key
57
- event_type VARCHAR(128) NOT NULL, -- "com.alva.bc05.user.registered.v1"
57
+ event_type VARCHAR(128) NOT NULL, -- "com.alva.iam.user.registered.v1"
58
58
  payload JSONB NOT NULL,
59
59
  traceparent VARCHAR(64) NOT NULL, -- captured at insert time (W3C Trace Context)
60
60
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
@@ -78,7 +78,22 @@ CREATE TABLE outbox_event (
78
78
 
79
79
  Both `traceparent` and `traceContext` are captured at insert time from `Span.current().getSpanContext()` — manual construction of trace strings is prohibited (see quarkus-observability-otel skill).
80
80
 
81
- The domain/application layer only knows an outbound port `EventPublisher.publish(DomainEvent e)`; the outbox insert is its JDBC adapter, `OutboxEventPublisher` in `infrastructure/messaging`.
81
+ Under the vertical-slice standard there is no `EventPublisher` port and no publisher adapter: the slice's `Handler` writes the outbox row through **its own `Sql` class**, inside `execution()`, on the same `Connection` and therefore in the same transaction as the state change:
82
+
83
+ ```java
84
+ private UUID execution(CreatePartyIndividualRequestDto request) {
85
+ try (Connection conn = dataSource.getConnection()) {
86
+ UUID partyId = sql.insertParty(conn, ...);
87
+ sql.insertOutboxEvent(conn, partyId, "party",
88
+ "com.alva.customer.party.created.v1", payloadJson(partyId, request), traceparent());
89
+ return partyId;
90
+ } catch (SQLException e) {
91
+ throw SqlStateTranslator.translate("PTY-500-001", e);
92
+ }
93
+ }
94
+ ```
95
+
96
+ That is the whole dual-write guarantee — one connection, one transaction, no port indirection. `OutboxRelayJob` lives in `common/messaging` (or is replaced by the Debezium connector) and is the only component that talks to Kafka on the produce side.
82
97
 
83
98
  Fire-and-forget technical messages (metrics, notifications with no consistency requirement) MAY use a direct `Emitter`:
84
99
 
@@ -92,17 +107,14 @@ Canonical rules in the quarkus-hexagonal-core skill; messaging specifics:
92
107
 
93
108
  | Artifact | Convention | Example |
94
109
  |---|---|---|
95
- | Domain event | past tense, no suffix, in `domain/event` | `UserRegistered` |
96
- | Wire payload DTO | `<Event>Payload`, in `infrastructure/messaging/dto` | `UserRegisteredPayload` |
97
- | Outbound port | `EventPublisher` (capability, no technology) | |
98
- | Publisher adapter | `Outbox<Port>` / `Kafka<Port>` | `OutboxEventPublisher` |
99
- | Consumer | `<Event>Consumer` | `UserRegisteredConsumer` |
100
- | Outbox relay job | `<Task>Job` | `OutboxRelayJob` |
101
- | Event ↔ payload mapper | `<Entity>EventMapper` | `UserEventMapper` |
110
+ | Wire payload DTO | `<Event>Payload`, in the producing/consuming slice's `dto/` | `UserRegisteredPayload` |
111
+ | Consumer | `<Event>Consumer`, in the slice folder | `UserRegisteredConsumer` |
112
+ | Outbox write | a method on the slice's own `Sql` | `insertOutboxEvent(conn, ...)` |
113
+ | Outbox relay job | `<Task>Job`, in `common/messaging` | `OutboxRelayJob` |
102
114
  | SmallRye channel | `<entity>-<event>-in` / `-out`, kebab-case | `user-registered-in` |
103
- | CloudEvents `type` | `<javaPackageStyle>.<entity>.<event>.v<major>` | `com.alva.bc05.user.registered.v1` |
115
+ | CloudEvents `type` | `<javaPackageStyle>.<entity>.<event>.v<major>` | `com.alva.iam.user.registered.v1` |
104
116
 
105
- Keep the domain event (`UserRegistered`) and the wire payload (`UserRegisteredPayload`) as separate classes even when their fields match: the domain event is free to change with the model, while the payload is a published contract versioned by topic. Never annotate the domain event with Jackson or `@RegisterForReflection` that annotation belongs on the payload.
117
+ `<Event>Payload` is the **only** event class: there is no separate in-memory domain event, because the slice has no domain layer to protect it from. The payload is a published contract versioned by topic treat a field rename as a breaking change and bump the topic version, never "because the model moved". It carries `@RegisterForReflection` for native. There is no `*EventMapper`: the payload is built in the `Handler`, the same place `getResult()` builds the response.
106
118
 
107
119
  ## Consuming
108
120
 
@@ -110,25 +122,29 @@ Keep the domain event (`UserRegistered`) and the wire payload (`UserRegisteredPa
110
122
  @ApplicationScoped
111
123
  public class UserRegisteredConsumer {
112
124
 
125
+ @Inject RegisterUserFromEventHandler handler; // the slice's business logic
126
+
113
127
  @Incoming("user-registered-in")
114
128
  @Blocking // JDBC inside → run on worker thread (or virtual thread)
115
- public void on(UserRegisteredPayload payload) { // wire DTO, not the domain event
116
- // 1. idempotency check 2. delegate to use case 3. done
129
+ public void on(UserRegisteredPayload payload) {
130
+ handler.process(toRequest(payload)); // idempotency + side effects, one transaction
117
131
  }
118
132
  }
119
133
  ```
120
134
 
135
+ The consumer is as thin as a `Resource`: deserialize, convert to the slice's request DTO, call `process()`. **The idempotency check belongs inside `Handler.execution()`**, not here — it must share the transaction with the side effects, and only the Handler owns the `Connection`. For explicit ack/nack, take `Message<UserRegisteredPayload>` and return `message.ack()` / `message.nack(e)`; do not catch `BusinessException` to build your own error response.
136
+
121
137
  ```properties
122
138
  mp.messaging.incoming.user-registered-in.connector=smallrye-kafka
123
- mp.messaging.incoming.user-registered-in.topic=alva.bc05.user.registered.v1
139
+ mp.messaging.incoming.user-registered-in.topic=alva.iam.user.registered.v1
124
140
  mp.messaging.incoming.user-registered-in.group.id=${quarkus.application.name}
125
141
  mp.messaging.incoming.user-registered-in.auto.offset.reset=earliest
126
142
  mp.messaging.incoming.user-registered-in.failure-strategy=delayed-retry-then-dead-letter-queue
127
- mp.messaging.incoming.user-registered-in.dead-letter-queue.topic=alva.bc05.user.registered.v1.dlq
143
+ mp.messaging.incoming.user-registered-in.dead-letter-queue.topic=alva.iam.user.registered.v1.dlq
128
144
  ```
129
145
 
130
146
  Rules:
131
- - **Idempotency is mandatory**: `processed_event(consumer_group, event_id)` table checked/inserted in the same transaction as the side effects. Kafka is at-least-once; duplicates WILL happen.
147
+ - **Idempotency is mandatory**: `processed_event(consumer_group, event_id)` checked/inserted through the slice's `Sql` on the same `Connection`, in the same transaction as the side effects. Kafka is at-least-once; duplicates WILL happen.
132
148
  - Consumer group = service name; stable across deployments.
133
149
  - Failure strategy: bounded retries with delay, then DLQ topic `<topic>.dlq`. DLQ messages keep original headers plus `dead-letter-reason`. A DLQ must have an owner and an alert — never a silent graveyard.
134
150
  - Poison-pill safety: deserialization failures also route to DLQ (`...deserialization-failure-handler` or failure strategy), never block the partition.
@@ -141,8 +157,9 @@ Rules:
141
157
 
142
158
  ## Checklist for a new event
143
159
 
144
- 1. Define `<Event>Payload` record + JSON Schema in `contracts/`, name the topic and channel per convention.
145
- 2. Producer: outbox insert in use-case transaction; relay config.
146
- 3. `@RegisterForReflection` on the payload record (never on the domain event).
147
- 4. Consumer(s): idempotency table entry, DLQ topic + alert, `@Blocking` if JDBC.
160
+ 1. Define `<Event>Payload` in the slice's `dto/` + its JSON Schema in `contracts/`; name the topic and channel per convention.
161
+ 2. Producer: `insertOutboxEvent` method on the slice's `Sql`, called from `Handler.execution()` on the shared `Connection`; relay config for `OutboxRelayJob`/Debezium.
162
+ 3. `@RegisterForReflection` on the payload; `traceparent` + `traceContext` captured from `Span.current()` at insert time.
163
+ 4. Consumer: `<Event>Consumer` in the slice folder delegating to the `Handler`, idempotency inside the transaction, DLQ topic + owner + alert, `@Blocking`.
148
164
  5. Contract test: serialize/deserialize round-trip against the JSON Schema.
165
+ 6. New error codes registered in `ErrorCatalog` and present in all locale bundles.
@@ -1,18 +1,18 @@
1
1
  ---
2
2
  name: quarkus-observability-otel
3
- description: Observability standard for Quarkus native services using OpenTelemetry — distributed tracing with W3C Trace Context (traceparent), trace_id/span_id in every log line (JSON logs + MDC), OTLP export, metrics with Micrometer, custom spans, and trace propagation across REST, gRPC, and Kafka. Use this skill whenever the user mentions OpenTelemetry, OTel, tracing, trace_id, span_id, traceparent, distributed tracing, logging standard, log correlation, metrics, Grafana/Tempo/Jaeger/Prometheus, or debugging requests across services. Includes span, metric, logger and audit-adapter naming conventions.
3
+ description: Observability standard for Quarkus native services using OpenTelemetry — distributed tracing with W3C Trace Context (traceparent), trace_id/span_id in every log line (JSON logs + MDC), OTLP export, metrics with Micrometer, custom spans, and trace propagation across REST, gRPC, and Kafka. Use this skill whenever the user mentions OpenTelemetry, OTel, tracing, trace_id, span_id, traceparent, distributed tracing, logging standard, log correlation, metrics, Grafana/Tempo/Jaeger/Prometheus, or debugging requests across services. Includes span, metric, logger and audit-bean naming conventions for the vertical-slice layout (one span per Handler.process()).
4
4
  ---
5
5
 
6
6
  # OpenTelemetry Observability Standard (Quarkus)
7
7
 
8
- Every request must be traceable end-to-end: REST → use case → JDBC → outbox → Kafka → consumer → gRPC. `trace_id`/`span_id` appear in every log line and propagate across every transport via W3C Trace Context. Applies to any Quarkus backend project; project directives (CLAUDE.md, ADRs, explicit instructions) override these defaults.
8
+ Every request must be traceable end-to-end: REST → Handler → JDBC → outbox → Kafka → consumer → gRPC. `trace_id`/`span_id` appear in every log line and propagate across every transport via W3C Trace Context. Applies to any Quarkus backend project; project directives (CLAUDE.md, ADRs, explicit instructions) override these defaults.
9
9
 
10
10
  Extensions: `quarkus-opentelemetry` (+ `quarkus-micrometer-registry-prometheus` or Micrometer→OTLP for metrics, `quarkus-logging-json` for structured logs). All native-compatible.
11
11
 
12
12
  ## Baseline configuration
13
13
 
14
14
  ```properties
15
- quarkus.application.name=bc05-users # becomes service.name
15
+ quarkus.application.name=iam-users # becomes service.name
16
16
  quarkus.otel.exporter.otlp.endpoint=http://otel-collector:4317
17
17
  quarkus.otel.resource.attributes=deployment.environment=${ENV:dev},service.namespace=alva
18
18
 
@@ -85,35 +85,43 @@ Stored value (snake_case keys, exactly these):
85
85
  { "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "span_id": "00f067aa0ba902b7" }
86
86
  ```
87
87
 
88
- Reference implementation. The application layer depends on an outbound port `AuditRecorder` (`application/port/out`); capturing the active span is an infrastructure concern, so it happens in the adapter:
88
+ Reference implementation. Auditing is cross-cutting, so it is a single capability-named bean in `common/audit` no port, no adapter pair. It captures the active span itself and takes the caller's `Connection` so the audit row joins the caller's transaction:
89
89
 
90
90
  ```java
91
- // application/port/out — capability noun, no technology in the name
92
- public interface AuditRecorder {
93
- void record(String action, String entity, String entityId);
94
- }
95
-
96
- // infrastructure/persistence — Jdbc<Port>
91
+ // common/audit — capability noun, no technology in the name, no interface
97
92
  @ApplicationScoped
98
- public class JdbcAuditRecorder implements AuditRecorder {
93
+ public class AuditRecorder {
99
94
 
100
- private final AuditEventRepository repository;
95
+ private static final String INSERT_AUDIT = """
96
+ INSERT INTO audit_event (action, entity, entity_id, context)
97
+ VALUES (?, ?, ?, ?::jsonb)
98
+ """;
101
99
 
102
- public JdbcAuditRecorder(AuditEventRepository repository) { this.repository = repository; }
103
-
104
- @Override
105
- public void record(String action, String entity, String entityId) {
100
+ public void record(Connection conn, String action, String entity, String entityId) throws SQLException {
106
101
  String traceId = Span.current().getSpanContext().getTraceId();
107
102
  String spanId = Span.current().getSpanContext().getSpanId();
108
- JsonObject context = new JsonObject()
103
+ String context = new JsonObject()
109
104
  .put("trace_id", traceId)
110
- .put("span_id", spanId);
111
- repository.save(action, entity, entityId, context.encode());
105
+ .put("span_id", spanId)
106
+ .encode();
107
+ try (PreparedStatement ps = conn.prepareStatement(INSERT_AUDIT)) {
108
+ ps.setString(1, action);
109
+ ps.setString(2, entity);
110
+ ps.setString(3, entityId);
111
+ ps.setString(4, context);
112
+ ps.executeUpdate();
113
+ }
112
114
  }
113
115
  }
114
116
  ```
115
117
 
116
- Identifiers are English (`record`, `action`, `entity`), even when the business vocabulary and the UI are Spanish Spanish belongs in the i18n bundles, not in class and method names. `*Service` is not used here: this is an outbound adapter, not a use case.
118
+ The slice `Handler` calls it from `execution()` on the same `Connection` it already holds, so an audit row can never survive a rolled-back operation:
119
+
120
+ ```java
121
+ audit.record(conn, "CREATE", "party", partyId.toString());
122
+ ```
123
+
124
+ Identifiers are English (`record`, `action`, `entity`), even when the business vocabulary and the UI are Spanish — Spanish belongs in the message bundles, not in class and method names. `*Service` is not used here: under the slice standard that suffix is reserved for `*GrpcService`.
117
125
 
118
126
  Any code path that writes `audit_event` without populating `context` is non-compliant — flag it in review.
119
127
 
@@ -132,15 +140,23 @@ Both values come from `Span.current()` at insert time — never from manual stri
132
140
 
133
141
  ## Custom spans and attributes
134
142
 
135
- Annotate use cases that matter for diagnosis (not every method):
143
+ Annotate the entry point of each slice — `Handler.process()` — not every method:
136
144
 
137
145
  ```java
138
- // application/usecaseRegisterUserService implements RegisterUserUseCase
146
+ // the slice's Handler one span per business operation
147
+ @Transactional
139
148
  @WithSpan("usecase.registerUser")
140
- public UserId register(@SpanAttribute("user.email.domain") String emailDomain, RegisterUserCommand cmd) { ... }
149
+ public RegisterUserResponseDto process(RegisterUserRequestDto request) {
150
+ Span.current().setAttribute("user.email.domain", domainOf(request.getEmail()));
151
+ validate(request);
152
+ return getResult(execution(request));
153
+ }
141
154
  ```
142
155
 
143
- - Span names: `usecase.<verb><Entity>` (lowerCamelCase, matching the use-case method), `outbox.publish`, `job.<name>`.
156
+ - Span names: `usecase.<sliceCamelCase>` (lowerCamelCase). Because a slice is named verb+entity (`register_user`), this is identical to the older `usecase.<verb><Entity>` convention — existing dashboards keep working.
157
+ - **`process()` is the span boundary.** One span per business operation; do not annotate `validate()`/`execution()`/`getResult()` (they are private, so the interceptor would not fire anyway). JDBC spans come free from `quarkus.datasource.jdbc.telemetry=true`.
158
+ - Other span names: `outbox.publish`, `job.<name>`.
159
+ - Attributes are set inside `process()` via `Span.current().setAttribute(...)` — never by adding `@SpanAttribute` parameters, which would change the `process(RequestDto)` single-argument signature that every transport adapter and the Handler contract depend on (see quarkus-hexagonal-core skill).
144
160
  - Attributes: business-relevant, LOW-cardinality, NEVER PII (no emails, MSISDNs, tokens — use derived/hashed values).
145
161
  - Manual spans (`Tracer.spanBuilder`) only for background work not covered by annotations; always `try/finally` end the span and restore scope.
146
162
 
@@ -150,12 +166,12 @@ Canonical class-naming rules live in the quarkus-hexagonal-core skill; these are
150
166
 
151
167
  | Artifact | Convention | Example |
152
168
  |---|---|---|
153
- | Audit port / adapter | `AuditRecorder` / `Jdbc<Port>` | `AuditRecorder`, `JdbcAuditRecorder` |
169
+ | Audit bean | `AuditRecorder` one class in `common/audit`, no port | `AuditRecorder` |
154
170
  | Trace-id response filter | `<Purpose>Filter` | `TraceIdResponseFilter` |
155
- | Span name | `usecase.<verb><Entity>`, `outbox.publish`, `job.<name>` | `usecase.registerUser` |
171
+ | Span name | `usecase.<sliceCamelCase>` on `Handler.process()`, `outbox.publish`, `job.<name>` | `usecase.registerUser` |
156
172
  | Span attribute | dotted lowercase, OTel semconv where one exists | `user.email.domain` |
157
- | Metric | `<module>_<entity>_<action>_total` / `_seconds`, snake_case | `bc05_user_registrations_total` |
158
- | Logger | one per class via `Log`/`Logger.getLogger(Xxx.class)` — never a shared `LogUtil` | |
173
+ | Metric | `<module>_<entity>_<action>_total` / `_seconds`, snake_case | `iam_user_registrations_total` |
174
+ | Logger | one per class via `Log`/`Logger.getLogger(Xxx.class)` — never a shared `LogUtil`; the Handler's class name already carries the slice | |
159
175
  | MDC / jsonb trace keys | `snake_case`, exactly `trace_id` / `span_id` | |
160
176
 
161
177
  Span, metric and log-field names are a contract with the dashboards — renaming one silently breaks alerts. Treat a rename like an API change.
@@ -163,7 +179,7 @@ Span, metric and log-field names are a contract with the dashboards — renaming
163
179
  ## Metrics (Micrometer)
164
180
 
165
181
  - Rely on built-in HTTP/JVM-substrate/Kafka metrics first.
166
- - Custom business metrics: counters/timers via `MeterRegistry`, names `bc05_user_registrations_total` style, low-cardinality tags only.
182
+ - Custom business metrics: counters/timers via `MeterRegistry`, names `iam_user_registrations_total` style, low-cardinality tags only.
167
183
  - Expose `/q/metrics` for Prometheus scrape or bridge Micrometer→OTLP if the platform standardizes on the collector for metrics too.
168
184
 
169
185
  ## Health
@@ -174,7 +190,7 @@ Span, metric and log-field names are a contract with the dashboards — renaming
174
190
 
175
191
  1. `quarkus-opentelemetry` + JSON logging configured as above; OTLP → collector.
176
192
  2. Log format includes traceId/spanId (both profiles); `X-Trace-Id` response filter registered.
177
- 3. JDBC telemetry enabled; `audit_event.context` jsonb populated with `trace_id`/`span_id` (+ index); outbox rows carry `traceparent` (header) AND `traceContext` in payload; the relay forwards the header.
193
+ 3. JDBC telemetry enabled; `audit_event.context` jsonb populated with `trace_id`/`span_id` (+ index) on the Handler's own `Connection`; outbox rows carry `traceparent` (header) AND `traceContext` in payload; the relay forwards the header.
178
194
  3b. No manual trace-string construction anywhere — grep-check in review; only `Span.current()` via the OTel SDK.
179
- 4. Key use cases annotated `@WithSpan`; attributes reviewed for PII/cardinality.
195
+ 4. Every slice `Handler.process()` annotated `@WithSpan("usecase.<sliceCamelCase>")`; attributes reviewed for PII/cardinality.
180
196
  5. Verify end-to-end in dev: one request produces a single trace spanning REST → DB → Kafka → consumer (Dev UI or Jaeger/Tempo).
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: quarkus-openapi-tmforum
3
- description: API design and documentation standard for Quarkus REST services — OpenAPI 3.1 via SmallRye, Swagger UI restricted to dev/test, and full alignment with TM Forum Open API specifications (TMF630 design guidelines, TMF resource patterns, Error schema, pagination, filtering, JSON Merge Patch). Use this skill whenever the user mentions Swagger, OpenAPI, API documentation, REST API design, TMF/TM Forum APIs, endpoints/resources naming, pagination, PATCH semantics, or creates/reviews ANY REST resource class. Includes resource, DTO, mapper and operationId naming conventions.
3
+ description: API design and documentation standard for Quarkus REST services — OpenAPI 3.1 via SmallRye, Swagger UI restricted to dev/test, and full alignment with TM Forum Open API specifications (TMF630 design guidelines, TMF resource patterns, Error schema, pagination, filtering, JSON Merge Patch). Use this skill whenever the user mentions Swagger, OpenAPI, API documentation, REST API design, TMF/TM Forum APIs, endpoints/resources naming, pagination, PATCH semantics, or creates/reviews ANY REST resource class. Includes resource, DTO and operationId naming conventions for the vertical-slice layout (the Resource class lives in the slice folder and delegates to the slice Handler; there are no REST mappers).
4
4
  ---
5
5
 
6
6
  # OpenAPI + TM Forum API Standard (Quarkus)
@@ -31,7 +31,7 @@ The raw OpenAPI document at `/q/openapi` may stay enabled in prod behind the gat
31
31
 
32
32
  ### URLs & versioning
33
33
  - Plural kebab-free camelCase resource names as in TMF specs: `/user`, `/partyAccount` — follow the TMF spec exactly when implementing one; plural nouns for custom resources.
34
- - Base path: `/{context}/{apiName}/v{major}` e.g. `/tmf-api/digitalIdentityManagement/v4`. Major version in path only.
34
+ - Base path starts with the module's semantic name (`iam`, `tenant` — see quarkus-hexagonal-core skill, never a `bcNN` code): `/{module}/{context}/{apiName}/v{major}` e.g. `/iam/tmf-api/digitalIdentityManagement/v4`. Major version in path only.
35
35
 
36
36
  ### Standard operations
37
37
  | Operation | Verb | Response |
@@ -57,7 +57,9 @@ No PUT unless the TMF spec defines it. No RPC-ish URLs (`/user/activate`) — st
57
57
 
58
58
  ## Annotating resources (SmallRye OpenAPI)
59
59
 
60
- DTOs live in `infrastructure/rest/dto` — never annotate domain classes.
60
+ DTOs live in the slice's own `dto/` package (`<slice_folder>/dto/*Dto.java`)see quarkus-hexagonal-core skill. There is no separate domain class to protect: the DTO **is** the wire contract, so annotate it freely, and keep business logic out of it.
61
+
62
+ The `Resource` class lives in the slice folder alongside the `Handler` it fronts, and does exactly three things: extract headers/params, call `handler.process(...)`, shape the response. It never catches `BusinessException` (the global handler owns that) and never touches JDBC.
61
63
 
62
64
  ### Naming
63
65
 
@@ -65,55 +67,69 @@ Canonical rules in the quarkus-hexagonal-core skill; REST/TMF specifics:
65
67
 
66
68
  | Artifact | Convention | Example |
67
69
  |---|---|---|
68
- | Resource class | `<Resource>Resource` — matches the TMF resource name | `DigitalIdentityResource` |
69
- | Response DTO | `<Resource>Dto` | `DigitalIdentityDto` |
70
+ | Resource class | `<Resource>Resource` — matches the TMF resource name, lives in the slice folder | `DigitalIdentityResource` |
71
+ | Response DTO | `<Resource>Dto`, or `${SERVICE_CLASS_PREFIX}ResponseDto` for a slice-specific shape | `DigitalIdentityDto` |
70
72
  | Create / update DTO | `<Resource>CreateDto` / `<Resource>UpdateDto` (TMF `X_Create` / `X_Update`) | `DigitalIdentityCreateDto` |
73
+ | Request DTO (non-TMF) | `${SERVICE_CLASS_PREFIX}RequestDto` | `CreatePartyIndividualRequestDto` |
71
74
  | Error DTO | `ErrorDto` (see quarkus-error-handling-i18n skill) | |
72
- | DTO ↔ domain mapper | `<Resource>RestMapper` | `DigitalIdentityRestMapper` |
73
75
  | `operationId` | `list/retrieve/create/patch/delete` + `<Resource>` | `listDigitalIdentity` |
74
76
  | Path segment | camelCase, exactly as the TMF spec spells it | `/partyAccount` |
75
77
  | JSON field | camelCase; TMF meta-fields keep their `@` | `@type`, `@baseType` |
76
78
 
79
+ There is **no `*RestMapper` class** — the DTO↔internal conversion is the `Handler`'s `getResult()` (see quarkus-hexagonal-core skill), and `*Mapper` is a banned suffix. When a TMF spec prescribes `X_Create`/`X_Update`, those names win over the generic `RequestDto`; use the generic form only for non-TMF operations.
80
+
81
+ **One Resource per TMF resource, or one per slice?** When several slices serve the same TMF path, the default is one `Resource` class per TMF resource that injects each slice's `Handler` — JAX-RS path resolution stays unambiguous and the OpenAPI tag stays clean. One class per slice sharing a base `@Path` also works in Quarkus REST provided no two classes declare the same method+sub-path; the `/q/openapi` snapshot test below is what catches a collision.
82
+
77
83
  When implementing a TMF spec the wire names are **not negotiable** — DTO field names must match the spec even where our own convention would differ. The Java class name adds the `Dto` suffix; the serialized schema name stays the TMF one (`@Schema(name = "DigitalIdentity")`).
78
84
 
79
85
  ```java
80
- @Path("/tmf-api/digitalIdentityManagement/v4/digitalIdentity")
86
+ @Path("/iam/tmf-api/digitalIdentityManagement/v4/digitalIdentity")
81
87
  @Tag(name = "DigitalIdentity")
82
88
  public class DigitalIdentityResource {
83
89
 
84
- private final CreateDigitalIdentityUseCase createIdentity; // inbound port
85
- private final DigitalIdentityRestMapper mapper;
86
-
87
- public DigitalIdentityResource(CreateDigitalIdentityUseCase createIdentity,
88
- DigitalIdentityRestMapper mapper) {
89
- this.createIdentity = createIdentity;
90
- this.mapper = mapper;
91
- }
90
+ @Inject CreateDigitalIdentityHandler createHandler; // the slice's business logic
91
+ @Inject FindDigitalIdentityHandler findHandler;
92
92
 
93
93
  @GET
94
+ @Blocking
94
95
  @Operation(operationId = "listDigitalIdentity", summary = "List or find DigitalIdentity objects")
95
96
  @APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = DigitalIdentityDto[].class)))
96
97
  @APIResponse(responseCode = "400", ref = "#/components/responses/BadRequest")
97
98
  public RestResponse<List<DigitalIdentityDto>> list(
99
+ @HeaderParam("tenantId") String tenantId,
98
100
  @QueryParam("fields") String fields,
99
101
  @QueryParam("offset") @DefaultValue("0") int offset,
100
- @QueryParam("limit") @DefaultValue("20") int limit) { ... }
102
+ @QueryParam("limit") @DefaultValue("20") int limit) {
103
+ var query = FilterDigitalIdentityDto.builder()
104
+ .tenantId(tenantId).fields(fields).offset(offset).limit(limit).build();
105
+ return RestResponse.ok(findHandler.process(query).getItems());
106
+ }
101
107
 
102
108
  @POST
109
+ @Blocking
103
110
  @Operation(operationId = "createDigitalIdentity")
104
111
  @APIResponse(responseCode = "201", headers = @Header(name = "Location"))
105
- public RestResponse<DigitalIdentityDto> create(@Valid DigitalIdentityCreateDto dto, @Context UriInfo uri) {
106
- var created = createIdentity.create(mapper.toCommand(dto));
107
- return RestResponse.created(uri.getAbsolutePathBuilder().path(created.id()).build());
112
+ public RestResponse<DigitalIdentityDto> create(@HeaderParam("tenantId") String tenantId,
113
+ @Valid DigitalIdentityCreateDto dto,
114
+ @Context UriInfo uri) {
115
+ dto.setTenantId(tenantId);
116
+ var created = createHandler.process(dto); // BusinessException propagates to the global handler
117
+ return RestResponse.ResponseBuilder
118
+ .created(uri.getAbsolutePathBuilder().path(created.getId()).build())
119
+ .entity(created).build();
108
120
  }
109
121
  }
110
122
  ```
111
123
 
112
124
  Rules:
125
+ - Return `RestResponse<T>` synchronously — typed, so the generated schema is right. Never `Uni`/`Multi`: the `Handler` is blocking JDBC.
126
+ - `@Blocking` (`io.smallrye.common.annotation.Blocking`) on JDBC-backed methods. With a plain return type Quarkus REST already dispatches to a worker thread, so it is redundant today — keep it as an explicit threading contract that survives a later signature change. `@RunOnVirtualThread` is the high-concurrency alternative (see sql skill §7).
127
+ - Extract `tenantId`, `partyId`, `partyRolList`, `language` from headers where the contract requires them and set them on the request DTO before calling `process()`.
128
+ - No `try/catch` around `process()`. Errors travel as `BusinessException` to `GlobalExceptionHandler`, which resolves the localized TMF Error body (see quarkus-error-handling-i18n skill).
113
129
  - `operationId` on every operation (client generation depends on it); match TMF naming (`listX`, `retrieveX`, `createX`, `patchX`, `deleteX`).
114
130
  - Reusable components: define common responses (400/401/404/409/500 with TMF Error schema) once via an `@OpenAPIDefinition`/filter class, `ref` them everywhere.
115
131
  - `*CreateDto` (no id/href) vs full DTO — TMF pattern (`X_Create`, `X_Update`).
116
- - Bean Validation on DTOs (`@NotNull`, `@Size`) so constraints appear in the schema.
132
+ - Bean Validation on DTOs (`@NotNull`, `@Size`) so constraints appear in the schema — *form* only; business rules live in `Handler.validate()`.
117
133
 
118
134
  ## PATCH implementation (JSON Merge Patch)
119
135
 
@@ -121,8 +137,9 @@ Consume `application/merge-patch+json`. Apply merge onto the current DTO represe
121
137
 
122
138
  ## Checklist for a new endpoint
123
139
 
124
- 1. TMF spec exists? Copy & trim official OpenAPI, implement to match.
125
- 2. `<Resource>Resource` class with full annotations + operationIds; `<Resource>Dto` / `<Resource>CreateDto` with validation + `@RegisterForReflection`.
126
- 3. Error responses referenced to the shared TMF Error components.
127
- 4. fields/offset/limit on list operations; Location header on create.
128
- 5. Snapshot test: `/q/openapi` diff against committed contract.
140
+ 1. TMF spec exists? Copy & trim official OpenAPI into `contracts/`, implement to match.
141
+ 2. `<Resource>Resource` class in the slice folder with full annotations + operationIds, `@Blocking`, injecting the slice `Handler`; DTOs in `<slice>/dto` with validation + `@RegisterForReflection`.
142
+ 3. Error responses referenced to the shared TMF Error components — and no `try/catch` in the resource.
143
+ 4. fields/offset/limit on list operations; `Location` header on create; 201 on create, 204 on delete.
144
+ 5. Snapshot test: `/q/openapi` diff against the committed contract (also catches a `@Path` collision between slices).
145
+ 6. `@QuarkusTest` + REST Assured covering one success and one localized error body (`Accept-Language: es`).