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.
- package/package.json +1 -1
- package/removals.txt +10 -0
- package/src/bmm-skills/agents/bmad-quarkus-build/SKILL.md +19 -16
- package/src/bmm-skills/agents/bmad-quarkus-build/customize.toml +7 -5
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/bqa-setup/assets/module-help.csv +0 -1
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/bqa-setup/assets/module.yaml +2 -9
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-hexagonal-core/SKILL.md +30 -10
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-kafka-messaging/SKILL.md +163 -15
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-sql-jdbc-agroal/SKILL.md +37 -4
- package/src/bmm-skills/module.yaml +0 -7
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/SKILL.md +0 -108
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/customize.toml +0 -62
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/references/architecture-review.md +0 -17
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-architect/references/prompt-quality-canon.md +0 -79
- package/src/bmm-skills/agents/bmad-quarkus-dev/.memlog.md +0 -12
- package/src/bmm-skills/agents/bmad-quarkus-dev/.memlog.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/SKILL.md +0 -86
- package/src/bmm-skills/agents/bmad-quarkus-dev/SKILL.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/customize.toml +0 -37
- package/src/bmm-skills/agents/bmad-quarkus-dev/customize.toml:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/references/enrich-stories.md +0 -19
- package/src/bmm-skills/agents/bmad-quarkus-dev/references/enrich-stories.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/references/prompt-quality-canon.md +0 -79
- package/src/bmm-skills/agents/bmad-quarkus-dev/references/prompt-quality-canon.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/SKILL.md +0 -80
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/SKILL.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module-help.csv +0 -9
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module-help.csv:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module.yaml +0 -16
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/assets/module.yaml:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/cleanup-legacy.py +0 -287
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/cleanup-legacy.py:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-config.py +0 -441
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-config.py:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-help-csv.py +0 -246
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/bqa-setup/scripts/merge-help-csv.py:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md +0 -215
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-error-handling-i18n/SKILL.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md +0 -160
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-grpc-services/SKILL.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md +0 -661
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-hexagonal-core/SKILL.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md +0 -165
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-kafka-messaging/SKILL.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md +0 -196
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-observability-otel/SKILL.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md +0 -145
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-openapi-tmforum/SKILL.md:Zone.Identifier +0 -0
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md +0 -275
- package/src/bmm-skills/agents/bmad-quarkus-dev/skills/quarkus-sql-jdbc-agroal/SKILL.md:Zone.Identifier +0 -0
package/package.json
CHANGED
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
26
|
-
- Naming reveals the
|
|
27
|
-
-
|
|
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
|
|
49
|
-
| `quarkus-sql-jdbc-agroal` |
|
|
50
|
-
| `quarkus-error-handling-i18n` |
|
|
51
|
-
| `quarkus-openapi-tmforum` |
|
|
52
|
-
| `quarkus-grpc-services` |
|
|
53
|
-
| `quarkus-kafka-messaging` | Domain events,
|
|
54
|
-
| `quarkus-observability-otel` |
|
|
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 `
|
|
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
|
|
44
|
-
identity = "Disciplined in Kent Beck's TDD and in this codebase's
|
|
45
|
-
communication_style = "Ultra-succinct. Speaks in file paths, AC IDs, and
|
|
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
|
-
"
|
|
52
|
-
"
|
|
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
|
|
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.
|
|
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",
|
|
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`
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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`;
|
|
162
|
-
3.
|
|
163
|
-
4.
|
|
164
|
-
5.
|
|
165
|
-
6.
|
|
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.
|
|
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
|
|
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
|