@eventmodelers/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/README.md +175 -0
  2. package/cli.js +676 -0
  3. package/package.json +35 -0
  4. package/shared/build-kit/code-export.mjs +560 -0
  5. package/shared/build-kit/lib/ollama-agent.js +147 -0
  6. package/shared/build-kit/package.json +11 -0
  7. package/shared/build-kit/ralph-ollama.js +39 -0
  8. package/shared/build-kit/realtime-agent.js +18 -0
  9. package/stacks/axon/templates/.claude/skills/build-automation/SKILL.md +407 -0
  10. package/stacks/axon/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +114 -0
  11. package/stacks/axon/templates/.claude/skills/build-automation-workflow/SKILL.md +496 -0
  12. package/stacks/axon/templates/.claude/skills/build-automation-workflow/references/axon-workflow-api.md +279 -0
  13. package/stacks/axon/templates/.claude/skills/build-automation-workflow/references/feature-flag-patterns.md +114 -0
  14. package/stacks/axon/templates/.claude/skills/build-state-change/SKILL.md +396 -0
  15. package/stacks/axon/templates/.claude/skills/build-state-change/references/axon-test-fixture-patterns.md +162 -0
  16. package/stacks/axon/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +56 -0
  17. package/stacks/axon/templates/.claude/skills/build-state-change/references/rest-api-patterns.md +115 -0
  18. package/stacks/axon/templates/.claude/skills/build-state-view/SKILL.md +282 -0
  19. package/stacks/axon/templates/.claude/skills/connect/SKILL.md +178 -0
  20. package/stacks/axon/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +611 -0
  21. package/stacks/axon/templates/.claude/skills/load-slice/SKILL.md +141 -0
  22. package/stacks/axon/templates/.claude/skills/update-slice-status/SKILL.md +105 -0
  23. package/stacks/axon/templates/build-kit/lib/AGENT.md +47 -0
  24. package/stacks/axon/templates/build-kit/lib/agent.sh +20 -0
  25. package/stacks/axon/templates/build-kit/lib/backend-prompt.md +158 -0
  26. package/stacks/axon/templates/build-kit/lib/prompt.md +126 -0
  27. package/stacks/axon/templates/build-kit/lib/ralph.js +369 -0
  28. package/stacks/axon/templates/build-kit/ralph-claude.js +44 -0
  29. package/stacks/axon/templates/build-kit/ralph.sh +98 -0
  30. package/stacks/axon/templates/root/.env.example +5 -0
  31. package/stacks/axon/templates/root/CLAUDE.md +60 -0
  32. package/stacks/axon/templates/root/README.md +44 -0
  33. package/stacks/axon/templates/root/docker-compose.yml +64 -0
  34. package/stacks/axon/templates/root/mvnw +259 -0
  35. package/stacks/axon/templates/root/mvnw.cmd +149 -0
  36. package/stacks/axon/templates/root/pom.xml +125 -0
  37. package/stacks/axon/templates/root/src/main/java/io/axoniq/quickstart/QuickstartApplication.java +128 -0
  38. package/stacks/axon/templates/root/src/main/java/io/axoniq/quickstart/config/WebConfig.java +155 -0
  39. package/stacks/axon/templates/root/src/main/resources/application.properties +2 -0
  40. package/stacks/axon/templates/root/src/main/resources/static/bg-body.png +0 -0
  41. package/stacks/axon/templates/root/src/main/resources/static/bg-header.png +0 -0
  42. package/stacks/axon/templates/root/src/main/resources/static/index.html +596 -0
  43. package/stacks/cratis-csharp/templates/.claude/skills/_shared/cratis-conventions.md +251 -0
  44. package/stacks/cratis-csharp/templates/.claude/skills/build-automation/SKILL.md +99 -0
  45. package/stacks/cratis-csharp/templates/.claude/skills/build-automation/references/patterns.md +115 -0
  46. package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/SKILL.md +163 -0
  47. package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/references/patterns.md +234 -0
  48. package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/SKILL.md +120 -0
  49. package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/references/patterns.md +166 -0
  50. package/stacks/cratis-csharp/templates/.claude/skills/connect/SKILL.md +169 -0
  51. package/stacks/cratis-csharp/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +609 -0
  52. package/stacks/cratis-csharp/templates/.claude/skills/load-slice/SKILL.md +141 -0
  53. package/stacks/cratis-csharp/templates/.claude/skills/update-slice-status/SKILL.md +105 -0
  54. package/stacks/cratis-csharp/templates/build-kit/lib/AGENT.md +58 -0
  55. package/stacks/cratis-csharp/templates/build-kit/lib/agent.sh +20 -0
  56. package/stacks/cratis-csharp/templates/build-kit/lib/backend-prompt.md +129 -0
  57. package/stacks/cratis-csharp/templates/build-kit/lib/prompt.md +124 -0
  58. package/stacks/cratis-csharp/templates/build-kit/lib/ralph.js +302 -0
  59. package/stacks/cratis-csharp/templates/build-kit/ralph-claude.js +37 -0
  60. package/stacks/cratis-csharp/templates/build-kit/ralph.sh +98 -0
  61. package/stacks/cratis-csharp/templates/root/.frontend/index.css +29 -0
  62. package/stacks/cratis-csharp/templates/root/.frontend/index.html +17 -0
  63. package/stacks/cratis-csharp/templates/root/.frontend/main.tsx +18 -0
  64. package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.json +42 -0
  65. package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.node.json +11 -0
  66. package/stacks/cratis-csharp/templates/root/.frontend/vite.config.ts +56 -0
  67. package/stacks/cratis-csharp/templates/root/App.tsx +23 -0
  68. package/stacks/cratis-csharp/templates/root/CLAUDE.md +62 -0
  69. package/stacks/cratis-csharp/templates/root/CratisApp.csproj +25 -0
  70. package/stacks/cratis-csharp/templates/root/CratisApp.sln +18 -0
  71. package/stacks/cratis-csharp/templates/root/GlobalUsings.cs +3 -0
  72. package/stacks/cratis-csharp/templates/root/Home.tsx +102 -0
  73. package/stacks/cratis-csharp/templates/root/Program.cs +26 -0
  74. package/stacks/cratis-csharp/templates/root/README.md +192 -0
  75. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/AllListings.ts +47 -0
  76. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.cs +11 -0
  77. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.ts +12 -0
  78. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/ListingDataTable.tsx +17 -0
  79. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/index.ts +1 -0
  80. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Register.ts +51 -0
  81. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/RegisterDialog.tsx +18 -0
  82. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Registration.cs +27 -0
  83. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/index.ts +1 -0
  84. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeFeature.tsx +22 -0
  85. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeName.cs +3 -0
  86. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/index.ts +1 -0
  87. package/stacks/cratis-csharp/templates/root/appsettings.Development.json +9 -0
  88. package/stacks/cratis-csharp/templates/root/appsettings.json +26 -0
  89. package/stacks/cratis-csharp/templates/root/docker-compose.yml +23 -0
  90. package/stacks/cratis-csharp/templates/root/package.json +33 -0
  91. package/stacks/cratis-csharp/templates/root/tsconfig.json +3 -0
  92. package/stacks/modeling-kit/templates/.claude/settings.local.json +13 -0
  93. package/stacks/modeling-kit/templates/.claude/skills/analyze-existing-model/SKILL.md +215 -0
  94. package/stacks/modeling-kit/templates/.claude/skills/attributes/SKILL.md +176 -0
  95. package/stacks/modeling-kit/templates/.claude/skills/connect/SKILL.md +182 -0
  96. package/stacks/modeling-kit/templates/.claude/skills/discover-storyboard/SKILL.md +383 -0
  97. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-applying-conways-law/SKILL.md +465 -0
  98. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/SKILL.md +587 -0
  99. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/references/facilitating-event-modeling-workshops.md +626 -0
  100. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-checking-completeness/SKILL.md +542 -0
  101. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-designing-event-models/SKILL.md +497 -0
  102. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +783 -0
  103. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-inputs/SKILL.md +604 -0
  104. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +699 -0
  105. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-integrating-legacy-systems/SKILL.md +674 -0
  106. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/SKILL.md +231 -0
  107. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/domain-patterns.md +150 -0
  108. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/patterns.md +329 -0
  109. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/snapshotting.md +204 -0
  110. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +352 -0
  111. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/references/project-planning-with-event-modeling.md +341 -0
  112. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-plotting-events/SKILL.md +153 -0
  113. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/SKILL.md +136 -0
  114. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/references/examples.md +78 -0
  115. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/references/patterns.md +85 -0
  116. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md +655 -0
  117. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-translating-external-events/SKILL.md +479 -0
  118. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models/SKILL.md +234 -0
  119. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models-checklist/SKILL.md +353 -0
  120. package/stacks/modeling-kit/templates/.claude/skills/examples/SKILL.md +200 -0
  121. package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +94 -0
  122. package/stacks/modeling-kit/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +441 -0
  123. package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +290 -0
  124. package/stacks/modeling-kit/templates/.claude/skills/storyboard/SKILL.md +212 -0
  125. package/stacks/modeling-kit/templates/.claude/skills/storyboard-screen/SKILL.md +123 -0
  126. package/stacks/modeling-kit/templates/.claude/skills/timeline/SKILL.md +353 -0
  127. package/stacks/modeling-kit/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
  128. package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +134 -0
  129. package/stacks/modeling-kit/templates/kit/lib/agent.sh +20 -0
  130. package/stacks/modeling-kit/templates/kit/lib/ollama-agent.js +147 -0
  131. package/stacks/modeling-kit/templates/kit/lib/ralph.js +286 -0
  132. package/stacks/modeling-kit/templates/kit/package.json +12 -0
  133. package/stacks/modeling-kit/templates/kit/ralph-claude.js +50 -0
  134. package/stacks/modeling-kit/templates/kit/ralph-ollama.js +38 -0
  135. package/stacks/modeling-kit/templates/kit/ralph.sh +61 -0
  136. package/stacks/modeling-kit/templates/kit/realtime-agent.js +18 -0
  137. package/stacks/modeling-kit/templates/root/CLAUDE.md +56 -0
  138. package/stacks/node/templates/.claude/skills/build-automation/SKILL.md +266 -0
  139. package/stacks/node/templates/.claude/skills/build-state-change/SKILL.md +344 -0
  140. package/stacks/node/templates/.claude/skills/build-state-view/SKILL.md +412 -0
  141. package/stacks/node/templates/.claude/skills/connect/SKILL.md +178 -0
  142. package/stacks/node/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +628 -0
  143. package/stacks/node/templates/.claude/skills/load-slice/SKILL.md +143 -0
  144. package/stacks/node/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
  145. package/stacks/node/templates/build-kit/README.md +86 -0
  146. package/stacks/node/templates/build-kit/lib/AGENT.md +47 -0
  147. package/stacks/node/templates/build-kit/lib/agent.sh +20 -0
  148. package/stacks/node/templates/build-kit/lib/backend-prompt.md +152 -0
  149. package/stacks/node/templates/build-kit/lib/prompt.md +122 -0
  150. package/stacks/node/templates/build-kit/lib/ralph.js +369 -0
  151. package/stacks/node/templates/build-kit/ralph-claude.js +44 -0
  152. package/stacks/node/templates/build-kit/ralph.sh +98 -0
  153. package/stacks/node/templates/root/.env.example +15 -0
  154. package/stacks/node/templates/root/CLAUDE.md +60 -0
  155. package/stacks/node/templates/root/docker-compose.yml +15 -0
  156. package/stacks/node/templates/root/flyway.conf +17 -0
  157. package/stacks/node/templates/root/migrations/V1__schema.sql.example +12 -0
  158. package/stacks/node/templates/root/package.json +50 -0
  159. package/stacks/node/templates/root/server.ts +130 -0
  160. package/stacks/node/templates/root/setup-env.sh +53 -0
  161. package/stacks/node/templates/root/src/common/assertions.ts +6 -0
  162. package/stacks/node/templates/root/src/common/db.ts +32 -0
  163. package/stacks/node/templates/root/src/common/loadPostgresEventstore.ts +23 -0
  164. package/stacks/node/templates/root/src/common/parseEndpoint.ts +51 -0
  165. package/stacks/node/templates/root/src/common/processorDlq.ts +28 -0
  166. package/stacks/node/templates/root/src/common/realtimeBroadcast.ts +12 -0
  167. package/stacks/node/templates/root/src/common/replay.ts +16 -0
  168. package/stacks/node/templates/root/src/common/routes.ts +19 -0
  169. package/stacks/node/templates/root/src/common/testHelpers.ts +44 -0
  170. package/stacks/node/templates/root/src/swagger.ts +34 -0
  171. package/stacks/node/templates/root/src/util/assertions.ts +6 -0
  172. package/stacks/node/templates/root/src/util/hash.ts +9 -0
  173. package/stacks/node/templates/root/src/util/sanitize.ts +23 -0
  174. package/stacks/node/templates/root/tsconfig.json +32 -0
  175. package/stacks/node/templates/root/vercel.json +8 -0
  176. package/stacks/supabase/templates/.claude/skills/build-automation/SKILL.md +266 -0
  177. package/stacks/supabase/templates/.claude/skills/build-state-change/SKILL.md +344 -0
  178. package/stacks/supabase/templates/.claude/skills/build-state-view/SKILL.md +390 -0
  179. package/stacks/supabase/templates/.claude/skills/build-webhook/SKILL.md +303 -0
  180. package/stacks/supabase/templates/.claude/skills/connect/SKILL.md +178 -0
  181. package/stacks/supabase/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +628 -0
  182. package/stacks/supabase/templates/.claude/skills/load-slice/SKILL.md +143 -0
  183. package/stacks/supabase/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
  184. package/stacks/supabase/templates/build-kit/README.md +86 -0
  185. package/stacks/supabase/templates/build-kit/lib/AGENT.md +47 -0
  186. package/stacks/supabase/templates/build-kit/lib/agent.sh +20 -0
  187. package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +152 -0
  188. package/stacks/supabase/templates/build-kit/lib/prompt.md +123 -0
  189. package/stacks/supabase/templates/build-kit/lib/ralph.js +369 -0
  190. package/stacks/supabase/templates/build-kit/ralph-claude.js +44 -0
  191. package/stacks/supabase/templates/build-kit/ralph.sh +98 -0
  192. package/stacks/supabase/templates/root/.env.example +22 -0
  193. package/stacks/supabase/templates/root/CLAUDE.md +61 -0
  194. package/stacks/supabase/templates/root/flyway.conf +17 -0
  195. package/stacks/supabase/templates/root/package.json +52 -0
  196. package/stacks/supabase/templates/root/server.ts +177 -0
  197. package/stacks/supabase/templates/root/setup-env.sh +53 -0
  198. package/stacks/supabase/templates/root/src/common/assertions.ts +6 -0
  199. package/stacks/supabase/templates/root/src/common/db.ts +32 -0
  200. package/stacks/supabase/templates/root/src/common/loadPostgresEventstore.ts +23 -0
  201. package/stacks/supabase/templates/root/src/common/parseEndpoint.ts +51 -0
  202. package/stacks/supabase/templates/root/src/common/processorDlq.ts +28 -0
  203. package/stacks/supabase/templates/root/src/common/realtimeBroadcast.ts +19 -0
  204. package/stacks/supabase/templates/root/src/common/replay.ts +16 -0
  205. package/stacks/supabase/templates/root/src/common/routes.ts +23 -0
  206. package/stacks/supabase/templates/root/src/common/testHelpers.ts +54 -0
  207. package/stacks/supabase/templates/root/src/supabase/README.md +141 -0
  208. package/stacks/supabase/templates/root/src/supabase/api.ts +56 -0
  209. package/stacks/supabase/templates/root/src/supabase/requireOrgaAdmin.ts +32 -0
  210. package/stacks/supabase/templates/root/src/supabase/requireSysUser.ts +40 -0
  211. package/stacks/supabase/templates/root/src/supabase/requireUser.ts +72 -0
  212. package/stacks/supabase/templates/root/src/swagger.ts +34 -0
  213. package/stacks/supabase/templates/root/src/util/assertions.ts +6 -0
  214. package/stacks/supabase/templates/root/src/util/hash.ts +9 -0
  215. package/stacks/supabase/templates/root/src/util/sanitize.ts +23 -0
  216. package/stacks/supabase/templates/root/supabase/config.toml +295 -0
  217. package/stacks/supabase/templates/root/supabase/migrations/V1__schema.sql.example +12 -0
  218. package/stacks/supabase/templates/root/supabase/seed.sql +1 -0
  219. package/stacks/supabase/templates/root/tsconfig.json +32 -0
  220. package/stacks/supabase/templates/root/vercel.json +8 -0
