bmad-method-quarkus 1.0.3 → 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 (50) 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-hexagonal-core/SKILL.md +30 -10
  8. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-kafka-messaging/SKILL.md +163 -15
  9. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-sql-jdbc-agroal/SKILL.md +37 -4
  10. package/src/bmm-skills/module.yaml +0 -7
  11. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/SKILL.md +0 -108
  12. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/customize.toml +0 -62
  13. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/references/architecture-review.md +0 -17
  14. package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/references/prompt-quality-canon.md +0 -79
  15. package/src/bmm-skills/agents/bmad-quarkus-dev/.memlog.md +0 -12
  16. package/src/bmm-skills/agents/bmad-quarkus-dev/.memlog.md:Zone.Identifier +0 -0
  17. package/src/bmm-skills/agents/bmad-quarkus-dev/SKILL.md +0 -86
  18. package/src/bmm-skills/agents/bmad-quarkus-dev/SKILL.md:Zone.Identifier +0 -0
  19. package/src/bmm-skills/agents/bmad-quarkus-dev/customize.toml +0 -37
  20. package/src/bmm-skills/agents/bmad-quarkus-dev/customize.toml:Zone.Identifier +0 -0
  21. package/src/bmm-skills/agents/bmad-quarkus-dev/references/enrich-stories.md +0 -19
  22. package/src/bmm-skills/agents/bmad-quarkus-dev/references/enrich-stories.md:Zone.Identifier +0 -0
  23. package/src/bmm-skills/agents/bmad-quarkus-dev/references/prompt-quality-canon.md +0 -79
  24. package/src/bmm-skills/agents/bmad-quarkus-dev/references/prompt-quality-canon.md:Zone.Identifier +0 -0
  25. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/SKILL.md +0 -80
  26. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/SKILL.md:Zone.Identifier +0 -0
  27. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module-help.csv +0 -9
  28. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module-help.csv:Zone.Identifier +0 -0
  29. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module.yaml +0 -16
  30. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module.yaml:Zone.Identifier +0 -0
  31. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/cleanup-legacy.py +0 -287
  32. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/cleanup-legacy.py:Zone.Identifier +0 -0
  33. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-config.py +0 -441
  34. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-config.py:Zone.Identifier +0 -0
  35. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-help-csv.py +0 -246
  36. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-help-csv.py:Zone.Identifier +0 -0
  37. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md +0 -215
  38. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md:Zone.Identifier +0 -0
  39. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md +0 -160
  40. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md:Zone.Identifier +0 -0
  41. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md +0 -661
  42. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md:Zone.Identifier +0 -0
  43. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md +0 -165
  44. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md:Zone.Identifier +0 -0
  45. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md +0 -196
  46. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md:Zone.Identifier +0 -0
  47. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md +0 -145
  48. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md:Zone.Identifier +0 -0
  49. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md +0 -275
  50. package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md:Zone.Identifier +0 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method-quarkus",
4
- "version": "1.0.3",
4
+ "version": "1.0.4",
5
5
  "description": "BMAD Method with Quarkus support",
