@hellraisercenobit/ai-engineering-gate 0.8.0
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/README.md +82 -0
- package/agents/design-pattern-reviewer.md +40 -0
- package/agents/modern-typescript-reviewer.md +24 -0
- package/agents/testing-pattern-reviewer.md +23 -0
- package/bin/ai-engineering-gate.mjs +6 -0
- package/contracts/axi/axi-standard.md +273 -0
- package/contracts/axi/pin.json +36 -0
- package/contracts/members.json +58 -0
- package/contracts/schemas/arbitration.schema.json +45 -0
- package/contracts/schemas/decision-envelope.schema.json +215 -0
- package/contracts/schemas/declaration.schema.json +190 -0
- package/contracts/schemas/dispute.schema.json +67 -0
- package/contracts/schemas/evidence-append.schema.json +72 -0
- package/contracts/schemas/gate-output.schema.json +296 -0
- package/contracts/schemas/marker.schema.json +75 -0
- package/contracts/schemas/review-envelope.schema.json +378 -0
- package/contracts/schemas/verdict-record.schema.json +191 -0
- package/contracts/suite-contract.md +270 -0
- package/dist/ai-engineering-gate.mjs +2762 -0
- package/package.json +47 -0
- package/skills/engineering/review-design-patterns/LICENSE +6 -0
- package/skills/engineering/review-design-patterns/SKILL.md +196 -0
- package/skills/engineering/review-design-patterns/agents/openai.yaml +3 -0
- package/skills/engineering/review-design-patterns/references/smell-signatures.md +117 -0
- package/skills/engineering/review-design-patterns/review-design-patterns.yaml +25 -0
- package/skills/engineering/review-modern-typescript/LICENSE +21 -0
- package/skills/engineering/review-modern-typescript/SKILL.md +88 -0
- package/skills/engineering/review-modern-typescript/agents/openai.yaml +3 -0
- package/skills/engineering/review-modern-typescript/references/smell-signatures.md +36 -0
- package/skills/engineering/review-modern-typescript/review-modern-typescript.yaml +22 -0
- package/skills/engineering/review-testing-patterns/SKILL.md +108 -0
- package/skills/engineering/review-testing-patterns/agents/openai.yaml +4 -0
- package/skills/engineering/review-testing-patterns/references/compiler-profile.mjs +103 -0
- package/skills/engineering/review-testing-patterns/references/smell-signatures.md +30 -0
- package/skills/engineering/review-testing-patterns/review-testing-patterns.yaml +23 -0
- package/skills/engineering/transpose-design-patterns/LICENSE +6 -0
- package/skills/engineering/transpose-design-patterns/SKILL.md +231 -0
- package/skills/engineering/transpose-design-patterns/agents/openai.yaml +3 -0
- package/skills/engineering/transpose-design-patterns/references/arbitration.schema.json +45 -0
- package/skills/engineering/transpose-design-patterns/references/decision-envelope.schema.json +215 -0
- package/skills/engineering/transpose-design-patterns/references/declaration.schema.json +190 -0
- package/skills/engineering/transpose-design-patterns/references/design-decision-record.schema.json +731 -0
- package/skills/engineering/transpose-design-patterns/references/dispute.schema.json +67 -0
- package/skills/engineering/transpose-design-patterns/references/evidence-append.schema.json +72 -0
- package/skills/engineering/transpose-design-patterns/references/marker.schema.json +75 -0
- package/skills/engineering/transpose-design-patterns/references/pattern-catalog.md +305 -0
- package/skills/engineering/transpose-design-patterns/references/record.example.json +120 -0
- package/skills/engineering/transpose-design-patterns/references/review-envelope.schema.json +378 -0
- package/skills/engineering/transpose-design-patterns/references/suite-contract.md +270 -0
- package/skills/engineering/transpose-design-patterns/references/transpose-angular.md +224 -0
- package/skills/engineering/transpose-design-patterns/references/transpose-php.md +561 -0
- package/skills/engineering/transpose-design-patterns/references/transpose-quarkus.md +283 -0
- package/skills/engineering/transpose-design-patterns/references/transpose-react.md +256 -0
- package/skills/engineering/transpose-design-patterns/references/transpose-vanilla.md +204 -0
- package/skills/engineering/transpose-design-patterns/references/transpose-vue.md +234 -0
- package/skills/engineering/transpose-design-patterns/transpose-design-patterns.yaml +24 -0
- package/skills/engineering/transpose-modern-typescript/LICENSE +21 -0
- package/skills/engineering/transpose-modern-typescript/SKILL.md +90 -0
- package/skills/engineering/transpose-modern-typescript/agents/openai.yaml +3 -0
- package/skills/engineering/transpose-modern-typescript/references/arbitration.schema.json +45 -0
- package/skills/engineering/transpose-modern-typescript/references/catalog.md +38 -0
- package/skills/engineering/transpose-modern-typescript/references/collections.md +50 -0
- package/skills/engineering/transpose-modern-typescript/references/compatibility.md +61 -0
- package/skills/engineering/transpose-modern-typescript/references/decision-envelope.schema.json +215 -0
- package/skills/engineering/transpose-modern-typescript/references/decision-record.schema.json +830 -0
- package/skills/engineering/transpose-modern-typescript/references/declaration.schema.json +190 -0
- package/skills/engineering/transpose-modern-typescript/references/dispute.schema.json +67 -0
- package/skills/engineering/transpose-modern-typescript/references/evidence-append.schema.json +72 -0
- package/skills/engineering/transpose-modern-typescript/references/idioms.md +74 -0
- package/skills/engineering/transpose-modern-typescript/references/marker.schema.json +75 -0
- package/skills/engineering/transpose-modern-typescript/references/platform.md +147 -0
- package/skills/engineering/transpose-modern-typescript/references/record.example.json +172 -0
- package/skills/engineering/transpose-modern-typescript/references/record.md +37 -0
- package/skills/engineering/transpose-modern-typescript/references/review-envelope.schema.json +378 -0
- package/skills/engineering/transpose-modern-typescript/references/suite-contract.md +270 -0
- package/skills/engineering/transpose-modern-typescript/references/types.md +97 -0
- package/skills/engineering/transpose-modern-typescript/transpose-modern-typescript.yaml +22 -0
- package/skills/engineering/transpose-testing-patterns/SKILL.md +96 -0
- package/skills/engineering/transpose-testing-patterns/agents/openai.yaml +4 -0
- package/skills/engineering/transpose-testing-patterns/references/arbitration.schema.json +45 -0
- package/skills/engineering/transpose-testing-patterns/references/catalog.md +218 -0
- package/skills/engineering/transpose-testing-patterns/references/decision-envelope.schema.json +215 -0
- package/skills/engineering/transpose-testing-patterns/references/decision-record.schema.json +857 -0
- package/skills/engineering/transpose-testing-patterns/references/declaration.schema.json +190 -0
- package/skills/engineering/transpose-testing-patterns/references/detect-adapter.mjs +98 -0
- package/skills/engineering/transpose-testing-patterns/references/dispute.schema.json +67 -0
- package/skills/engineering/transpose-testing-patterns/references/doubles-data.md +45 -0
- package/skills/engineering/transpose-testing-patterns/references/evidence-append.schema.json +72 -0
- package/skills/engineering/transpose-testing-patterns/references/journal-event.schema.json +115 -0
- package/skills/engineering/transpose-testing-patterns/references/marker.schema.json +75 -0
- package/skills/engineering/transpose-testing-patterns/references/record.example.json +230 -0
- package/skills/engineering/transpose-testing-patterns/references/record.md +25 -0
- package/skills/engineering/transpose-testing-patterns/references/review-envelope.schema.json +378 -0
- package/skills/engineering/transpose-testing-patterns/references/suite-contract.md +270 -0
- package/skills/engineering/transpose-testing-patterns/references/tdd.md +62 -0
- package/skills/engineering/transpose-testing-patterns/references/transpose-codeception.md +84 -0
- package/skills/engineering/transpose-testing-patterns/references/transpose-karma-jasmine-angular.md +73 -0
- package/skills/engineering/transpose-testing-patterns/references/transpose-vitest.md +111 -0
- package/skills/engineering/transpose-testing-patterns/references/typescript.md +42 -0
- package/skills/engineering/transpose-testing-patterns/transpose-testing-patterns.yaml +23 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# Quarkus Transposition — Design Patterns (Java 21+, 2026)
|
|
2
|
+
|
|
3
|
+
Generic, portable Quarkus wiring for the patterns in [`pattern-catalog.md`](pattern-catalog.md).
|
|
4
|
+
Framework-specific only — keep business logic framework-agnostic in the catalog. Examples use
|
|
5
|
+
throwaway domains (user, cart, payment, order); substitute your own types. This guide states **common**
|
|
6
|
+
Quarkus practice (Quarkus 3.x, Java 21+), not any single project's house rules.
|
|
7
|
+
|
|
8
|
+
## Quarkus building blocks
|
|
9
|
+
|
|
10
|
+
The transpositions below lean on a few modern-Quarkus defaults:
|
|
11
|
+
|
|
12
|
+
- **Build-time CDI (ArC)** — wiring is resolved at build time (no runtime classpath scanning, no Portable
|
|
13
|
+
Extensions). Prefer **constructor injection** (final fields, testable; `@Inject` is optional for a
|
|
14
|
+
single constructor) over field injection.
|
|
15
|
+
- **`@ApplicationScoped`** for almost every bean — a _normal_ scope: a client proxy is injected and the
|
|
16
|
+
instance is created lazily. `@Singleton` is a _pseudo-scope_ (no proxy, eager, can't break cycles);
|
|
17
|
+
reach for it only to shave proxy indirection.
|
|
18
|
+
- **Quarkus REST** (`quarkus-rest` + `quarkus-rest-jackson`) — Jakarta REST annotations (`@Path`, `@GET`).
|
|
19
|
+
It is the current name for RESTEasy Reactive; RESTEasy Classic (`quarkus-resteasy`) is legacy. A method
|
|
20
|
+
runs on the Vert.x event loop unless it returns a plain value / is `@Blocking` / `@Transactional` /
|
|
21
|
+
`@RunOnVirtualThread` (then a worker or virtual thread).
|
|
22
|
+
- **Records** for immutable DTOs and value types; **sealed interfaces + pattern-matching `switch`** for
|
|
23
|
+
closed discriminated unions (compiler-checked exhaustiveness, no `default`).
|
|
24
|
+
- **`@ConfigMapping`** interface for typed, grouped, validated config (over scattered `@ConfigProperty`).
|
|
25
|
+
- **Native-image aware** — build-time wiring; payloads reached only by reflection need
|
|
26
|
+
`@RegisterForReflection`, beans resolved only by programmatic lookup need `@Unremovable`.
|
|
27
|
+
|
|
28
|
+
```java
|
|
29
|
+
@ApplicationScoped
|
|
30
|
+
public class CheckoutService { // proxied, lazy, one shared instance
|
|
31
|
+
private final OrderRepository orders; // constructor injection, final field
|
|
32
|
+
CheckoutService(OrderRepository orders) { this.orders = orders; } // no @Inject needed
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Pattern → Quarkus wiring
|
|
37
|
+
|
|
38
|
+
### Strategy → many beans of one interface, selected via CDI (no `switch`)
|
|
39
|
+
|
|
40
|
+
Each strategy is an `@ApplicationScoped` bean implementing the interface with a `supports()` / `key()`
|
|
41
|
+
method. Inject **`@All List<T>`** (immutable, `@Priority`-sorted) and pick by predicate — DI replaces the
|
|
42
|
+
`switch`. For a _closed_ variant set known at compile time, a sealed interface + pattern-matching `switch`
|
|
43
|
+
is the typed alternative.
|
|
44
|
+
|
|
45
|
+
```java
|
|
46
|
+
interface PaymentStrategy { boolean supports(String method); void pay(long amount); }
|
|
47
|
+
|
|
48
|
+
@ApplicationScoped
|
|
49
|
+
class PaymentService {
|
|
50
|
+
private final List<PaymentStrategy> strategies;
|
|
51
|
+
PaymentService(@All List<PaymentStrategy> strategies) { this.strategies = strategies; }
|
|
52
|
+
void pay(String method, long amount) {
|
|
53
|
+
strategies.stream().filter(s -> s.supports(method)).findFirst()
|
|
54
|
+
.orElseThrow(() -> new IllegalArgumentException(method)).pay(amount);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Disambiguate a one-of lookup with `@Identifier("…")` + `Instance<T>.select(…)` — **not** `@Named`
|
|
60
|
+
(`@Named` also implies `@Default`, causing ambiguity).
|
|
61
|
+
|
|
62
|
+
### Registry → typed map built from the injected implementations
|
|
63
|
+
|
|
64
|
+
DI _is_ the registry: inject `@All List<T>` and index by the key each bean exposes. New implementations
|
|
65
|
+
are discovered automatically — no central `switch`, no hand-maintained provider map. Duplicate keys fail
|
|
66
|
+
fast when the registry is first used (it is `@ApplicationScoped`, so the constructor runs lazily; make it
|
|
67
|
+
`@Singleton`/`@Startup` if you want the check at boot).
|
|
68
|
+
|
|
69
|
+
```java
|
|
70
|
+
interface Exporter { String format(); byte[] export(Report r); }
|
|
71
|
+
|
|
72
|
+
@ApplicationScoped
|
|
73
|
+
class ExporterRegistry {
|
|
74
|
+
private final Map<String, Exporter> byFormat;
|
|
75
|
+
ExporterRegistry(@All List<Exporter> exporters) {
|
|
76
|
+
this.byFormat = exporters.stream().collect(toUnmodifiableMap(Exporter::format, identity()));
|
|
77
|
+
}
|
|
78
|
+
Exporter forFormat(String f) {
|
|
79
|
+
return Optional.ofNullable(byFormat.get(f)).orElseThrow(() -> new NoSuchElementException(f));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Gate a bean by config with `@LookupIfProperty` / `@LookupUnlessProperty` (then obtain it via `Instance<T>`).
|
|
85
|
+
|
|
86
|
+
### Factory → CDI `@Produces` producer method
|
|
87
|
+
|
|
88
|
+
Isolate creation logic in a `@Produces` method when it is non-trivial, config-driven, or builds a
|
|
89
|
+
third-party type you can't annotate; return an abstraction. A `@Disposes` method cleans it up.
|
|
90
|
+
|
|
91
|
+
```java
|
|
92
|
+
@ApplicationScoped
|
|
93
|
+
class GatewayFactory {
|
|
94
|
+
@Produces @ApplicationScoped @Identifier("primary")
|
|
95
|
+
PaymentGateway primary(GatewayConfig cfg) {
|
|
96
|
+
return new HttpPaymentGateway(cfg.url(), cfg.token()); // returns the abstraction
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Caveat: **interceptors are not woven on produced instances** — `@Transactional` / custom bindings on a
|
|
102
|
+
producer-returned object do nothing. If you need interception, make it an annotated bean class instead.
|
|
103
|
+
|
|
104
|
+
### Command → distinct `@ApplicationScoped` use-case the resource delegates to
|
|
105
|
+
|
|
106
|
+
Encapsulate one action in a focused application-service bean; the Quarkus REST resource stays thin and
|
|
107
|
+
delegates. Put `@Transactional` on the use-case method (the unit of work), never on the resource or the
|
|
108
|
+
domain.
|
|
109
|
+
|
|
110
|
+
```java
|
|
111
|
+
@ApplicationScoped
|
|
112
|
+
class PlaceOrder { // the use-case / Command
|
|
113
|
+
private final OrderRepository orders;
|
|
114
|
+
PlaceOrder(OrderRepository orders) { this.orders = orders; }
|
|
115
|
+
@Transactional
|
|
116
|
+
OrderId handle(PlaceOrderCommand cmd) { /* domain logic */ return orders.save(cmd.toOrder()); }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@Path("/orders")
|
|
120
|
+
class OrderResource {
|
|
121
|
+
private final PlaceOrder placeOrder;
|
|
122
|
+
OrderResource(PlaceOrder placeOrder) { this.placeOrder = placeOrder; }
|
|
123
|
+
@POST OrderId create(@Valid PlaceOrderCommand cmd) { return placeOrder.handle(cmd); }
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A Panache repository method or a resource method is **not automatically** the Command — when the action
|
|
128
|
+
must be reusable, composable, or unit-testable in isolation, keep it a distinct `@ApplicationScoped`
|
|
129
|
+
use-case bean the resource/repository **delegates** to. Full rule + layering: catalog → _Command_.
|
|
130
|
+
|
|
131
|
+
### Adapter / DTO Mapping → records + pure `from`/`to` at the boundary
|
|
132
|
+
|
|
133
|
+
Map DTO ↔ domain/entity at the resource boundary; **never serialize JPA entities** (leaks internals,
|
|
134
|
+
breaks on lazy relations, bloats the native reflection surface). The catalog's canonical mapper is
|
|
135
|
+
TypeScript; the Java equivalent is a record with a static `from` (or, for large surfaces, MapStruct):
|
|
136
|
+
|
|
137
|
+
```java
|
|
138
|
+
public record UserDto(String id, Instant createdAt) {
|
|
139
|
+
static UserDto from(User u) { return new UserDto(u.id(), u.createdAt()); }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// larger surfaces: MapStruct, CDI-injectable and reflection-free (native-safe).
|
|
143
|
+
// Use "jakarta-cdi" on Quarkus 3.x — plain "cdi" can emit javax.inject.@Inject, which ArC won't resolve.
|
|
144
|
+
@Mapper(componentModel = "jakarta-cdi")
|
|
145
|
+
interface UserMapper { UserDto toDto(User u); }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Keep mappers and DTOs in the application/web layer — the domain must not depend on them.
|
|
149
|
+
|
|
150
|
+
### Composition → CDI injection + interceptors + events (not inheritance)
|
|
151
|
+
|
|
152
|
+
Compose behavior through injected collaborators, **CDI interceptors** for cross-cutting concerns
|
|
153
|
+
(`@InterceptorBinding` + `@AroundInvoke`, ordered by `@Priority`; `@Transactional` is the built-in
|
|
154
|
+
example), and **CDI events** to decouple producers from consumers (`Event<T>` + `@Observes` /
|
|
155
|
+
`@ObservesAsync`). Model variants with sealed records, not class hierarchies.
|
|
156
|
+
|
|
157
|
+
```java
|
|
158
|
+
@ApplicationScoped
|
|
159
|
+
class OrderService {
|
|
160
|
+
private final Event<OrderPlaced> placed;
|
|
161
|
+
OrderService(Event<OrderPlaced> placed) { this.placed = placed; }
|
|
162
|
+
void place(Order o) { /* … */ placed.fireAsync(new OrderPlaced(o.id())); }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@ApplicationScoped
|
|
166
|
+
class EmailNotifier {
|
|
167
|
+
void onOrder(@ObservesAsync OrderPlaced e) { /* offloaded to a worker pool */ }
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Singleton / shared state → `@ApplicationScoped` bean exposing read-only state
|
|
172
|
+
|
|
173
|
+
The common case for app-wide state: a single `@ApplicationScoped` instance with private state and a
|
|
174
|
+
read-only surface. Because that one instance is hit by **all request threads**, mutable fields are a data
|
|
175
|
+
race — guard them with container-managed `@Lock` (write-exclusive by default, `@Lock(READ)` for
|
|
176
|
+
concurrent reads) or `java.util.concurrent` / atomics.
|
|
177
|
+
|
|
178
|
+
```java
|
|
179
|
+
@Lock
|
|
180
|
+
@ApplicationScoped
|
|
181
|
+
class FeatureFlags {
|
|
182
|
+
private final Map<String, Boolean> flags = new HashMap<>();
|
|
183
|
+
void set(String key, boolean on) { flags.put(key, on); } // exclusive
|
|
184
|
+
@Lock(value = Lock.Type.READ) boolean isEnabled(String key) { // concurrent reads
|
|
185
|
+
return flags.getOrDefault(key, false);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Cross-cutting Quarkus practice
|
|
191
|
+
|
|
192
|
+
### Keep resources thin
|
|
193
|
+
|
|
194
|
+
Quarkus REST resources orchestrate HTTP, bind/validate input, and delegate. Push logic and mapping into
|
|
195
|
+
`@ApplicationScoped` services and pure functions. (Resource classes are `@Singleton` by default — never
|
|
196
|
+
store per-request mutable state in their fields.)
|
|
197
|
+
|
|
198
|
+
### Never block the event loop
|
|
199
|
+
|
|
200
|
+
Endpoint methods run on the Vert.x event loop. Blocking work (JDBC, blocking clients, filesystem) must
|
|
201
|
+
either return a Mutiny `Uni` / `Multi`, or be annotated `@Blocking` (worker thread) or
|
|
202
|
+
`@RunOnVirtualThread` (Java 21 virtual thread — the 2026 default for blocking I/O endpoints). Blocking the
|
|
203
|
+
loop stalls every concurrent request.
|
|
204
|
+
|
|
205
|
+
```java
|
|
206
|
+
@GET @Path("/{id}") @RunOnVirtualThread // imperative blocking on a virtual thread
|
|
207
|
+
UserDto get(@RestPath String id) { return UserDto.from(users.findById(id)); }
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Reactive only at the async boundary
|
|
211
|
+
|
|
212
|
+
Keep Mutiny `Uni` / `Multi` at the edges (REST signatures, reactive clients/messaging); map DTO→domain in
|
|
213
|
+
the chain and convert to plain values inside imperative code. Never call `.await()` on the event-loop
|
|
214
|
+
thread.
|
|
215
|
+
|
|
216
|
+
### Transactions at the service boundary
|
|
217
|
+
|
|
218
|
+
`@Transactional` (JTA) wraps the use-case method, making it atomic — not the domain, not the resource. On
|
|
219
|
+
the _reactive_ stack use `@WithTransaction` / `Panache.withTransaction(...)` (returns `Uni`); JTA does not
|
|
220
|
+
apply there.
|
|
221
|
+
|
|
222
|
+
### Validate at the boundary
|
|
223
|
+
|
|
224
|
+
Annotate DTO fields with `jakarta.validation.constraints.*` and REST params with `@Valid` — Quarkus
|
|
225
|
+
auto-returns HTTP 400 with a violation report. Keep the rules as constraints / pure functions; inject
|
|
226
|
+
`Validator` for programmatic checks in services.
|
|
227
|
+
|
|
228
|
+
### Persistence behind a repository
|
|
229
|
+
|
|
230
|
+
Prefer a `PanacheRepository<T>` (`@ApplicationScoped`) over the active-record entity for layered, mockable
|
|
231
|
+
code — it keeps query logic off the entity and behind a seam.
|
|
232
|
+
|
|
233
|
+
### Stay native-image-friendly
|
|
234
|
+
|
|
235
|
+
Wire through build-time CDI, not `Class.forName` / classpath scanning. Add `@RegisterForReflection` to
|
|
236
|
+
payloads reached only by reflection, and `@Unremovable` to beans resolved only via programmatic lookup
|
|
237
|
+
(else `quarkus.arc.remove-unused-beans` drops them). Use the tracing agent in integration tests to find gaps.
|
|
238
|
+
|
|
239
|
+
### Testing
|
|
240
|
+
|
|
241
|
+
`@QuarkusTest` + REST Assured for app / endpoint tests; `@QuarkusComponentTest` to test one bean in
|
|
242
|
+
isolation; `@InjectMock` / `@InjectSpy` (`quarkus-junit5-mockito`) to replace collaborators;
|
|
243
|
+
`@TestHTTPEndpoint` to resolve paths from the resource; `@QuarkusIntegrationTest` to exercise the packaged
|
|
244
|
+
jar / native image.
|
|
245
|
+
|
|
246
|
+
## Quarkus Decision Matrix
|
|
247
|
+
|
|
248
|
+
| Problem | Recommended Solution |
|
|
249
|
+
| -------------------------------------- | -------------------------------------------------------------------------- |
|
|
250
|
+
| Shared app state / service | `@ApplicationScoped` bean (read-only surface; `@Lock` if mutable) |
|
|
251
|
+
| Singleton without proxy / lazy needs | `@Singleton` (pseudo-scope, eager) |
|
|
252
|
+
| Depend on an abstraction | inject the interface via the constructor |
|
|
253
|
+
| Interchangeable behaviors | Strategy (`@All List<T>` + `supports()`, no switch) |
|
|
254
|
+
| Closed, finite variant set | sealed interface + pattern-matching `switch` |
|
|
255
|
+
| Plugin / extension system | Registry (map built from `@All List<T>`) |
|
|
256
|
+
| Object creation with logic | Factory (`@Produces` producer method) |
|
|
257
|
+
| Reusable action | Command (distinct `@ApplicationScoped` use-case the resource delegates to) |
|
|
258
|
+
| API ↔ domain shape mismatch | record DTO + static `from` / MapStruct (`componentModel="jakarta-cdi"`) |
|
|
259
|
+
| Cross-cutting behavior | Composition (CDI interceptor / events; no inheritance) |
|
|
260
|
+
| Disambiguate multiple beans | `@Identifier` + `Instance.select` (not `@Named`) |
|
|
261
|
+
| Config-gated bean | `@LookupIfProperty` / `@LookupUnlessProperty` |
|
|
262
|
+
| Typed config | `@ConfigMapping` interface |
|
|
263
|
+
| Runtime validation | Bean Validation (`@Valid` + `jakarta.validation.constraints.*`) |
|
|
264
|
+
| Unit of work / atomicity | `@Transactional` on the service method (reactive: `@WithTransaction`) |
|
|
265
|
+
| Blocking I/O endpoint | `@RunOnVirtualThread` (or `@Blocking`) |
|
|
266
|
+
| Async / streaming boundary | Mutiny `Uni` / `Multi` |
|
|
267
|
+
| Persistence access | `PanacheRepository<T>` (repository over active-record) |
|
|
268
|
+
| Reflectively-reached payload (native) | `@RegisterForReflection` |
|
|
269
|
+
| Bean kept only via programmatic lookup | `@Unremovable` |
|
|
270
|
+
|
|
271
|
+
## Anti-Patterns to Avoid
|
|
272
|
+
|
|
273
|
+
- field injection instead of constructor injection (loses final fields, testability)
|
|
274
|
+
- business logic in Quarkus REST / JAX-RS resources — delegate to an `@ApplicationScoped` service
|
|
275
|
+
- blocking the IO / event-loop thread without `@Blocking` / `@RunOnVirtualThread` — stalls all requests
|
|
276
|
+
- mutable shared state in an `@ApplicationScoped` bean without `@Lock` / atomics — a data race across threads
|
|
277
|
+
- serializing JPA entities over HTTP / entities leaking across layers — map to record DTOs at the boundary
|
|
278
|
+
- `switch` on a type tag instead of CDI selection (open sets) or an exhaustive sealed `switch` (closed sets)
|
|
279
|
+
- `@Named` for internal bean disambiguation (implicit `@Default` clash) — use `@Identifier`
|
|
280
|
+
- `@Transactional` on a domain entity or a producer-returned instance (not woven) — put it on the service
|
|
281
|
+
- runtime reflection / `Class.forName` / classpath scanning that breaks native image — wire at build time
|
|
282
|
+
- deep inheritance hierarchies — prefer composition + CDI + sealed records
|
|
283
|
+
- a Command merged into a resource/repository method when it must be reusable — keep it a distinct use-case the resource delegates to
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# React Transposition — Design Patterns (TypeScript 2026)
|
|
2
|
+
|
|
3
|
+
Generic, portable React wiring for the patterns in [`pattern-catalog.md`](pattern-catalog.md).
|
|
4
|
+
Framework-specific only — keep business logic framework-agnostic in the catalog. Examples use
|
|
5
|
+
throwaway domains (user, cart, payment, order); substitute your own types. This guide states **common**
|
|
6
|
+
React practice, not any single project's house rules.
|
|
7
|
+
|
|
8
|
+
## React building blocks
|
|
9
|
+
|
|
10
|
+
The transpositions below lean on a few modern-React defaults (React 19+):
|
|
11
|
+
|
|
12
|
+
- **Function components + hooks** only — no class components, no `forwardRef` (in React 19 `ref` is a
|
|
13
|
+
normal prop).
|
|
14
|
+
- **`<Context>` as its own provider** — `<ThemeContext value={…}>`, not `<ThemeContext.Provider>`.
|
|
15
|
+
Consume with a typed custom hook (`useTheme()`), never raw `useContext` scattered in views.
|
|
16
|
+
- **The React Compiler does the memoization.** With it enabled, drop hand-written `useMemo` /
|
|
17
|
+
`useCallback` / `React.memo` — keep them only where the compiler is off or you have a measured need.
|
|
18
|
+
- **Derive during render**, hold only source-of-truth state in `useState` / `useReducer`. Effects are
|
|
19
|
+
for synchronizing with _external_ systems, not for computing values from props/state.
|
|
20
|
+
- **External shared state lives in a store** (Zustand), not in a high-frequency Context. Context is for
|
|
21
|
+
dependency injection of stable services.
|
|
22
|
+
- **Strong typing**: typed props, discriminated unions for variants, `as const satisfies` for lookup
|
|
23
|
+
maps, branded ids.
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
const ThemeContext = createContext<Theme | null>(null);
|
|
27
|
+
|
|
28
|
+
export function useTheme(): Theme {
|
|
29
|
+
const theme = use(ThemeContext); // React 19 reader — callable conditionally, unlike useContext
|
|
30
|
+
if (!theme) throw new Error('useTheme must be used inside <ThemeContext>');
|
|
31
|
+
return theme;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Pattern → React wiring
|
|
36
|
+
|
|
37
|
+
### Strategy → typed lookup map, resolved by key
|
|
38
|
+
|
|
39
|
+
Stateless strategies in an `as const satisfies` record; pick by key — no `switch`.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
export interface PaymentStrategy {
|
|
43
|
+
pay(amount: number): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const paymentStrategies = {
|
|
47
|
+
stripe: stripeStrategy,
|
|
48
|
+
paypal: paypalStrategy,
|
|
49
|
+
} as const satisfies Record<string, PaymentStrategy>;
|
|
50
|
+
|
|
51
|
+
export type PaymentMethod = keyof typeof paymentStrategies;
|
|
52
|
+
|
|
53
|
+
const strategy = paymentStrategies[method]; // typed, exhaustive, no switch
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Registry → self-registering map (validated when dynamic)
|
|
57
|
+
|
|
58
|
+
Plugins self-register into a module-level map; no central switch. Validate runtime/external plugins.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const registry = new Map<string, Plugin>();
|
|
62
|
+
export const registerPlugin = (p: Plugin): void => void registry.set(p.key, p);
|
|
63
|
+
export const getPlugin = (key: string): Plugin | undefined => registry.get(key);
|
|
64
|
+
// dynamic plugins: validate the shape with zod/valibot before registering
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
To expose a registry to the tree, provide it once via Context and read it through a custom hook —
|
|
68
|
+
never reach into module singletons from components when the registry is request/tenant-scoped.
|
|
69
|
+
|
|
70
|
+
### Factory → factory function returning an abstraction
|
|
71
|
+
|
|
72
|
+
Isolate creation logic; return an interface, not a concrete class. A custom hook can be a factory of
|
|
73
|
+
React-bound values.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
export const createLogger = (env: Env): Logger => (env.production ? new RemoteLogger() : new ConsoleLogger());
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Command → framework-agnostic use-case, optionally wrapped by an Action
|
|
80
|
+
|
|
81
|
+
Encapsulate one action as a plain (async) function in a framework-agnostic module. Invoke it from an
|
|
82
|
+
event handler, a store action, or — for form submission — a React 19 **Action**.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// application/use-case — no React imports
|
|
86
|
+
export const checkoutCart = (cart: Cart): Promise<Order> => orderApi.place(cart);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
// component: wrap the use-case in an Action for pending/optimistic state
|
|
91
|
+
const [order, submit, isPending] = useActionState<Order | null, FormData>(
|
|
92
|
+
(_prev, formData) => checkoutCart(cartFromForm(formData)),
|
|
93
|
+
null,
|
|
94
|
+
);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A Zustand action is **not automatically** the Command — keep the action a distinct framework-agnostic use-case (function) the store action **delegates** to. Full rule + layering: catalog → _Command_.
|
|
98
|
+
|
|
99
|
+
### Adapter / DTO Mapping → pure `from`/`to` at the data boundary
|
|
100
|
+
|
|
101
|
+
Map DTO ↔ domain inside the api client / query function; never leak DTOs into components or JSX.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// UserDto / User / userFromDto: see catalog -> Adapter / DTO Mapping
|
|
105
|
+
export const getUser = async (id: string): Promise<User> =>
|
|
106
|
+
userFromDto(await api.get<UserDto>(`/api/users/${id}`));
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Composition → custom hooks + children/slots
|
|
110
|
+
|
|
111
|
+
Compose behavior through custom hooks and component composition (children, slot props) — not
|
|
112
|
+
inheritance, not mixins, not deep HOC stacks.
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
function useTooltip(ref: RefObject<HTMLElement | null>) {
|
|
116
|
+
/* attach/detach behavior, returns state */
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function Card({ media, children }: { media?: ReactNode; children: ReactNode }) {
|
|
120
|
+
return (
|
|
121
|
+
<section>
|
|
122
|
+
{media}
|
|
123
|
+
{children}
|
|
124
|
+
</section>
|
|
125
|
+
); // behavior composed via hooks + slots, no base class
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Singleton / shared state → a Zustand store exposing state + actions
|
|
130
|
+
|
|
131
|
+
The common case for app-wide state. Keep the shape minimal; mutate only through actions; **select
|
|
132
|
+
narrowly** so components re-render on the slices they read. Use `useShallow` for object/array selections
|
|
133
|
+
and derive in selectors (or in actions) rather than duplicating derived state.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { create } from 'zustand';
|
|
137
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
138
|
+
|
|
139
|
+
interface CartState {
|
|
140
|
+
items: readonly CartItem[];
|
|
141
|
+
add: (item: CartItem) => void;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const useCartStore = create<CartState>((set) => ({
|
|
145
|
+
items: [],
|
|
146
|
+
add: (item) => set((s) => ({ items: [...s.items, item] })),
|
|
147
|
+
}));
|
|
148
|
+
|
|
149
|
+
// consume — narrow selector; derived value computed in the selector
|
|
150
|
+
const total = useCartStore((s) => s.items.reduce((sum, i) => sum + i.price, 0));
|
|
151
|
+
const { items, add } = useCartStore(useShallow((s) => ({ items: s.items, add: s.add })));
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
For app-wide _services_ (not reactive state), inject via Context + a typed hook. Reserve Context for
|
|
155
|
+
stable dependencies; route high-frequency state through the store to avoid re-rendering the subtree.
|
|
156
|
+
|
|
157
|
+
## Cross-cutting React practice
|
|
158
|
+
|
|
159
|
+
### Derive during render, not in effects
|
|
160
|
+
|
|
161
|
+
Compute values from props/state inline (the compiler memoizes). Do **not** mirror props into state with
|
|
162
|
+
`useEffect` + `setState`.
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
// Bad: const [full, setFull] = useState(''); useEffect(() => setFull(`${first} ${last}`), [first, last]);
|
|
166
|
+
// Good:
|
|
167
|
+
const fullName = `${first} ${last}`;
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Effects only at external boundaries
|
|
171
|
+
|
|
172
|
+
`useEffect` is for synchronizing with systems outside React (subscriptions, timers, non-React widgets,
|
|
173
|
+
manual DOM). Always return a cleanup. Event-response logic belongs in the event handler, not an effect.
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
const sub = socket.subscribe(onMessage);
|
|
178
|
+
return () => sub.unsubscribe();
|
|
179
|
+
}, [socket]);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Data fetching at the edge
|
|
183
|
+
|
|
184
|
+
Read promises with `use()` under `<Suspense>`, or use a cache (TanStack Query) for client data; fetch
|
|
185
|
+
on the server with Server Components / server functions where the framework supports them. Map DTO→domain
|
|
186
|
+
in the fetcher. Don't fetch in `useEffect` for primary data in new code.
|
|
187
|
+
|
|
188
|
+
### Let the compiler memoize
|
|
189
|
+
|
|
190
|
+
With the React Compiler enabled, do not hand-roll `useMemo` / `useCallback` / `memo`. Add them back only
|
|
191
|
+
when the compiler is disabled for a file or profiling proves a hot path.
|
|
192
|
+
|
|
193
|
+
### Keep components thin
|
|
194
|
+
|
|
195
|
+
Components render UI, bind state, dispatch actions. Push logic and mapping into hooks, the store, and
|
|
196
|
+
pure framework-agnostic functions.
|
|
197
|
+
|
|
198
|
+
### Forms via Actions
|
|
199
|
+
|
|
200
|
+
Prefer React 19 form **Actions** — `useActionState` for submit + result, `useFormStatus` for pending
|
|
201
|
+
UI, `useOptimistic` for optimistic updates. Keep validation rules in pure functions (zod/valibot) the
|
|
202
|
+
Action just calls. Controlled inputs remain fine for small, interactive forms.
|
|
203
|
+
|
|
204
|
+
### `ref` is a prop
|
|
205
|
+
|
|
206
|
+
Pass `ref` directly to function components (React 19) — do **not** write `forwardRef` in new code.
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
function TextField({ ref, ...props }: { ref?: Ref<HTMLInputElement> } & InputProps) {
|
|
210
|
+
return <input ref={ref} {...props} />;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### JSX SRP
|
|
215
|
+
|
|
216
|
+
No business logic in JSX. Move ternaries, multi-condition branches, and data-transforming calls into
|
|
217
|
+
derived consts (or selectors).
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
// Bad: {user.isActive ? 'Active' : 'Inactive'}
|
|
221
|
+
const statusLabel = user.isActive ? 'Active' : 'Inactive';
|
|
222
|
+
// JSX: {statusLabel}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## React Decision Matrix
|
|
226
|
+
|
|
227
|
+
| Problem | Recommended Solution |
|
|
228
|
+
| ----------------------------- | ------------------------------------------------------------ |
|
|
229
|
+
| Local component state | useState / useReducer |
|
|
230
|
+
| Derived value | compute during render (compiler memoizes) |
|
|
231
|
+
| Shared app state | Zustand store + narrow selectors / useShallow |
|
|
232
|
+
| Dependency injection | Context + a typed custom hook (`<Context value>`) |
|
|
233
|
+
| Interchangeable behaviors | Strategy (`as const satisfies` lookup map) |
|
|
234
|
+
| Plugin / extension system | Registry (self-registering Map, zod for dynamic) |
|
|
235
|
+
| Object creation with logic | Factory (factory function / hook returning an abstraction) |
|
|
236
|
+
| Reusable action | Command (framework-agnostic use-case, store/Action delegates) |
|
|
237
|
+
| API ↔ domain shape mismatch | Adapter — fromDto / toDto in the fetcher |
|
|
238
|
+
| Cross-cutting behavior | Composition (custom hooks + children/slots) |
|
|
239
|
+
| Data fetching | use() + Suspense / TanStack Query / Server Components |
|
|
240
|
+
| Form | React 19 Actions (useActionState / useFormStatus / useOptimistic) |
|
|
241
|
+
| External system sync | useEffect with cleanup |
|
|
242
|
+
| Forwarding a ref | `ref` as a normal prop (no forwardRef) |
|
|
243
|
+
| Memoization | React Compiler (no manual useMemo/useCallback) |
|
|
244
|
+
|
|
245
|
+
## Anti-Patterns to Avoid
|
|
246
|
+
|
|
247
|
+
- class components; `forwardRef` in new code
|
|
248
|
+
- deriving state with `useEffect` + `setState`; storing derived data in `useState`
|
|
249
|
+
- fetching primary data in `useEffect` (use `use()` + Suspense / a query cache / the server)
|
|
250
|
+
- business logic in components; DTOs leaking into JSX; god hooks/components; string magic / `switch` on a type tag instead of an `as const satisfies` lookup map
|
|
251
|
+
- high-frequency global state in Context (re-renders the subtree) — use a Zustand store; reserve Context for DI
|
|
252
|
+
- selecting the whole store object without `useShallow` (re-renders on every change)
|
|
253
|
+
- hand-rolled `useMemo` / `useCallback` / `memo` noise when the React Compiler is enabled
|
|
254
|
+
- prop-drilling shared state deep instead of composition / a store
|
|
255
|
+
- a Command merged into a store action when it must be reusable — keep it a distinct use-case the action delegates to
|
|
256
|
+
- inheritance / mixins; `any`; magic strings; mutating props or store state outside actions
|