@@ -0,0 +1,396 @@
1
+ ---
2
+ name: build-state-change
3
+ authors:
4
+ - Mateusz Nowak
5
+ - Martin Dilger
6
+ description: >
7
+ Implement Event Sourcing write slices using Axon Framework 5.1.1 in this project's one established
8
+ pattern: Command record → mutable decision-model entity (@EventSourced + @EventCriteriaBuilder) →
9
+ @Component CommandHandler that checks entity state inline → AxonTestFixture unit test (no Spring
10
+ context). Use when implementing a new write slice / command handler from a slice.json event model in
11
+ this project. There is exactly one supported style — do not offer alternatives.
12
+ ---
13
+
14
+ # Axon Framework 5 — Write Slice
15
+
16
+ One pattern only. Directory layout is flat — `src/main/java/.../slices/{context}/{slicename}/`, no
17
+ `write`/`read`/`automation` folder layer in between (only the shared `slices/{context}/events/` folder
18
+ sits alongside slice folders). Every step below is grounded in the `RegisterCustomer` slice
19
+ (`src/main/java/.../foo/register/`, test in
20
+ `src/test/java/.../foo/register/RegisterCustomerDecisionModelTest.java`) and, for the compound-identifier
21
+ case in Step 1, the `SubscribeToCourse` slice (`src/main/java/.../foo/subscribetocourse/`) — both
22
+ verified, compiled and passing under `mvn test` against `io.axoniq.framework:axoniq-framework-bom:5.1.1`.
23
+
24
+ ## Step 0: Read the slice definition
25
+
26
+ Read `.build-kit/.slices/{context}/{slicename}/slice.json`. Extract, and use **only** what's there:
27
+
28
+ - `commands[].fields[]` → Command record fields, in order
29
+ - `events[].fields[]` → Event record fields, in order
30
+ - `specifications[]` (GWT scenarios) → one test method per scenario
31
+ - Which command field(s) have `idAttribute: true` — these carry `@TargetEntityId` (see Step 1) and the
32
+ matching event field(s) carry `@EventTag`
33
+
34
+ Never invent a field, business rule, or event that isn't in slice.json.
35
+
36
+ ## Step 1: Command
37
+
38
+ **Exactly one field has `idAttribute: true` in slice.json** — annotate it directly:
39
+
40
+ ```java
41
+ package io.axoniq.quickstart.slices.{context}.{slicename};
42
+
43
+ import org.axonframework.messaging.commandhandling.annotation.Command;
44
+ import org.axonframework.modelling.annotation.TargetEntityId;
45
+
46
+ @Command(namespace = "{Context}", name = "{SliceName}", version = "1.0.0")
47
+ public record {SliceName}Command(
48
+ String field1,
49
+ @TargetEntityId
50
+ String idField
51
+ ) {}
52
+ ```
53
+
54
+ **Two or more fields have `idAttribute: true`** — Axon only allows the resolved identifier to collapse
55
+ to exactly one distinct non-null value (`AnnotationBasedEntityIdResolver`, package
56
+ `org.axonframework.modelling.annotation`); annotating two fields with different values throws
57
+ `EntityIdResolutionException` at command-dispatch time, not at compile time. **Never annotate more than
58
+ one field directly.** Instead build a compound id record and put `@TargetEntityId` on a derived method —
59
+ verified against the `SubscribeToCourse` slice (`email` + `courseId` both `idAttribute: true`):
60
+
61
+ ```java
62
+ package io.axoniq.quickstart.slices.{context}.{slicename};
63
+
64
+ public record {SliceName}Id(String field1, String field2) {}
65
+ ```
66
+
67
+ ```java
68
+ package io.axoniq.quickstart.slices.{context}.{slicename};
69
+
70
+ import org.axonframework.messaging.commandhandling.annotation.Command;
71
+ import org.axonframework.modelling.annotation.TargetEntityId;
72
+
73
+ @Command(namespace = "{Context}", name = "{SliceName}", version = "1.0.0")
74
+ public record {SliceName}Command(String field1, String field2) {
75
+
76
+ @TargetEntityId
77
+ public {SliceName}Id identifier() {
78
+ return new {SliceName}Id(field1, field2);
79
+ }
80
+ }
81
+ ```
82
+
83
+ The entity's generic id type becomes `{SliceName}Id` everywhere it's referenced (Step 3's
84
+ `@EventCriteriaBuilder` first parameter, Step 7's `EventSourcedEntityModule.autodetected({SliceName}Id.class, ...)`)
85
+ instead of `String`. `@TargetEntityId` scans both fields and methods on the payload
86
+ (`AnnotationBasedEntityIdResolver.findMembers`), so a method works exactly like a field for this purpose.
87
+
88
+ ## Step 2: Event — only if it doesn't already exist
89
+
90
+ Check `src/main/java/.../{context}/events/` first; add to the existing sealed/marker interface rather
91
+ than creating a duplicate.
92
+
93
+ ```java
94
+ package io.axoniq.quickstart.slices.{context}.events;
95
+
96
+ import org.axonframework.eventsourcing.annotation.EventTag;
97
+ import org.axonframework.messaging.eventhandling.annotation.Event;
98
+
99
+ @Event(namespace = "{Context}", name = "{EventName}", version = "1.0.0")
100
+ public record {EventName}(
101
+ String field1,
102
+ @EventTag(key = EventTags.{TAG_CONSTANT})
103
+ String idField
104
+ ) implements {Context}Event {}
105
+ ```
106
+
107
+ Add the tag constant to the context's `EventTags` class if it isn't already there:
108
+
109
+ ```java
110
+ public static final String {TAG_CONSTANT} = "idField";
111
+ ```
112
+
113
+ ## Step 3: Decision-model entity
114
+
115
+ Package-private, mutable field(s) — **not** an immutable `State` record with free-standing
116
+ `decide()`/`evolve()` static methods. One boolean/value field per fact the command handler's rule
117
+ check needs, nothing else.
118
+
119
+ ```java
120
+ package io.axoniq.quickstart.slices.{context}.{slicename};
121
+
122
+ import io.axoniq.quickstart.slices.{context}.events.{EventName};
123
+ import io.axoniq.quickstart.slices.{context}.events.EventTags;
124
+ import org.axonframework.eventsourcing.annotation.EventCriteriaBuilder;
125
+ import org.axonframework.eventsourcing.annotation.EventSourcingHandler;
126
+ import org.axonframework.eventsourcing.annotation.reflection.EntityCreator;
127
+ import org.axonframework.extension.spring.stereotype.EventSourced;
128
+ import org.axonframework.messaging.core.MessageTypeResolver;
129
+ import org.axonframework.messaging.eventstreaming.EventCriteria;
130
+ import org.axonframework.messaging.eventstreaming.Tag;
131
+
132
+ @EventSourced
133
+ class {SliceName}DecisionModel {
134
+
135
+ public boolean <ruleFlag>;
136
+
137
+ @EntityCreator
138
+ private {SliceName}DecisionModel() {
139
+ }
140
+
141
+ @EventCriteriaBuilder
142
+ private static EventCriteria resolveCriteria(String idField, MessageTypeResolver messageTypeResolver) {
143
+ return EventCriteria.havingTags(Tag.of(EventTags.{TAG_CONSTANT}, idField))
144
+ .andBeingOneOfTypes(messageTypeResolver, {EventName}.class);
145
+ }
146
+
147
+ @EventSourcingHandler
148
+ public void on({EventName} event) {
149
+ this.<ruleFlag> = true;
150
+ }
151
+ }
152
+ ```
153
+
154
+ **Always write `@EventCriteriaBuilder`. Never rely on bare `@EventSourced(tagKey = "...")`.** A tag-key
155
+ mismatch produces no compiler error and no runtime exception — the entity just silently never loads any
156
+ prior events, so it always looks "fresh" and the business rule never fires. `@EventCriteriaBuilder`'s
157
+ first parameter is always the `@TargetEntityId` type; a second `MessageTypeResolver` parameter is
158
+ resolved automatically from `Configuration` — pass it plus the event class straight into
159
+ `andBeingOneOfTypes(messageTypeResolver, {EventName}.class)`.
160
+
161
+ **The `MessageTypeResolver` parameter is not optional — don't drop it to save an injection.** It was
162
+ tempting to think `andBeingOneOfTypes(new QualifiedName({EventName}.class))` should work without it,
163
+ since `QualifiedName` has a `Class<?>` constructor. Verified experimentally that it does NOT:
164
+ `QualifiedName(Class<?>)`'s only job is `clazz.getName()` — the raw Java class name
165
+ (`io.axoniq.quickstart.slices.foo.events.CustomerRegistered`) — not the `@Event(namespace, name)` value
166
+ the event was actually appended under (`Foo.CustomerRegistered`). Swapping to it in
167
+ `SubscribeToCourseDecisionModel` as a test made 2 of 3 passing tests fail immediately, silently, with
168
+ no exception pointing at the real cause — the criteria just stopped matching anything, identical in
169
+ effect to a `tagKey` mismatch. `MessageTypeResolver` is what correctly bridges the `@Event` annotation
170
+ to the type Axon actually matches on; there is no annotation-free shortcut. Never hand-type a
171
+ `"Namespace.Name"` string either, and never pass `{EventName}.class.getName()` — both drift silently
172
+ out of sync with the event's `@Event` annotation with no compiler check.
173
+
174
+ `EventCriteria`/`Tag` live in `org.axonframework.messaging.eventstreaming` in 5.1.1 — not
175
+ `org.axonframework.eventsourcing.eventstore` (that package has no such classes in this version).
176
+
177
+ **If Step 1 used a compound `{SliceName}Id`** (two or more `idAttribute: true` fields), the first
178
+ parameter here is `{SliceName}Id id` instead of `String idField` — pull the individual values back out
179
+ with `id.field1()`/`id.field2()` for tagging.
180
+
181
+ **Tag each event type by what THIS decision actually needs checked for it — not uniformly.** When
182
+ multiple event types feed one entity (via `EventCriteria.either(...)`), each branch gets its own tag
183
+ set, chosen per the specific invariant that event type is being loaded to verify. This is context-
184
+ dependent: the same event type can legitimately need a wider or narrower tag set in a different slice
185
+ that reads it for a different rule. Verified worked example — `SubscribeToCourseDecisionModel`, id is
186
+ `SubscriptionId(email, courseId)`, two rules, two different tag scopes on two different event types:
187
+
188
+ ```java
189
+ @EventCriteriaBuilder
190
+ private static EventCriteria resolveCriteria(SubscriptionId id, MessageTypeResolver messageTypeResolver) {
191
+ return EventCriteria.either(
192
+ // "is this customer registered at all" — scoped to email only
193
+ EventCriteria.havingTags(Tag.of(EventTags.EMAIL, id.email()))
194
+ .andBeingOneOfTypes(messageTypeResolver, CustomerRegistered.class),
195
+ // "did this customer already subscribe to THIS course" — scoped to email + courseId
196
+ EventCriteria.havingTags(Tag.of(EventTags.EMAIL, id.email()), Tag.of(EventTags.COURSE_ID, id.courseId()))
197
+ .andBeingOneOfTypes(messageTypeResolver, SubscribedToCourse.class)
198
+ );
199
+ }
200
+ ```
201
+
202
+ `CustomerRegistered` only needs the `email` tag — "is this customer registered" doesn't involve a
203
+ course. `SubscribedToCourse` needs **both** `email` and `courseId` tags together — the rule is "already
204
+ subscribed to *this* course", not "subscribed to any course", so loading a customer's subscription to a
205
+ *different* course must not satisfy it. This also directly simplifies the entity: narrowing the second
206
+ branch to `email` + `courseId` means the entity only ever sees `SubscribedToCourse` events for this
207
+ exact course, so a `Set<String> subscribedCourseIds` collecting every course is unnecessary — a single
208
+ `boolean alreadySubscribedToThisCourse` set by `@EventSourcingHandler` is enough. Getting the tag scope
209
+ wrong doesn't fail loudly: too-wide a scope silently pulls in unrelated events (correct here only by
210
+ coincidence, e.g. it still worked when courses were checked in Java after the fact) or double-counts;
211
+ too-narrow a scope silently drops events the rule actually needed.
212
+
213
+ A read-side use case for the *same* `SubscribedToCourse` event (e.g. "how many courses is this customer
214
+ subscribed to") would legitimately query it with `email` only, no `courseId` — the correct tag scope is
215
+ a property of the specific decision/query consuming the event, not a fixed property of the event type
216
+ itself.
217
+
218
+ ## Step 4: Command handler
219
+
220
+ ```java
221
+ package io.axoniq.quickstart.slices.{context}.{slicename};
222
+
223
+ import io.axoniq.quickstart.slices.{context}.events.{EventName};
224
+ import org.axonframework.messaging.commandhandling.annotation.CommandHandler;
225
+ import org.axonframework.messaging.core.Metadata;
226
+ import org.axonframework.messaging.eventhandling.gateway.EventAppender;
227
+ import org.axonframework.modelling.annotation.InjectEntity;
228
+ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
229
+ import org.springframework.stereotype.Component;
230
+
231
+ @Component
232
+ @ConditionalOnProperty(prefix = "slices.{context}.write", name = "{slicename}.enabled")
233
+ public class {SliceName}CommandHandler {
234
+
235
+ @CommandHandler
236
+ public void handle(
237
+ {SliceName}Command command,
238
+ Metadata metadata,
239
+ @InjectEntity {SliceName}DecisionModel entity,
240
+ EventAppender eventAppender
241
+ ) {
242
+ if (entity.<ruleFlag>) {
243
+ throw new IllegalStateException("...");
244
+ }
245
+ eventAppender.append(new {EventName}(command.field1(), command.idField()));
246
+ }
247
+ }
248
+ ```
249
+
250
+ `Metadata` is `org.axonframework.messaging.core.Metadata` — **not** `AxonMetadata`, which does not
251
+ exist as a type in this version.
252
+
253
+ ## Step 5: REST endpoint — only if slice.json shows an inbound `SCREEN` dependency on the command
254
+
255
+ ```java
256
+ package io.axoniq.quickstart.slices.{context}.{slicename};
257
+
258
+ import org.axonframework.messaging.commandhandling.gateway.CommandGateway;
259
+ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
260
+ import org.springframework.http.ResponseEntity;
261
+ import org.springframework.web.bind.annotation.PostMapping;
262
+ import org.springframework.web.bind.annotation.RequestBody;
263
+ import org.springframework.web.bind.annotation.RestController;
264
+ import reactor.core.publisher.Mono;
265
+
266
+ @RestController
267
+ @ConditionalOnProperty(prefix = "slices.{context}.write", name = "{slicename}.enabled")
268
+ public class {SliceName}RestController {
269
+
270
+ private final CommandGateway commandGateway;
271
+
272
+ public {SliceName}RestController(CommandGateway commandGateway) {
273
+ this.commandGateway = commandGateway;
274
+ }
275
+
276
+ @PostMapping("/api/{context}/{resource}")
277
+ public Mono<ResponseEntity<Void>> handle(@RequestBody {SliceName}RequestBody body) {
278
+ var command = new {SliceName}Command(body.field1(), body.idField());
279
+ return Mono.fromFuture(commandGateway.send(command).getResultMessage())
280
+ .map(ignored -> ResponseEntity.ok().<Void>build())
281
+ .onErrorResume(e -> Mono.just(ResponseEntity.badRequest().<Void>build()));
282
+ }
283
+
284
+ public record {SliceName}RequestBody(String field1, String idField) {}
285
+ }
286
+ ```
287
+
288
+ If the only inbound dependency is another slice's `AUTOMATION`, skip this step — it calls the
289
+ `CommandGateway` in-process, it doesn't need HTTP.
290
+
291
+ This project uses WebFlux (`Mono<ResponseEntity<...>>`, not plain `ResponseEntity`) — see
292
+ [references/rest-api-patterns.md](references/rest-api-patterns.md) for the full annotated breakdown and
293
+ a `WebTestClient`-based test shape (this repo has no REST test yet — that file's test is the
294
+ recommended pattern for the first one, not a verified-passing example like the other references).
295
+
296
+ ## Step 6: Feature flag
297
+
298
+ Every slice component (handler, REST controller) gets `@ConditionalOnProperty(prefix =
299
+ "slices.{context}.write", name = "{slicename}.enabled")` — the entity does not need it. Wire the flag
300
+ in all three places:
301
+
302
+ - `src/main/resources/application.properties` — `slices.{context}.write.{slicename}.enabled=true`
303
+ - `src/test/resources/application.properties` — `slices.{context}.write.{slicename}.enabled=false`
304
+ - `META-INF/additional-spring-configuration-metadata.json` — add a `{"name": "...", "type":
305
+ "java.lang.Boolean", "description": "..."}` entry
306
+
307
+ This flag is irrelevant to the Step 7 test below — that test never boots Spring, so
308
+ `@ConditionalOnProperty` never runs.
309
+
310
+ ## Step 7: Test — `AxonTestFixture`, no Spring context
311
+
312
+ One-time `pom.xml` addition (no version needed — resolved via the project's existing
313
+ `axoniq-framework-bom` → `axon-framework-bom` import):
314
+
315
+ ```xml
316
+ <dependency>
317
+ <groupId>org.axonframework</groupId>
318
+ <artifactId>axon-test</artifactId>
319
+ <scope>test</scope>
320
+ </dependency>
321
+ ```
322
+
323
+ ```java
324
+ package io.axoniq.quickstart.slices.{context}.{slicename};
325
+
326
+ import io.axoniq.quickstart.slices.{context}.events.{EventName};
327
+ import org.axonframework.eventsourcing.configuration.EventSourcedEntityModule;
328
+ import org.axonframework.eventsourcing.configuration.EventSourcingConfigurer;
329
+ import org.axonframework.messaging.commandhandling.configuration.CommandHandlingModule;
330
+ import org.axonframework.test.fixture.AxonTestFixture;
331
+ import org.junit.jupiter.api.AfterEach;
332
+ import org.junit.jupiter.api.BeforeEach;
333
+ import org.junit.jupiter.api.DisplayName;
334
+ import org.junit.jupiter.api.Test;
335
+
336
+ class {SliceName}DecisionModelTest {
337
+
338
+ private AxonTestFixture fixture;
339
+
340
+ @BeforeEach
341
+ void setUp() {
342
+ var entityModule = EventSourcedEntityModule
343
+ .autodetected(String.class, {SliceName}DecisionModel.class);
344
+ var commandHandlerModule = CommandHandlingModule.named("{SliceName}")
345
+ .commandHandlers()
346
+ .autodetectedCommandHandlingComponent(c -> new {SliceName}CommandHandler());
347
+ var configurer = EventSourcingConfigurer.create()
348
+ .registerEntity(entityModule)
349
+ .registerCommandHandlingModule(commandHandlerModule);
350
+ fixture = AxonTestFixture.with(configurer);
351
+ }
352
+
353
+ @AfterEach
354
+ void tearDown() {
355
+ fixture.stop();
356
+ }
357
+
358
+ @Test
359
+ @DisplayName("given no prior activity, when {sliceName}, then {eventName} emitted")
360
+ void happyPath() {
361
+ fixture.given().noPriorActivity()
362
+ .when().command(new {SliceName}Command("value1", "id-1"))
363
+ .then().success()
364
+ .events(new {EventName}("value1", "id-1"));
365
+ }
366
+
367
+ @Test
368
+ @DisplayName("given <rule already true>, when {sliceName}, then rejected")
369
+ void ruleViolation() {
370
+ fixture.given().event(new {EventName}("value1", "id-1"))
371
+ .when().command(new {SliceName}Command("value2", "id-1"))
372
+ .then().exception(IllegalStateException.class, "...");
373
+ }
374
+ }
375
+ ```
376
+
377
+ One test method per GWT scenario in slice.json's `specifications[]`. This works because
378
+ `EventSourcedEntityModule.autodetected(...)` and `...autodetectedCommandHandlingComponent(...)` operate
379
+ directly on the `@EventSourced`/`@Component` classes via reflection — the same classes Spring
380
+ auto-discovers in production also work standalone, with zero `ApplicationContext`, zero
381
+ `@SpringBootTest`.
382
+
383
+ Fluent API cheat sheet and known gotchas (wrong `EventCriteria`/`Tag` package, a builder method that
384
+ was renamed between AF5 patch versions, `Customization.disableAxonServer()` not existing in 5.1.1):
385
+ see [references/axon-test-fixture-patterns.md](references/axon-test-fixture-patterns.md).
386
+
387
+ ## Final Verification
388
+
389
+ Before considering the slice done:
390
+
391
+ - [ ] Every field in slice.json's `commands[]` is in the Command record — no invented fields, none missing
392
+ - [ ] Every field in slice.json's `events[]` is in the Event record — no invented fields, none missing
393
+ - [ ] Every `specifications[]` scenario has a corresponding test method
394
+ - [ ] No business rule exists in the handler that isn't traceable to slice.json's `description`/`comments`
395
+ - [ ] `mvn compile -q`, then run the slice's own tests only
396
+ - [ ] If checks pass, commit with `feat: {Slice Name}` and set slice status to `Done`
@@ -0,0 +1,162 @@
1
+ # AxonTestFixture Patterns (Verified against Axon Framework 5.1.1)
2
+
3
+ Everything on this page was compiled and run with `mvn test` against
4
+ `io.axoniq.framework:axoniq-framework-bom:5.1.1` (which pulls in
5
+ `org.axonframework:axon-framework-bom:5.1.1`). It supersedes any conflicting claim elsewhere in this
6
+ skill about `@SpringBootTest` / `AxonTestFixture.configSlice(...)` — that method does not exist in
7
+ 5.1.1. Ground-truth worked example: `RegisterCustomer` slice in this repo
8
+ s(`src/main/java/.../foo/register/`, test in `src/test/java/.../foo/register/RegisterCustomerDecisionModelTest.java`).
9
+ Slice folders sit directly under their context — `slices/{context}/{slicename}/`, no `write`/`read`/
10
+ `automation` layer in between.
11
+
12
+ ## 0. Add the dependency
13
+
14
+ `axon-test` is **not** pulled in by `axoniq-spring-boot-starter`. Add it explicitly (no version needed —
15
+ it's managed by the `axoniq-framework-bom` → `axon-framework-bom` import already in the project's `pom.xml`):
16
+
17
+ ```xml
18
+ <dependency>
19
+ <groupId>org.axonframework</groupId>
20
+ <artifactId>axon-test</artifactId>
21
+ <scope>test</scope>
22
+ </dependency>
23
+ ```
24
+
25
+ ## 0b. `@EventSourced` (Spring stereotype) works directly with `EventSourcedEntityModule.autodetected(...)`
26
+
27
+ `org.axonframework.extension.spring.stereotype.EventSourced` is not a plain marker — it aliases its
28
+ attributes onto `org.axonframework.eventsourcing.annotation.EventSourcedEntity` via Spring's
29
+ `@AliasFor` mechanism. `AnnotationBasedEventCriteriaResolver`/`EventSourcedEntityModule.autodetected(...)`
30
+ resolve that alias correctly at runtime even with zero Spring context — verified empirically:
31
+ `EventSourcedEntityModule.autodetected(String.class, RegisterCustomerDecisionModel.class)` succeeded
32
+ against an entity annotated only with the bare `@EventSourced` (no explicit `@EventSourcedEntity`).
33
+ You do **not** need to dual-annotate or swap to `@EventSourcedEntity` just to make the entity
34
+ fixture-testable.
35
+
36
+ ## 1. Key finding: you do NOT need a Spring context to test the entity/handler pair
37
+
38
+ Production code stays `@EventSourced` (Spring stereotype) entity + `@Component` handler, auto-discovered
39
+ by `SpringEventSourcedEntityLookup` at runtime. The test does **not** have to boot Spring —
40
+ `EventSourcedEntityModule.autodetected(...)` and `CommandHandlingModule...autodetectedCommandHandlingComponent(...)`
41
+ work directly off the annotated classes via reflection, regardless of them also being
42
+ `@Component`/`@EventSourced`-Spring-stereotyped for production wiring. This is dramatically faster than
43
+ `@SpringBootTest` (no `ApplicationContext` startup) and needs zero Spring test slices.
44
+
45
+ ```java
46
+ class RegisterCustomerDecisionModelTest {
47
+
48
+ private AxonTestFixture fixture;
49
+
50
+ @BeforeEach
51
+ void setUp() {
52
+ var entityModule = EventSourcedEntityModule
53
+ .autodetected(String.class, RegisterCustomerDecisionModel.class);
54
+ var commandHandlerModule = CommandHandlingModule.named("Register")
55
+ .commandHandlers()
56
+ .autodetectedCommandHandlingComponent(c -> new RegisterCustomerCommandHandler());
57
+ var configurer = EventSourcingConfigurer.create()
58
+ .registerEntity(entityModule)
59
+ .registerCommandHandlingModule(commandHandlerModule);
60
+ fixture = AxonTestFixture.with(configurer);
61
+ }
62
+
63
+ @AfterEach
64
+ void tearDown() {
65
+ fixture.stop();
66
+ }
67
+
68
+ @Test
69
+ @DisplayName("given no prior registration, when register customer, then customer registered emitted")
70
+ void happyPath() {
71
+ fixture.given().noPriorActivity()
72
+ .when().command(new RegisterCustomerCommand("Martin", "test@test.de"))
73
+ .then().success()
74
+ .events(new CustomerRegistered("Martin", "test@test.de"));
75
+ }
76
+
77
+ @Test
78
+ @DisplayName("given customer registered, when register customer with same email, then already registered")
79
+ void alreadyRegistered() {
80
+ fixture.given().event(new CustomerRegistered("Martin", "test@test.de"))
81
+ .when().command(new RegisterCustomerCommand("Max", "test@test.de"))
82
+ .then().exception(IllegalStateException.class, "Email already registered");
83
+ }
84
+ }
85
+ ```
86
+
87
+ **Method name note**: in 5.0.1 the fluent builder method was `annotatedCommandHandlingComponent`; it was
88
+ **renamed** to `autodetectedCommandHandlingComponent` in 5.1.x. Use `javap` against the project's actual
89
+ resolved jar before trusting either name in a new project — don't assume the older name still compiles.
90
+
91
+ `AxonTestFixture.Customization::disableAxonServer` (seen in older AF4-adjacent examples) does **not**
92
+ exist as a method in 5.1.1's `AxonTestFixture.Customization` record — the plain `AxonTestFixture.with(configurer)`
93
+ overload (no customization argument) already runs fully in-memory with no Axon Server connection. Only
94
+ reach for the `UnaryOperator<Customization>` overload (`.asIntegrationTest()`, `.registerFieldFilter(...)`,
95
+ `.registerIgnoredField(...)`) when you actually need one of those specific behaviors.
96
+
97
+ ## 2. Fluent API cheat sheet
98
+
99
+ ```java
100
+ fixture.given().noPriorActivity() // no prior events
101
+ fixture.given().event(someEvent) // one prior event
102
+ fixture.given().events(e1, e2, ...) // several prior events
103
+
104
+ fixture.when().command(someCommand)
105
+ fixture.when().event(someEvent) // dispatch via when() also supported
106
+
107
+ .then().success() // command handler returned normally
108
+ .then().events(expectedEvent1, expectedEvent2) // exact events emitted (equality via all-fields match)
109
+ .then().exception(IllegalStateException.class) // exception type only
110
+ .then().exception(IllegalStateException.class, "Email already registered") // type + exact message
111
+ .then().noEvents() // handler ran, nothing emitted
112
+ ```
113
+
114
+ `given()`/`when()`/`then()` all live on `AxonTestFixture` (`fixture.given()`, `fixture.when()`), chained
115
+ onto the previous phase's return value (`.given()....when()....then()...`), not called repeatedly on
116
+ `fixture` mid-chain.
117
+
118
+ ## 3. `@EventCriteriaBuilder` over bare `tagKey`
119
+
120
+ **Do not** rely on `@EventSourced(tagKey = "...")` (or the plain `@EventSourcedEntity(tagKey = "...")`)
121
+ for anything beyond the most trivial single-tag case — it silently produces an entity that never loads
122
+ any prior events if the tag key doesn't happen to match by coincidence, and there is no compiler or
123
+ runtime error to catch the mismatch; you only notice when a "should already exist" test unexpectedly
124
+ passes as if the entity were fresh. Prefer an explicit `@EventCriteriaBuilder` static method:
125
+
126
+ ```java
127
+ @EventCriteriaBuilder
128
+ private static EventCriteria resolveCriteria(String email, MessageTypeResolver messageTypeResolver) {
129
+ return EventCriteria.havingTags(Tag.of(EventTags.EMAIL, email))
130
+ .andBeingOneOfTypes(messageTypeResolver, CustomerRegistered.class);
131
+ }
132
+ ```
133
+
134
+ Rules (verified from `AnnotationBasedEventCriteriaResolver`, package `org.axonframework.eventsourcing.annotation`):
135
+ - Must be `static`, must return `EventCriteria` (package `org.axonframework.messaging.eventstreaming`, **not**
136
+ `org.axonframework.eventsourcing.eventstore` — that package does not contain `EventCriteria`/`Tag` in 5.1.1).
137
+ - First parameter is always the entity's `@InjectEntity`/`@TargetEntityId` identifier type (here `String`).
138
+ - Any additional parameters are resolved as components from the `Configuration` — `MessageTypeResolver` is
139
+ a valid injectable component and is the type-safe way to restrict the criteria to specific event classes:
140
+ `andBeingOneOfTypes(MessageTypeResolver, Class<?>...)`. This avoids hand-typing `"Namespace.Name"` strings
141
+ that can silently drift out of sync with the `@Event(namespace=..., name=...)` annotation on the event
142
+ record — prefer it over the string-literal `andBeingOneOfTypes(String...)` overload wherever a
143
+ `MessageTypeResolver` is easily injectable (which it always is, inside an `@EventCriteriaBuilder` method).
144
+ - Only one `@EventCriteriaBuilder` method may exist per distinct identifier parameter type on a given entity.
145
+
146
+ ## 4. Minimal working `pom.xml` diff
147
+
148
+ ```xml
149
+ <dependency>
150
+ <groupId>org.junit.jupiter</groupId>
151
+ <artifactId>junit-jupiter</artifactId>
152
+ <scope>test</scope>
153
+ </dependency>
154
+ <dependency> <!-- add this -->
155
+ <groupId>org.axonframework</groupId>
156
+ <artifactId>axon-test</artifactId>
157
+ <scope>test</scope>
158
+ </dependency>
159
+ ```
160
+
161
+ No other pom changes are required — version resolution comes from the BOM already imported for the
162
+ main `org.axonframework`/`io.axoniq.framework` dependencies.
@@ -0,0 +1,56 @@
1
+ # Feature Flag Patterns
2
+
3
+ This project's one supported approach: `@ConditionalOnProperty` on the handler and REST controller
4
+ (**not** on the entity — verified against the `RegisterCustomer`, `AllCustomers`, and `NotifyCustomer`
5
+ slices, none of which put the annotation on their entity/projector-internals class). Examples use a
6
+ generic `Ordering` bounded context.
7
+
8
+ ## Annotation on slice components
9
+
10
+ ```java
11
+ // Handler
12
+ @ConditionalOnProperty(prefix = "slices.ordering.write", name = "placeorder.enabled")
13
+ @Component
14
+ public class PlaceOrderCommandHandler { ... }
15
+
16
+ // REST controller (if applicable)
17
+ @ConditionalOnProperty(prefix = "slices.ordering.write", name = "placeorder.enabled")
18
+ @RestController
19
+ public class PlaceOrderRestController { ... }
20
+ ```
21
+
22
+ ## `application.properties` (main — enable by default)
23
+
24
+ ```properties
25
+ slices.ordering.write.placeorder.enabled=true
26
+ slices.ordering.read.getorders.enabled=true
27
+ slices.ordering.automation.notifycustomeronorder.enabled=true
28
+ ```
29
+
30
+ ## `application.properties` (test — disable by default)
31
+
32
+ ```properties
33
+ slices.ordering.write.placeorder.enabled=false
34
+ slices.ordering.read.getorders.enabled=false
35
+ slices.ordering.automation.notifycustomeronorder.enabled=false
36
+ ```
37
+
38
+ This only matters for tests that boot a Spring context (`@SpringBootTest`). The `AxonTestFixture`
39
+ unit-test pattern in [axon-test-fixture-patterns.md](axon-test-fixture-patterns.md) never boots Spring,
40
+ so these properties don't affect it either way.
41
+
42
+ ## `META-INF/additional-spring-configuration-metadata.json`
43
+
44
+ Register each property so IDEs auto-complete and validate it:
45
+
46
+ ```json
47
+ {
48
+ "properties": [
49
+ {
50
+ "name": "slices.ordering.write.placeorder.enabled",
51
+ "type": "java.lang.Boolean",
52
+ "description": "Enable/disable the PlaceOrder write slice in the Ordering bounded context."
53
+ }
54
+ ]
55
+ }
56
+ ```