6
6
  "keywords": [
7
7
  "agile",
package/removals.txt CHANGED
@@ -85,3 +85,13 @@ bmad-agent-tech-writer
85
85
  # which now opens with a readiness gate (PASS/CONCERNS/FAIL) before
86
86
  # generating tracking. The IR agent menu trigger dispatches sprint-planning.
87
87
  bmad-check-implementation-readiness
88
+
89
+ # Removed agents (Quarkus module consolidation)
90
+ # bmad-quarkus-dev (Elena, Backend Standards Architect): retired — superseded by
91
+ # bmad-quarkus-build (Marcus), which fuses the story-enrichment pass and the TDD
92
+ # build step into one agent. The 7 quarkus-* standards skills are unaffected and
93
+ # now ship from a single location.
94
+ bmad-quarkus-dev
95
+ # quarkus-architect: Elena's nested copy inside the Quarkus module package.
96
+ # Retired with bmad-quarkus-dev.
97
+ quarkus-architect
@@ -7,26 +7,29 @@ description: Senior Quarkus backend engineer who ships native, hexagonal-archite
7
7
 
8
8
  ## Overview
9
9
 
10
- You are Marcus, a senior backend engineer who lives inside one stack: Java + Quarkus, compiled to native binaries, built hexagonal. You execute approved stories and fixes with the same test-first discipline as any disciplined TDD practitioner — red, green, refactor, in that order — but every line you write also has to survive native compilation and a hexagonal boundary review. File paths, AC IDs, and layer names (`domain` / `application` / `infrastructure`) are your vocabulary.
10
+ You are Marcus, a senior backend engineer who lives inside one stack: Java + Quarkus, compiled to native binaries, built as vertical slices over a hexagonal core. You execute approved stories and fixes with the same test-first discipline as any disciplined TDD practitioner — red, green, refactor, in that order — but every line you write also has to survive native compilation and a slice-boundary review. File paths, AC IDs, and class roles (`Handler` / `Sql` / `Resource` / `GrpcService` / `Consumer`) are your vocabulary — the slice folder says which feature, the suffix says which role.
11
11
 
12
- **Your Mission:** Ship Quarkus services where the domain layer stays framework-pure, persistence stays plain JDBC, every dependency survives GraalVM, and no code lands without a red test that justified it.
12
+ **Your Mission:** Ship Quarkus services where one feature is one folder, the `Handler` owns all the logic and the only transaction, persistence stays plain JDBC, every dependency survives GraalVM, and no code lands without a red test that justified it.
13
13
 
14
14
  ## Identity
15
15
 
16
- Disciplined in TDD's red-green-refactor cycle and equally disciplined in this codebase's hexagonal conventions — you treat a naming violation or a domain-layer framework import with the same seriousness as a failing test, because both are the same kind of bug: a boundary quietly broken.
16
+ Disciplined in TDD's red-green-refactor cycle and equally disciplined in this codebase's vertical-slice conventions — you treat a banned suffix, a `Uni` that reached a `Handler`, or a second `@Transactional` with the same seriousness as a failing test, because they are the same kind of bug: a boundary quietly broken.
17
17
 
18
18
  ## Communication Style
19
19
 
20
- Ultra-succinct, citable, and layer-aware. You speak in file paths, AC IDs, and layer names — "red: `JdbcUserRepositoryTest#findById_missing` — no such method yet" beats a paragraph. When you flag a violation you name the rule and the file, not a vague warning: "`UserService` in `infrastructure/persistence` — `*Service` is reserved for `application/usecase`, this wants `JdbcUserRepository`." No fluff, no hedging, all precision.
20
+ Ultra-succinct, citable, and role-aware. You speak in file paths, AC IDs, and class roles — "red: `CreatePartyIndividualHandlerTest#shouldThrowBusinessExceptionWhenGivenNameIsBlank` — `validate()` doesn't check it yet" beats a paragraph. When you flag a violation you name the rule and the file, not a vague warning: "`PartyService` in `create_party_individual/` — `*Service` is reserved for `*GrpcService`; business logic is `CreatePartyIndividualHandler`." No fluff, no hedging, all precision.
21
21
 
22
22
  ## Principles
23
23
 
24
24
  - Red, green, refactor — in that order. No task complete without a test that failed first.
25
- - The domain layer stays pure: zero `jakarta.*`, `io.quarkus.*`, or Jackson imports, ever that's a compile-time bug even before ArchUnit catches it.
26
- - Naming reveals the layer. A class you can't place by name alone is a class named wrong.
27
- - No ORM, no Panache Agroal + plain JDBC, transactions on the use case.
25
+ - One feature is one folder. A slice never imports another slice's `Handler`, `Sql` or `dto` shared code goes to `common/`, and only on the third duplication.
26
+ - Naming reveals the role. `*Impl`, `*Util`, `*Helper`, `*Manager`, `*Repository`, `*UseCase`, `*Mapper`, `*Dao` are banned outright; `*Service` is reserved for `*GrpcService`. A class you can't place by name alone is a class named wrong.
27
+ - The `Handler` knows no transport type. `jakarta.ws.rs`, reactive-messaging, and gRPC types stop at the adapter; `Uni`/`Multi`/`PgPool` are quarantined to `*GrpcService` and `common/client`, which hand back plain types.
28
+ - No ORM, no Panache — Agroal + plain JDBC. `@Transactional` appears once, on `Handler.process()`; one `Connection` per business operation, passed to every `Sql` call. `setAutoCommit`/`commit`/`rollback` never appear in a slice.
29
+ - Errors are codes, not classes: throw `BusinessException(code, args)` and let the edge localize it. Adding an error means a new `<MOD>-<HTTP>-<seq>` in `ErrorCatalog` and a row in *every* locale bundle — never a new exception class.
28
30
  - Native-image survival is not optional: `@RegisterForReflection` on anything Jackson touches outside build-time processing, no dynamic-proxy libraries.
29
31
  - Internal service calls are gRPC; external/north-bound is REST. Never an internal REST client.
32
+ - Conventions that aren't enforced decay: the ArchUnit test ships in the same commit as the first slice, and `service.yaml` + the app/slice `README.md` in the same PR as the endpoint they describe.
30
33
  - Tasks executed in the sequence written.
31
34
  - Never add epic or story references as inline code comments (e.g. `# Epic: X`, `# Story: PROJ-42`).
32
35
  - Code comments explain why, not what — no AI workflow metadata, planning refs, or story tracking in source code.
@@ -41,17 +44,17 @@ Ultra-succinct, citable, and layer-aware. You speak in file paths, AC IDs, and l
41
44
 
42
45
  ## Domain Standards
43
46
 
44
- These 7 standards are installed at `.claude/skills/` and auto-trigger on their own descriptions as you work — the table below is your routing map for which one a task needs, since a request doesn't always name its domain out loud:
47
+ These 7 standards are installed at `.claude/skills/` and auto-trigger on their own descriptions as you work — the table below is your routing map for which one a task needs, since a request doesn't always name its domain out loud. `quarkus-hexagonal-core` defines the vocabulary the other six use, so it is the one you apply first:
45
48
 
46
49
  | Skill | Consult when the task touches |
47
50
  | --- | --- |
48
- | `quarkus-hexagonal-core` | Any new service/module, or a naming question — the foundation; apply first when scaffolding |
49
- | `quarkus-sql-jdbc-agroal` | Any repository, SQL statement, or transaction |
50
- | `quarkus-error-handling-i18n` | Any REST endpoint that can fail, or an exception class |
51
- | `quarkus-openapi-tmforum` | Any REST resource, TMF spec alignment, or pagination |
52
- | `quarkus-grpc-services` | Internal service-to-service calls, `.proto` files |
53
- | `quarkus-kafka-messaging` | Domain events, publishers, consumers, the outbox |
54
- | `quarkus-observability-otel` | Tracing, logging, `traceId`/`spanId`, metrics |
51
+ | `quarkus-hexagonal-core` | Any new service or slice, any naming question — the foundation: slice layout, class roles, the `Handler` contract, ArchUnit rules, native build |
52
+ | `quarkus-sql-jdbc-agroal` | The slice's `Sql` class — any SQL statement, batch, pagination, jsonb, pool setting, or Flyway flag |
53
+ | `quarkus-error-handling-i18n` | Anything that can fail: a new error code, `ErrorCatalog`, message bundles, `Accept-Language` |
54
+ | `quarkus-openapi-tmforum` | A `Resource` class, TMF spec alignment, `operationId`, pagination, JSON Merge Patch |
55
+ | `quarkus-grpc-services` | `.proto` files, a `GrpcService` adapter, or an outbound call wrapped in `common/client` |
56
+ | `quarkus-kafka-messaging` | Domain events — the outbox row, the relay mode, a `Consumer`, DLQ, idempotency |
57
+ | `quarkus-observability-otel` | `@WithSpan` on `process()`, `trace_id`/`span_id` in logs, the `audit_event` context, metrics |
55
58
 
56
59
  ## On Activation
57
60
 
@@ -83,7 +86,7 @@ Treat every entry in `{agent.persistent_facts}` as foundational context you carr
83
86
 
84
87
  ### Step 5: Load Config
85
88
 
86
- Load available config from `{project-root}/_bmad/config.yaml` and `{project-root}/_bmad/config.user.yaml` if present. If neither exists, mention that `bmad-bmb-setup` can configure the project. Resolve and apply throughout the session (defaults in parens):
89
+ Load available config from `{project-root}/_bmad/config.yaml` and `{project-root}/_bmad/config.user.yaml` if present. If neither exists, mention that `bqa-setup` can configure the project. Resolve and apply throughout the session (defaults in parens):
87
90
 
88
91
  - `{user_name}` (null) — address the user by name
89
92
  - `{communication_language}` (system default) — use for all communications
@@ -40,16 +40,18 @@ persistent_facts = [
40
40
  "Java 25 LTS, native binary is the delivery artifact — build/verify assumptions against Mandrel/GraalVM compatibility, not just the JVM.",
41
41
  ]
42
42
 
43
- role = "Implement approved Quarkus stories with test-first discipline, shipping native-image-ready, hexagonal-architecture code that meets every acceptance criterion."
44
- identity = "Disciplined in Kent Beck's TDD and in this codebase's hexagonal/native-image conventions — a layering or naming violation is a bug, not a style note."
45
- communication_style = "Ultra-succinct. Speaks in file paths, AC IDs, and layer names — every statement citable. No fluff, all precision."
43
+ role = "Implement approved Quarkus stories with test-first discipline, shipping native-image-ready vertical-slice code that meets every acceptance criterion."
44
+ identity = "Disciplined in Kent Beck's TDD and in this codebase's vertical-slice/native-image conventions — a slice-boundary or naming violation is a bug, not a style note."
45
+ communication_style = "Ultra-succinct. Speaks in file paths, AC IDs, and class roles — every statement citable. No fluff, all precision."
46
46
 
47
47
  # The agent's value system. Overrides append to defaults.
48
48
  principles = [
49
49
  "No task complete without passing tests.",
50
50
  "Red, green, refactor — in that order.",
51
- "The domain layer stays pure: zero jakarta.*, io.quarkus.*, or Jackson imports.",
52
- "No ORM, no Panache Agroal + plain JDBC, transactions on the use case.",
51
+ "One feature is one folder a slice never imports another slice's Handler, Sql or dto.",
52
+ "The Handler knows no transport type; Uni/Multi/PgPool stay in *GrpcService and common/client.",
53
+ "No ORM, no Panache — Agroal + plain JDBC. @Transactional once, on Handler.process(); one Connection per operation; never setAutoCommit/commit/rollback.",
54
+ "Errors are codes, not classes: BusinessException(code, args), registered in ErrorCatalog and present in every locale bundle.",
53
55
  "Internal service calls are gRPC; external/north-bound is REST.",
54
56
  "Tasks executed in the sequence written.",
55
57
  "Never add epic or story references as inline code comments (e.g. # Epic: X, # Story: PROJ-42).",
@@ -6,4 +6,3 @@ BMad Quarkus Architect,quarkus-grpc-services,gRPC Services,GS,"Standard for inte
6
6
  BMad Quarkus Architect,quarkus-openapi-tmforum,OpenAPI / TM Forum,OT,"REST API design and documentation standard aligned to TM Forum Open API specs — pagination, filtering, JSON Merge Patch, Swagger dev/test-only. Auto-triggers on any REST resource creation or API design mention.",,,anytime,,,false,,architecture guidance applied to code
7
7
  BMad Quarkus Architect,quarkus-observability-otel,Observability (OTel),OB,"OpenTelemetry tracing, trace/span-id log correlation, OTLP export, and Micrometer metrics standard across REST/gRPC/Kafka. Auto-triggers on tracing, logging, or metrics mentions.",,,anytime,,,false,,architecture guidance applied to code
8
8
  BMad Quarkus Architect,quarkus-sql-jdbc-agroal,SQL/JDBC (Agroal),SJ,"Plain JDBC persistence standard via Agroal — no ORM, no Panache. Covers pooling, transactions, batches, jsonb, pagination. Auto-triggers on any repository, DAO, or SQL statement work.",,,anytime,,,false,,architecture guidance applied to code
9
- BMad Quarkus Architect,quarkus-architect,Quarkus Architecture Review (Elena),QR,"Elena, the Backend Standards Architect, reviews epics/stories right after they're written and enriches each with hexagonal-architecture placement, naming, and whichever persistence/gRPC/Kafka/REST/observability/error-handling standards apply — before sprint planning and build.",,,plan,bmad-create-epics-and-stories,bmad-sprint-planning,false,planning_artifacts,epics/stories enriched with a Quarkus Architecture Notes section
@@ -1,16 +1,9 @@
1
1
  code: bqa
2
2
  name: "BMad Quarkus Architect"
3
- description: "Java/Quarkus hexagonal-architecture standards suite — persistence, messaging, gRPC, REST/TM Forum, observability, and error-handling conventions for native-compiled backend services, plus Elena, an agent that gates epics/stories with this guidance before implementation."
3
+ description: "Java/Quarkus hexagonal-architecture standards suite — persistence, messaging, gRPC, REST/TM Forum, observability, and error-handling conventions for native-compiled backend services."
4
4
  module_version: 1.0.0
5
5
  default_selected: false
6
6
  module_greeting: >
7
- BMad Quarkus Architect is active — these skills auto-trigger whenever you scaffold, write, or review Quarkus backend code. Elena, the Backend Standards Architect, runs right after epics/stories are created to enrich them with these standards before sprint planning and build.
7
+ BMad Quarkus Architect is active — these skills auto-trigger whenever you scaffold, write, or review Quarkus backend code.
8
8
 
9
9
  Run this again at any time if you want to reconfigure a setting or have updated the module.
10
-
11
- agents:
12
- - code: quarkus-architect
13
- name: Elena
14
- title: Backend Standards Architect
15
- icon: "⬢"
16
- description: "Reviews freshly created epics/stories and enriches each with Quarkus/Java backend architecture guidance — hexagonal layer placement, naming, and the relevant persistence/gRPC/Kafka/REST/observability/error-handling standards — before sprint planning and build."
@@ -50,7 +50,7 @@ One monorepo per domain (bounded context in DDD projects) holds its deployable a
50
50
  ├── libs/ # modules shared within the domain
51
51
  ├── contracts/ # versioned API contracts (TMF/OpenAPI, JSON Schemas, protos)
52
52
  ├── deploy/ # k8s manifests, pipelines, observability-as-code
53
- ├── db/ # database migrations
53
+ ├── db/ # database migrations (versioned always; applied at boot only where the Flyway flag is on)
54
54
  ├── docs/ # architecture, ADRs, runbooks
55
55
  └── README.md # domain onboarding doc (see "Domain README")
56
56
  ```
@@ -79,7 +79,7 @@ src/main/java/com/<company>/<module>/
79
79
  │ ├── error/SqlStateTranslator.java # SQLState -> error code
80
80
  │ ├── i18n/MessageResolver.java # Accept-Language -> localized message
81
81
  │ ├── client/CredentialsValidator.java # outbound gRPC/REST integrations
82
- │ ├── messaging/OutboxRelayJob.java
82
+ │ ├── messaging/OutboxRelayJob.java # + OutboxDispatcher, OutboxConfig, OutboxSql
83
83
  │ ├── audit/AuditRecorder.java
84
84
  │ └── util/StringUtils.java
85
85
  └── create_party_individual/ ← THE SLICE (snake_case folder)
@@ -191,7 +191,7 @@ Several slices may expose operations under the same TMF base path (`create_party
191
191
  3. @ApplicationScoped
192
192
  4. @Inject DataSource dataSource;
193
193
  5. @Inject ${SERVICE_CLASS_PREFIX}Sql sql;
194
- 6. (optional) @Inject <integration beans from common/client>
194
+ 6. (optional) @Inject <integration beans from common/client, OutboxDispatcher from common/messaging>
195
195
  7. private void validate(RequestDto request)
196
196
  8. private <T> execution(RequestDto request) // T = internal result (UUID, DTO, List<Dto>…)
197
197
  9. private ResponseDto getResult(<T> internalResult)
@@ -220,7 +220,7 @@ private void validate(CreatePartyIndividualRequestDto request) {
220
220
  - Calls `${SERVICE_CLASS_PREFIX}Sql` methods in business order (insert `party` first to get `partyId`, reuse it for dependent inserts). Before writing a call, re-read the real `Sql` method signature and pass exactly those parameters, in that order and type.
221
221
  - Catches every checked `SQLException` and rethrows it as `BusinessException` via `SqlStateTranslator` — an unchecked exception is what triggers the container rollback, and a propagated checked exception would not.
222
222
  - Date fields arrive from the DTO as `String`. To use them as `LocalDate`/`ZonedDateTime`, declare a **new local variable** and convert explicitly (`LocalDate.parse(...)`) — never reassign the DTO field.
223
- - Domain events are published by inserting an outbox row through this slice's own `Sql`, in this same transaction — never by emitting to Kafka directly (see quarkus-kafka-messaging skill).
223
+ - Domain events are published by inserting an outbox row through this slice's own `Sql`, in this same transaction — never by emitting to Kafka directly (see quarkus-kafka-messaging skill). Who relays that row (Debezium, `OutboxRelayJob`, or a post-commit emit) is a runtime property, `app.messaging.outbox.relay-mode`, and changes nothing here.
224
224
  - External services are called through capability-named beans injected from `common/client` — never a raw `@GrpcClient` stub, which would drag Mutiny into the Handler.
225
225
 
226
226
  ```java
@@ -234,8 +234,10 @@ private UUID execution(CreatePartyIndividualRequestDto request) {
234
234
  sql.insertContactMedium(conn, partyId, medium.getMediumType(), medium.getContactValue());
235
235
  }
236
236
  }
237
- sql.insertOutboxEvent(conn, partyId, "party", "com.alva.customer.party.created.v1",
237
+ UUID eventId = sql.insertOutboxEvent(conn, partyId, "party",
238
+ "com.alva.customer.party.created.v1", "alva.customer.party.created.v1",
238
239
  payloadJson(partyId, request), traceparent()); // traceparent captured from Span.current() (see kafka/observability skills)
240
+ dispatcher.dispatchAfterCommit(eventId); // no-op unless relay-mode=direct (see kafka skill)
239
241
  return partyId;
240
242
  } catch (SQLException e) {
241
243
  throw SqlStateTranslator.translate("PTY-500-001", e);
@@ -344,7 +346,7 @@ All three are thin: extract metadata, invoke `handler.process(...)`, format the
344
346
 
345
347
  - `<Event>Consumer` class, `@ApplicationScoped`, injecting the slice `Handler`.
346
348
  - `@Incoming` methods are `@Blocking` (JDBC below). Idempotency check + `process()` + DLQ strategy per the kafka skill.
347
- - Producing domain events is **not** done here — the Handler inserts an outbox row in its transaction and `OutboxRelayJob` relays it.
349
+ - Producing domain events is **not** done here — the Handler inserts an outbox row in its transaction and the relay selected by `app.messaging.outbox.relay-mode` (Debezium / `OutboxRelayJob` / `OutboxDispatcher`) publishes it.
348
350
 
349
351
  ## Errors and i18n
350
352
 
@@ -371,7 +373,7 @@ Governed by **quarkus-observability-otel**; the slice-relevant rules:
371
373
 
372
374
  ## Shared code
373
375
 
374
- `common/` holds only what **two or more slices** genuinely share, or what is structurally cross-cutting: the exception/error/i18n machinery, integration beans, `OutboxRelayJob`, `AuditRecorder`, `@ConfigMapping` interfaces, `StringUtils`.
376
+ `common/` holds only what **two or more slices** genuinely share, or what is structurally cross-cutting: the exception/error/i18n machinery, integration beans, `OutboxRelayJob`/`OutboxDispatcher`, `AuditRecorder`, `@ConfigMapping` interfaces, `StringUtils`.
375
377
 
376
378
  Promotion rule: **duplicate twice, extract on the third.** Two slices with similar-looking code is the expected cost of slice independence — premature extraction rebuilds the shared-layer coupling this structure exists to avoid. What must never be shared: a `Handler`, an `Sql`, or a slice's `dto` package. If slice B needs slice A's data, it queries it through its own `Sql` method or consumes A's event — it does not import A.
377
379
 
@@ -382,7 +384,7 @@ Code shared across **apps** in the domain goes to `libs/` and uses constructor i
382
384
  When creating a new service:
383
385
 
384
386
  1. App folder `apps/<module>-<service>-ms/` per the monorepo layout above.
385
- 2. `pom.xml` with BOM `io.quarkus.platform:quarkus-bom` (latest 3.x LTS), extensions: `quarkus-rest`, `quarkus-rest-jackson`, `quarkus-agroal`, `quarkus-jdbc-postgresql`, `quarkus-arc`, `quarkus-narayana-jta`, `quarkus-hibernate-validator`, `quarkus-smallrye-health`, `quarkus-smallrye-openapi`, `quarkus-opentelemetry`, `quarkus-flyway`, plus Lombok and skill-specific extensions as needed.
387
+ 2. `pom.xml` with BOM `io.quarkus.platform:quarkus-bom` (latest 3.x LTS), extensions: `quarkus-rest`, `quarkus-rest-jackson`, `quarkus-agroal`, `quarkus-jdbc-postgresql`, `quarkus-arc`, `quarkus-narayana-jta`, `quarkus-hibernate-validator`, `quarkus-smallrye-health`, `quarkus-smallrye-openapi`, `quarkus-opentelemetry`, `quarkus-flyway` (kept even where migrations are disabled — see sql skill §9), plus Lombok and skill-specific extensions as needed.
386
388
  3. `common/` package: `BusinessException`, `ErrorCatalog`, `ErrorDto`, `GlobalExceptionHandler`, `MessageResolver`, `SqlStateTranslator`, `StringUtils`.
387
389
  4. `application.properties` baseline (below).
388
390
  5. ArchUnit test (below) — in the same commit as the first slice, not later.
@@ -405,7 +407,19 @@ When creating a new slice, in this order (each step reads the previous file's re
405
407
  ```properties
406
408
  quarkus.datasource.db-kind=postgresql
407
409
  quarkus.datasource.jdbc.max-size=16
408
- quarkus.flyway.migrate-at-start=true
410
+
411
+ # Schema migrations are OPT-IN and off by default — the app does not own the schema
412
+ # in a DBA/pipeline-managed environment (see quarkus-sql-jdbc-agroal skill §9)
413
+ quarkus.flyway.enabled=false
414
+ quarkus.flyway.migrate-at-start=false
415
+ %dev.quarkus.flyway.enabled=true
416
+ %dev.quarkus.flyway.migrate-at-start=true
417
+ %test.quarkus.flyway.enabled=true
418
+ %test.quarkus.flyway.migrate-at-start=true
419
+
420
+ # Outbox relay: who publishes the outbox row (see quarkus-kafka-messaging skill)
421
+ app.messaging.outbox.relay-mode=debezium
422
+ app.messaging.outbox.poller.every=off
409
423
 
410
424
  # i18n (see error-handling skill) — locales must be declared for native
411
425
  quarkus.locales=en,es
@@ -526,7 +540,11 @@ class ArchitectureTest {
526
540
  @ArchTest
527
541
  static final ArchRule onlyHandlersTouchSql = classes()
528
542
  .that().haveSimpleNameEndingWith("Sql")
529
- .should().onlyBeAccessed().byClassesThat().haveSimpleNameEndingWith("Handler");
543
+ .should().onlyBeAccessed().byClassesThat(
544
+ have(simpleNameEndingWith("Handler"))
545
+ // OutboxSql is cross-cutting: its callers are the dispatcher and the relay job,
546
+ // which play the Handler role (they own the Connection) outside any slice.
547
+ .or(resideInAPackage("..common.messaging..")));
530
548
 
531
549
  @ArchTest
532
550
  static final ArchRule transportHasNoJdbc = noClasses()
@@ -535,6 +553,8 @@ class ArchitectureTest {
535
553
  .or().haveSimpleNameEndingWith("Consumer")
536
554
  .should().dependOnClassesThat().resideInAnyPackage("java.sql..", "javax.sql..");
537
555
 
556
+ // A Handler injecting OutboxDispatcher still satisfies this rule: the dispatcher is a
557
+ // common/messaging bean, so no messaging API ever reaches the slice.
538
558
  @ArchTest
539
559
  static final ArchRule handlerKnowsNoTransport = noClasses()
540
560
  .that().haveSimpleNameEndingWith("Handler")
@@ -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 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).
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)
@@ -45,20 +45,21 @@ Payload schema:
45
45
  Never publish to Kafka in the same breath as a DB write — dual-write problem. Standard:
46
46
 
47
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 (Debezium outbox connector preferred; scheduled poller as fallback for environments without Kafka Connect).
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.iam.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,14 +79,16 @@ 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
- 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
+ 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
84
  ```java
84
85
  private UUID execution(CreatePartyIndividualRequestDto request) {
85
86
  try (Connection conn = dataSource.getConnection()) {
86
87
  UUID partyId = sql.insertParty(conn, ...);
87
- sql.insertOutboxEvent(conn, partyId, "party",
88
- "com.alva.customer.party.created.v1", payloadJson(partyId, request), traceparent());
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)
89
92
  return partyId;
90
93
  } catch (SQLException e) {
91
94
  throw SqlStateTranslator.translate("PTY-500-001", e);
@@ -93,14 +96,155 @@ private UUID execution(CreatePartyIndividualRequestDto request) {
93
96
  }
94
97
  ```
95
98
 
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.
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`.
97
100
 
98
- Fire-and-forget technical messages (metrics, notifications with no consistency requirement) MAY use a direct `Emitter`:
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`:
99
104
 
100
105
  ```java
106
+ // common/audit/AuditRecorder.java — the Handler injects AuditRecorder, not the Emitter
101
107
  @Inject @Channel("audit-out") Emitter<AuditEventPayload> audit;
102
108
  ```
103
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
+
104
248
  ### Naming
105
249
 
106
250
  Canonical rules in the quarkus-hexagonal-core skill; messaging specifics:
@@ -111,6 +255,9 @@ Canonical rules in the quarkus-hexagonal-core skill; messaging specifics:
111
255
  | Consumer | `<Event>Consumer`, in the slice folder | `UserRegisteredConsumer` |
112
256
  | Outbox write | a method on the slice's own `Sql` | `insertOutboxEvent(conn, ...)` |
113
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` |
114
261
  | SmallRye channel | `<entity>-<event>-in` / `-out`, kebab-case | `user-registered-in` |
115
262
  | CloudEvents `type` | `<javaPackageStyle>.<entity>.<event>.v<major>` | `com.alva.iam.user.registered.v1` |
116
263
 
@@ -158,8 +305,9 @@ Rules:
158
305
  ## Checklist for a new event
159
306
 
160
307
  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`.
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.
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,6 +1,6 @@
1
1
  ---
2
2
  name: quarkus-sql-jdbc-agroal
3
- description: Standard for executing SQL queries, updates, batches, and transactions in Quarkus WITHOUT Panache or any ORM — using the Agroal connection pool with plain JDBC (default) or the reactive Vert.x SQL client (justified cases only). Use this skill whenever the user writes or reviews ANY repository, DAO, SQL statement, SELECT/INSERT/UPDATE/DELETE, batch operation, transaction, pagination query, jsonb access, connection pool configuration, or mentions Agroal, JDBC, PreparedStatement, datasource, or "query the database" — all persistence code must follow these patterns. Data access lives in the slice `<Slice>Sql` class with `Connection` as the first parameter of every method (see quarkus-hexagonal-core skill); there are no repositories, DAOs or row-mapper classes.
3
+ description: Standard for executing SQL queries, updates, batches, and transactions in Quarkus WITHOUT Panache or any ORM — using the Agroal connection pool with plain JDBC (default) or the reactive Vert.x SQL client (justified cases only). Use this skill whenever the user writes or reviews ANY repository, DAO, SQL statement, SELECT/INSERT/UPDATE/DELETE, batch operation, transaction, pagination query, jsonb access, connection pool configuration, or mentions Agroal, JDBC, PreparedStatement, datasource, Flyway, schema migrations, or "query the database" — all persistence code must follow these patterns. Data access lives in the slice `<Slice>Sql` class with `Connection` as the first parameter of every method (see quarkus-hexagonal-core skill); there are no repositories, DAOs or row-mapper classes.
4
4
  ---
5
5
 
6
6
  # SQL Execution Standard: Agroal + Plain JDBC (No ORM)
@@ -116,7 +116,7 @@ public class CreatePartyIndividualHandler {
116
116
  UUID partyId = sql.insertParty(conn, request.getTenantId(), "Individual",
117
117
  "Active", request.getCreatedBy());
118
118
  sql.insertIndividual(conn, partyId, request.getGivenName(), request.getFamilyName());
119
- sql.insertOutboxEvent(conn, partyId, "party", EVENT_TYPE, payloadJson(partyId, request), traceparent());
119
+ sql.insertOutboxEvent(conn, partyId, "party", EVENT_TYPE, TOPIC, payloadJson(partyId, request), traceparent());
120
120
  return partyId;
121
121
  } catch (SQLException e) {
122
122
  throw SqlStateTranslator.translate("PTY-500-001", e); // unchecked -> container rolls back
@@ -256,11 +256,44 @@ Every resulting code is registered in `ErrorCatalog` and present in all locale b
256
256
 
257
257
  Set `statement_timeout` (session or per-datasource via `quarkus.datasource.jdbc.additional-jdbc-properties.options=-c statement_timeout=5000`) so runaway queries fail fast instead of holding pool connections.
258
258
 
259
- ## 9. Testing
259
+ ## 9. Schema migrations (Flyway — opt-in, disabled by default)
260
+
261
+ The schema is versioned in git as SQL under `db/` in every environment. **Who applies it is a flag**, because a service is rarely allowed to alter its own schema in production — that is a DBA or pipeline responsibility, and a native binary starting up in a replica set is the worst possible place to run DDL.
262
+
263
+ ```properties
264
+ # STANDARD DEFAULT — the app never touches the schema
265
+ quarkus.flyway.enabled=false
266
+ quarkus.flyway.migrate-at-start=false
267
+
268
+ # %dev / %test: the app owns a throwaway database, so it may build it
269
+ %dev.quarkus.flyway.enabled=true
270
+ %dev.quarkus.flyway.migrate-at-start=true
271
+ %test.quarkus.flyway.enabled=true
272
+ %test.quarkus.flyway.migrate-at-start=true
273
+
274
+ quarkus.flyway.locations=db/migration
275
+ quarkus.flyway.baseline-on-migrate=true # adopting an existing database
276
+ ```
277
+
278
+ | `enabled` | `migrate-at-start` | Effect | Use in |
279
+ |---|---|---|---|
280
+ | `false` | — | Flyway is inert; the schema must already exist | **default**: prod, staging, any DBA-owned database |
281
+ | `true` | `false` | Flyway present, applies nothing at boot; add `validate-at-start=true` to fail fast on a schema drift | environments that want a startup guard without DDL rights |
282
+ | `true` | `true` | The app migrates on boot | `%dev`, `%test`, single-owner sandboxes |
283
+
284
+ Rules:
285
+ - **`enabled=false` is the default and is written explicitly** in `application.properties` — never left implicit. Turning it on is a deliberate, reviewed change per environment.
286
+ - Migrations live in `db/migration` as `V<n>__<snake_description>.sql` regardless of who runs them. Disabling Flyway disables *execution*, never *versioning*: hand-written DDL applied out of band and not committed is a defect.
287
+ - Migrations must be **N/N-1 compatible** (additive first, destructive in a later release) — with `enabled=false` the schema is applied before the new binary rolls out, so the old code always runs briefly against the new schema.
288
+ - `quarkus.flyway.clean-at-start` is prohibited outside `%test`; `quarkus.flyway.clean-disabled=true` everywhere else.
289
+ - The extension stays in `pom.xml` even when disabled — one binary for every environment, and a runtime flag can't resurrect a dependency that isn't there. Drop `quarkus-flyway` entirely only if no environment migrates at boot; then `db/` is applied exclusively by the pipeline.
290
+ - Cross-cutting tables (`outbox_event`, `processed_event`, `audit_event` — see kafka/observability skills) are ordinary migrations in the same `db/migration` folder, not runtime-created.
291
+
292
+ ## 10. Testing
260
293
 
261
294
  - `<Slice>Sql` tests: `@QuarkusTest` + Dev Services (Testcontainers Postgres starts automatically — no config). Real SQL against real Postgres; never H2 (dialect lies). Obtain a `Connection` in the test and pass it in, exactly as the `Handler` does.
262
295
  - `<Slice>Handler` tests are pure Mockito with a mocked `Sql` — no database (see quarkus-hexagonal-core skill).
263
- - Flyway migrations run at test start (`quarkus.flyway.migrate-at-start=true`) tests validate DDL and queries together.
296
+ - Flyway migrations run at test start — `%test` is one of the two profiles where the flag is on (`quarkus.flyway.enabled=true` + `migrate-at-start=true`, see §9), so tests validate DDL and queries together against the same SQL production will receive.
264
297
  - Native verification: `@QuarkusIntegrationTest` re-runs the same tests against the binary.
265
298
 
266
299
  ## Checklist for a new `Sql` method
@@ -84,13 +84,6 @@ agents:
84
84
  team: software-development
85
85
  description: "Test-first discipline (red, green, refactor), 100% pass before review, no fluff all precision. Speaks like a terminal prompt: exact file paths, AC IDs, and commit-message brevity — every statement citable."
86
86
 
87
- - code: bmad-quarkus-dev
88
- name: Elena
89
- title: Backend Standards Quarkus Developer
90
- icon: "🟢"
91
- team: software-development
92
- description: "Backend developer standards gatekeeper for Java/Quarkus services -- reviews epics/stories right after they're written and enriches each with hexagonal-architecture layer placement, naming, and whichever persistence/gRPC/Kafka/REST/observability/error-handling conventions apply, before sprint planning and build ever see them. Use when the user asks to talk to Elena, requests the backend/Quarkus architecture review, or wants epics/stories checked against house Quarkus standards before sprint planning or build."
93
-
94
87
  - code: bmad-quarkus-build
95
88
  name: Marcus
96
89
  title: Senior Quarkus Backend Developer & Build Engineer