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.
- 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
|
@@ -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
|
|
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> =
|
|
16
|
-
alva.
|
|
17
|
-
alva.
|
|
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.
|
|
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/
|
|
33
|
-
mp.messaging.outgoing.user-registered.cloud-events-type=com.alva.
|
|
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
|
|
45
|
+
Never publish to Kafka in the same breath as a DB write — dual-write problem. Standard:
|
|
46
46
|
|
|
47
|
-
1.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
|
96
|
-
|
|
|
97
|
-
|
|
|
98
|
-
|
|
|
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.
|
|
115
|
+
| CloudEvents `type` | `<javaPackageStyle>.<entity>.<event>.v<major>` | `com.alva.iam.user.registered.v1` |
|
|
104
116
|
|
|
105
|
-
|
|
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) {
|
|
116
|
-
//
|
|
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.
|
|
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.
|
|
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)`
|
|
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`
|
|
145
|
-
2. Producer:
|
|
146
|
-
3. `@RegisterForReflection` on the payload
|
|
147
|
-
4. Consumer
|
|
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-
|
|
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 →
|
|
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=
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
93
|
+
public class AuditRecorder {
|
|
99
94
|
|
|
100
|
-
private final
|
|
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
|
|
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
|
-
|
|
103
|
+
String context = new JsonObject()
|
|
109
104
|
.put("trace_id", traceId)
|
|
110
|
-
.put("span_id", spanId)
|
|
111
|
-
|
|
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
|
-
|
|
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
|
|
143
|
+
Annotate the entry point of each slice — `Handler.process()` — not every method:
|
|
136
144
|
|
|
137
145
|
```java
|
|
138
|
-
//
|
|
146
|
+
// the slice's Handler — one span per business operation
|
|
147
|
+
@Transactional
|
|
139
148
|
@WithSpan("usecase.registerUser")
|
|
140
|
-
public
|
|
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.<
|
|
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
|
|
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.<
|
|
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 | `
|
|
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 `
|
|
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)
|
|
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.
|
|
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
|
|
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 `
|
|
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
|
-
|
|
85
|
-
|
|
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(@
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
125
|
-
2. `<Resource>Resource` class with full annotations + operationIds
|
|
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`).
|