bmad-method-quarkus 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/package.json +1 -1
  2. package/removals.txt +10 -0
  3. package/src/bmm-skills/agents/bmad-quarkus-build/SKILL.md +19 -16
  4. package/src/bmm-skills/agents/bmad-quarkus-build/customize.toml +7 -5
  5. package/src/bmm-skills/agents/bmad-quarkus-build/skills/bqa-setup/assets/module-help.csv +0 -1
  6. package/src/bmm-skills/agents/bmad-quarkus-build/skills/bqa-setup/assets/module.yaml +2 -9
  7. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-error-handling-i18n/SKILL.md +61 -27
  8. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-grpc-services/SKILL.md +60 -41
  9. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-hexagonal-core/SKILL.md +504 -215
  10. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-kafka-messaging/SKILL.md +199 -34
  11. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-observability-otel/SKILL.md +47 -31
  12. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-openapi-tmforum/SKILL.md +42 -25
  13. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-sql-jdbc-agroal/SKILL.md +150 -87
  14. package/src/bmm-skills/module.yaml +0 -7
  15. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/SKILL.md +0 -108
  16. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/customize.toml +0 -62
  17. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/references/architecture-review.md +0 -17
  18. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/references/prompt-quality-canon.md +0 -79
  19. package/src/bmm-skills/agents/bmad-quarkus-dev/.memlog.md +0 -12
  20. package/src/bmm-skills/agents/bmad-quarkus-dev/.memlog.md:Zone.Identifier +0 -0
  21. package/src/bmm-skills/agents/bmad-quarkus-dev/SKILL.md +0 -86
  22. package/src/bmm-skills/agents/bmad-quarkus-dev/SKILL.md:Zone.Identifier +0 -0
  23. package/src/bmm-skills/agents/bmad-quarkus-dev/customize.toml +0 -37
  24. package/src/bmm-skills/agents/bmad-quarkus-dev/customize.toml:Zone.Identifier +0 -0
  25. package/src/bmm-skills/agents/bmad-quarkus-dev/references/enrich-stories.md +0 -19
  26. package/src/bmm-skills/agents/bmad-quarkus-dev/references/enrich-stories.md:Zone.Identifier +0 -0
  27. package/src/bmm-skills/agents/bmad-quarkus-dev/references/prompt-quality-canon.md +0 -79
  28. package/src/bmm-skills/agents/bmad-quarkus-dev/references/prompt-quality-canon.md:Zone.Identifier +0 -0
  29. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/SKILL.md +0 -80
  30. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/SKILL.md:Zone.Identifier +0 -0
  31. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module-help.csv +0 -9
  32. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module-help.csv:Zone.Identifier +0 -0
  33. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module.yaml +0 -16
  34. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module.yaml:Zone.Identifier +0 -0
  35. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/cleanup-legacy.py +0 -287
  36. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/cleanup-legacy.py:Zone.Identifier +0 -0
  37. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-config.py +0 -441
  38. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-config.py:Zone.Identifier +0 -0
  39. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-help-csv.py +0 -246
  40. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-help-csv.py:Zone.Identifier +0 -0
  41. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md +0 -181
  42. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md:Zone.Identifier +0 -0
  43. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md +0 -141
  44. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md:Zone.Identifier +0 -0
  45. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md +0 -392
  46. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md:Zone.Identifier +0 -0
  47. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md +0 -148
  48. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md:Zone.Identifier +0 -0
  49. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md +0 -180
  50. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md:Zone.Identifier +0 -0
  51. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md +0 -128
  52. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md:Zone.Identifier +0 -0
  53. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md +0 -245
  54. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md:Zone.Identifier +0 -0
@@ -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) and the property-driven relay mode (`app.messaging.outbox.relay-mode` = debezium | poller | direct) that decides whether Debezium, a scheduled poller or a direct Emitter publishes the row.
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,23 +42,24 @@ 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.
48
- 2. A relay publishes to Kafka (Debezium outbox connector preferred; scheduled poller as fallback for environments without Kafka Connect).
47
+ 1. The slice `Handler` (in the SAME JDBC transaction as the state change) inserts into the outbox table.
48
+ 2. A relay publishes to Kafka. **Which relay is a runtime property, not a code change** — Debezium outbox connector, in-process scheduled poller, or direct post-commit `Emitter`. See *Relay mode* below.
49
49
 
50
- Standard outbox table (one per service):
50
+ Standard outbox table (one per service), created by a versioned migration in `db/migration` — never at runtime, and independently of whether the Flyway flag lets the app apply it (see quarkus-sql-jdbc-agroal skill §9):
51
51
 
52
52
  ```sql
53
53
  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" (CloudEvents type)
58
+ topic VARCHAR(255) NOT NULL, -- "alva.iam.user.registered.v1" — resolved once, at insert
58
59
  payload JSONB NOT NULL,
59
60
  traceparent VARCHAR(64) NOT NULL, -- captured at insert time (W3C Trace Context)
60
61
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
61
- published_at TIMESTAMPTZ NULL -- poller mode only
62
+ published_at TIMESTAMPTZ NULL -- poller/direct modes only (see Relay mode)
62
63
  );
63
64
  ```
64
65
 
@@ -78,31 +79,189 @@ CREATE TABLE outbox_event (
78
79
 
79
80
  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
81
 
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`.
82
+ 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. (`OutboxDispatcher` below is not that port — it publishes nothing in the default mode and never appears in the event's write path.)
82
83
 
83
- Fire-and-forget technical messages (metrics, notifications with no consistency requirement) MAY use a direct `Emitter`:
84
+ ```java
85
+ private UUID execution(CreatePartyIndividualRequestDto request) {
86
+ try (Connection conn = dataSource.getConnection()) {
87
+ UUID partyId = sql.insertParty(conn, ...);
88
+ UUID eventId = sql.insertOutboxEvent(conn, partyId, "party",
89
+ "com.alva.customer.party.created.v1", "alva.customer.party.created.v1",
90
+ payloadJson(partyId, request), traceparent());
91
+ dispatcher.dispatchAfterCommit(eventId); // no-op unless relay-mode=direct (see Relay mode)
92
+ return partyId;
93
+ } catch (SQLException e) {
94
+ throw SqlStateTranslator.translate("PTY-500-001", e);
95
+ }
96
+ }
97
+ ```
98
+
99
+ `execution()` still returns the slice's internal result (`partyId`) for `getResult()`, `@Transactional` still sits on `process()` alone, and the method order of the hexagonal-core skill is unchanged. `OutboxDispatcher` is injected in the Handler's `common/` bean slot, next to `dataSource` and `sql`.
100
+
101
+ That is the whole dual-write guarantee — one connection, one transaction, no port indirection. Nothing in the slice knows how the row reaches Kafka: `common/messaging` owns the single publish path (`OutboxDispatcher` + `OutboxRelayJob`) and it is the only code that talks to Kafka on the produce side.
102
+
103
+ Fire-and-forget technical messages (metrics, notifications with no consistency requirement) MAY use a direct `Emitter` — but it lives in a **`common/` capability bean**, never in a slice. `@Channel`/`Emitter` are `org.eclipse.microprofile.reactive.messaging` types, and the `handlerKnowsNoTransport` ArchUnit rule forbids them in a `Handler`:
84
104
 
85
105
  ```java
106
+ // common/audit/AuditRecorder.java — the Handler injects AuditRecorder, not the Emitter
86
107
  @Inject @Channel("audit-out") Emitter<AuditEventPayload> audit;
87
108
  ```
88
109
 
110
+ ### Relay mode (property-driven, mandatory)
111
+
112
+ The outbox row is **always** written in the transaction — that never depends on a flag. What a flag selects is *who publishes it*, so the same `Handler` code runs unchanged in every environment:
113
+
114
+ ```properties
115
+ # debezium | poller | direct
116
+ app.messaging.outbox.relay-mode=debezium
117
+ ```
118
+
119
+ | Mode | Publisher | Use in |
120
+ |---|---|---|
121
+ | `debezium` | Debezium Outbox Event Router (Kafka Connect), out of process | prod / staging — the default |
122
+ | `poller` | `OutboxRelayJob` `@Scheduled` inside the service | environments with Kafka but no Kafka Connect |
123
+ | `direct` | `OutboxDispatcher` emits **after commit**, in process | local dev, integration tests, single-node deployments |
124
+
125
+ `direct` is NOT a licence to dual-write: the row is committed first, the emit is deferred to a post-commit synchronization, and a lost emit is recovered by re-running in `poller` mode (`published_at IS NULL` is still the source of truth). An emit that never reaches Kafka must never fail `process()` — the caller already has a committed result, so the dispatcher logs the failure and leaves `published_at` null for the next relay to pick up; it does not rethrow and it does not build an error response (see quarkus-error-handling-i18n skill). All three modes wrap the emit in the `outbox.publish` span (see quarkus-observability-otel skill).
126
+
127
+ ```java
128
+ @ConfigMapping(prefix = "app.messaging.outbox")
129
+ public interface OutboxConfig {
130
+ @WithDefault("debezium") RelayMode relayMode();
131
+ enum RelayMode { DEBEZIUM, POLLER, DIRECT }
132
+ }
133
+ ```
134
+
135
+ **Single dispatch point.** `common/messaging` owns one `OutboxDispatcher`; it is the only code on the produce side that touches the `Emitter`, and it is a no-op unless the mode says otherwise:
136
+
137
+ ```java
138
+ @ApplicationScoped
139
+ public class OutboxDispatcher {
140
+
141
+ @Inject OutboxConfig config;
142
+ @Inject OutboxSql sql; // common/messaging own Sql, same Connection-first contract
143
+ @Inject DataSource dataSource; // its own connection — the Handler's is closed by now
144
+ @Inject TransactionSynchronizationRegistry txRegistry;
145
+ @Inject @Channel("outbox-out") Emitter<JsonNode> emitter;
146
+
147
+ /** Called from execution(); the emit itself is deferred until the JTA transaction commits.
148
+ No-op unless relay-mode=direct. */
149
+ public void dispatchAfterCommit(UUID eventId) {
150
+ if (config.relayMode() != RelayMode.DIRECT) return; // Debezium or the job will pick it up
151
+ txRegistry.registerInterposedSynchronization(new Synchronization() {
152
+ @Override public void beforeCompletion() { }
153
+ @Override public void afterCompletion(int status) {
154
+ if (status == Status.STATUS_COMMITTED) { // rolled back → row never existed, nothing to emit
155
+ sql.findUnpublished(eventId).ifPresent(OutboxDispatcher.this::publish);
156
+ }
157
+ }
158
+ });
159
+ }
160
+
161
+ void publish(OutboxRow row) {
162
+ emitter.send(Message.of(row.payload())
163
+ .addMetadata(OutgoingKafkaRecordMetadata.builder()
164
+ .withTopic(row.topic()) // one channel, topic per event type
165
+ .withKey(row.aggregateId())
166
+ .withHeaders(new RecordHeaders()
167
+ .add("traceparent", row.traceparent().getBytes(UTF_8)))
168
+ .build())
169
+ .addMetadata(OutgoingCloudEventMetadata.builder()
170
+ .withId(row.id().toString())
171
+ .withType(row.eventType())
172
+ .withSource(URI.create(SOURCE))
173
+ .withSubject(row.aggregateId())
174
+ .build())
175
+ .withAck(() -> markPublished(row.id()))); // published_at = now()
176
+ }
177
+ }
178
+ ```
179
+
180
+ The Handler never touches an `Emitter`, a topic or a Kafka header — it hands over an id and the dispatcher decides whether anything happens at all. `dispatchAfterCommit()` is safe to call from inside `execution()` precisely because it only *registers* a synchronization: nothing reaches Kafka until the container commits `process()`. Never call `publish()` from a slice.
181
+
182
+ **Scheduler flag.** `OutboxRelayJob` is disabled by config expression, not by commenting code — Quarkus treats `off`/`disabled` as "do not schedule this method":
183
+
184
+ ```java
185
+ @ApplicationScoped
186
+ public class OutboxRelayJob {
187
+
188
+ @Inject OutboxDispatcher dispatcher;
189
+ @Inject OutboxSql sql;
190
+
191
+ @Scheduled(every = "${app.messaging.outbox.poller.every:off}",
192
+ concurrentExecution = ConcurrentExecution.SKIP)
193
+ void relay() {
194
+ sql.lockUnpublishedBatch(batchSize) // SELECT ... FOR UPDATE SKIP LOCKED, ORDER BY created_at
195
+ .forEach(dispatcher::publish); // ack callback sets published_at
196
+ }
197
+ }
198
+ ```
199
+
200
+ ```properties
201
+ # prod: Debezium owns the relay, the job never schedules
202
+ app.messaging.outbox.relay-mode=debezium
203
+ app.messaging.outbox.poller.every=off
204
+
205
+ # %dev / %test: no Kafka Connect available
206
+ %dev.app.messaging.outbox.relay-mode=direct
207
+ %dev.app.messaging.outbox.poller.every=off
208
+
209
+ # fallback environment: in-process poller
210
+ %onprem.app.messaging.outbox.relay-mode=poller
211
+ %onprem.app.messaging.outbox.poller.every=5s
212
+ app.messaging.outbox.poller.batch-size=200
213
+
214
+ # one outgoing channel for every event type; the topic comes from the outbox row.
215
+ # Left configured and simply never used when relay-mode=debezium.
216
+ mp.messaging.outgoing.outbox-out.connector=smallrye-kafka
217
+ mp.messaging.outgoing.outbox-out.topic=alva.unrouted # placeholder, always overridden per message
218
+ mp.messaging.outgoing.outbox-out.cloud-events=true
219
+ mp.messaging.outgoing.outbox-out.cloud-events-source=//alva/customer
220
+ mp.messaging.outgoing.outbox-out.key.serializer=org.apache.kafka.common.serialization.StringSerializer
221
+ mp.messaging.outgoing.outbox-out.value.serializer=io.quarkus.kafka.client.serialization.ObjectMapperSerializer
222
+ ```
223
+
224
+ Per-event outgoing channels (`user-registered-out`, one channel per topic, as in the CloudEvents section above) remain valid for fire-and-forget technical messages. Domain events go through the single `outbox-out` channel — one channel, topic per message — so adding an event never means adding messaging config.
225
+
226
+ Rules:
227
+ - The three modes are **mutually exclusive by configuration** — `relay-mode=debezium` with a scheduled poller means two publishers and duplicate events. Validate at startup (`@Startup` observer: fail fast if `relayMode != POLLER` and `poller.every` is not `off`).
228
+ - `ORDER BY created_at` + `SKIP LOCKED` in `poller`; multiple replicas are safe, per-entity ordering is preserved because the partition key is the aggregate id.
229
+ - `published_at` is only maintained in `poller`/`direct`; in `debezium` mode the router deletes (or ignores) the row and the column stays null — do not build alerts on it without checking the mode.
230
+ - `common/messaging` is **not** `common/client`: Mutiny stays quarantined there (hexagonal-core `reactiveIsQuarantined`). Use the MicroProfile `Emitter`, never `MutinyEmitter`.
231
+ - `OutboxSql` is the one `Sql` class whose callers are not a `Handler` — the dispatcher and the job own the `Connection` in its place. That exception is written into the `onlyHandlersTouchSql` ArchUnit rule, not left for the build to discover.
232
+ - Consumers are unaffected: the same CloudEvent lands on the same topic in all three modes. That is the contract the flag must not break — a contract test asserts headers/topic are identical between `direct` and Debezium output.
233
+
234
+ **Debezium mode** maps outbox columns to the envelope in the connector, not in Java:
235
+
236
+ ```json
237
+ "transforms": "outbox",
238
+ "transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter",
239
+ "transforms.outbox.table.field.event.id": "id",
240
+ "transforms.outbox.table.field.event.key": "aggregate_id",
241
+ "transforms.outbox.table.field.event.type": "event_type",
242
+ "transforms.outbox.table.field.event.payload": "payload",
243
+ "transforms.outbox.table.fields.additional.placement": "traceparent:header",
244
+ "transforms.outbox.route.by.field": "topic",
245
+ "transforms.outbox.route.topic.replacement": "${routedByValue}"
246
+ ```
247
+
89
248
  ### Naming
90
249
 
91
250
  Canonical rules in the quarkus-hexagonal-core skill; messaging specifics:
92
251
 
93
252
  | Artifact | Convention | Example |
94
253
  |---|---|---|
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` |
254
+ | Wire payload DTO | `<Event>Payload`, in the producing/consuming slice's `dto/` | `UserRegisteredPayload` |
255
+ | Consumer | `<Event>Consumer`, in the slice folder | `UserRegisteredConsumer` |
256
+ | Outbox write | a method on the slice's own `Sql` | `insertOutboxEvent(conn, ...)` |
257
+ | Outbox relay job | `<Task>Job`, in `common/messaging` | `OutboxRelayJob` |
258
+ | Outbox dispatcher | one per service, in `common/messaging` | `OutboxDispatcher` |
259
+ | Relay-mode config | `@ConfigMapping` in `common/messaging` | `OutboxConfig.RelayMode` |
260
+ | Generic outbox channel | single outgoing channel, dynamic topic | `outbox-out` |
102
261
  | 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` |
262
+ | CloudEvents `type` | `<javaPackageStyle>.<entity>.<event>.v<major>` | `com.alva.iam.user.registered.v1` |
104
263
 
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.
264
+ `<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
265
 
107
266
  ## Consuming
108
267
 
@@ -110,25 +269,29 @@ Keep the domain event (`UserRegistered`) and the wire payload (`UserRegisteredPa
110
269
  @ApplicationScoped
111
270
  public class UserRegisteredConsumer {
112
271
 
272
+ @Inject RegisterUserFromEventHandler handler; // the slice's business logic
273
+
113
274
  @Incoming("user-registered-in")
114
275
  @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
276
+ public void on(UserRegisteredPayload payload) {
277
+ handler.process(toRequest(payload)); // idempotency + side effects, one transaction
117
278
  }
118
279
  }
119
280
  ```
120
281
 
282
+ 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.
283
+
121
284
  ```properties
122
285
  mp.messaging.incoming.user-registered-in.connector=smallrye-kafka
123
- mp.messaging.incoming.user-registered-in.topic=alva.bc05.user.registered.v1
286
+ mp.messaging.incoming.user-registered-in.topic=alva.iam.user.registered.v1
124
287
  mp.messaging.incoming.user-registered-in.group.id=${quarkus.application.name}
125
288
  mp.messaging.incoming.user-registered-in.auto.offset.reset=earliest
126
289
  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
290
+ mp.messaging.incoming.user-registered-in.dead-letter-queue.topic=alva.iam.user.registered.v1.dlq
128
291
  ```
129
292
 
130
293
  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.
294
+ - **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
295
  - Consumer group = service name; stable across deployments.
133
296
  - 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
297
  - Poison-pill safety: deserialization failures also route to DLQ (`...deserialization-failure-handler` or failure strategy), never block the partition.
@@ -141,8 +304,10 @@ Rules:
141
304
 
142
305
  ## Checklist for a new event
143
306
 
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.
148
- 5. Contract test: serialize/deserialize round-trip against the JSON Schema.
307
+ 1. Define `<Event>Payload` in the slice's `dto/` + its JSON Schema in `contracts/`; name the topic and channel per convention.
308
+ 2. Producer: `insertOutboxEvent` method on the slice's `Sql`, called from `Handler.execution()` on the shared `Connection`, storing the resolved `topic`; `process()` passes the returned id to `OutboxDispatcher.dispatchAfterCommit()`.
309
+ 3. Relay: set `app.messaging.outbox.relay-mode` per environment and keep `poller.every=off` in every mode but `poller`; register the Debezium routing config where the mode is `debezium`.
310
+ 4. `@RegisterForReflection` on the payload; `traceparent` + `traceContext` captured from `Span.current()` at insert time.
311
+ 5. Consumer: `<Event>Consumer` in the slice folder delegating to the `Handler`, idempotency inside the transaction, DLQ topic + owner + alert, `@Blocking`.
312
+ 6. Contract test: serialize/deserialize round-trip against the JSON Schema, plus identical topic/headers between `direct` and Debezium output.
313
+ 7. 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`).