Install
$ agentstack add skill-proophboard-skills-write-slice ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Axon Framework 5 — Write Slice
Relationship to prooph board Event Modeling
This skill implements the write slice (blue stripe) from a prooph board Event Modeling board. It consumes:
- The slice's
## Business Rulessection — used as input fordecide()logic - The slice's
## Scenarios (GWTs)section (written with theslice-scenariosskill) — mapped to test methods - The slice's optional
## Implementation Guidelines— technical requirements that extend the standard pattern
Step 0: Discover Target Project Conventions
Before writing any code, read the target project's context file (e.g., CLAUDE.md, AGENTS.md, .cursorrules) and explore at least one existing write slice. Conventions vary. Look for:
- File naming (
FeatureName.Slice.ktvs separate files) - Section markers (Domain / Application / Presentation comment blocks)
- Visibility modifiers on State, Command, handler, REST classes
- Event definitions (value objects vs primitives, marker interfaces,
@EventTag) - Metadata handling — how correlation IDs are attached to commands and events. Refer to
[references/kotlin-extensions.md](references/kotlin-extensions.md) for AxonMetadata helpers that avoid name collisions with other Metadata types in the project.
- Imports and package structure
Identify the established convention for each of the following. If unclear, see steps below:
- Command handler registration style (Step 3b)
- REST API exposure (Step 4)
- Feature flag approach (Step 5)
Step 1: Understand the Input
Input can arrive in many forms. Extract these elements regardless of format:
| Element | What to extract | |--------------------------|----------------------------------------------------------------------| | Command | Name, properties, which property identifies the consistency boundary | | Events | Names, properties, which events this command produces | | Business rules | Preconditions, invariants, idempotency behavior | | State needed | What prior events must be replayed to evaluate rules | | Consistency boundary | Single tag (one stream) or multi-tag (DCB across streams) |
Input: Specification / Natural Language
Extract command name, events, and business rules directly from the description.
Input: Existing Tests
Analyze test file to understand expected behavior: commands sent, events asserted, failure cases.
Input: Event Modeling Artifact
The write slice (blue stripe) shows: Command on left, Events on right, State (read model) below.
Optionally, the slice details may contain:
## Business Rules— invariants and preconditions fordecide()implementation## Scenarios (GWTs)— Given-When-Then acceptance criteria using:::elementblocks
When GWT scenarios are present, each numbered scenario maps 1:1 to a test method. Properties in :::element blocks are only rule-relevant — fill remaining constructor params with test fixture values.
If the slice details contain ## Implementation Guidelines, follow them — they describe specific technical requirements that go beyond the standard slice pattern.
Input: Axon Framework 4 Code
Read the AF4 source: command class, aggregate class, events, domain rules, REST API. See [references/af4-input-mapping.md](references/af4-input-mapping.md) for concept-by-concept translation.
If requirements are unclear, ask the user before proceeding.
Step 2: Choose the AF5 Pattern
Spring Boot — entity and handler auto-discovered by Spring:
@EventSourced(tagKey = "tagName")on entity (single tag) or@EventSourcedEntity+@EventCriteriaBuilder(multi-tag)- Handler class is
@Component - Tested with
@AxonSpringBootTest(org.axonframework.extension.springboot.test.AxonSpringBootTest) - Default choice when the project uses Spring Boot
Explicit Registration — entity and handler registered manually via @Configuration:
@EventSourcedEntityon entity@EventCriteriaBuildercompanion method on entity@Configurationclass withEntityModule+CommandHandlingModulebeans- Tested with non-Spring Boot unit test (
axonTestFixture+configSlice) - Use when: user explicitly asks for unit tests without Spring context
Both patterns support single-tag and multi-tag (DCB). The difference is registration mechanism, not tag cardinality.
See [references/af5-write-slice-patterns.md](references/af5-write-slice-patterns.md) for complete patterns.
Step 3: Implement the Domain (decide + evolve)
Create FeatureName.Slice.kt with the Domain section:
////////////////////////////////////////////
////////// Domain
///////////////////////////////////////////
// 1. Command data class (public)
// 2. State data class (private) + initialState
// 3. decide(command, state): List -- pure function
// 4. evolve(state, event): State -- pure function
Key rules for domain components:
Command: Plain data class. Public. Add @get:JvmName on properties whose names match their type pattern. Annotate with @Command(namespace = "", name = "", version = "1.0.0") — import from org.axonframework.messaging.commandhandling.annotation.Command.
State: Private. Immutable data class. Contains ONLY fields needed by decide(). Companion initialState val.
decide(): Private standalone function. Takes (command, state), returns event(s). No side effects. Enforce rules here: throw IllegalStateException for violations, return emptyList() for idempotent no-ops.
evolve(): Private standalone function. Takes (state, event), returns new State. Uses when (event: SealedType) over the sealed interface.
⚠️ ABSOLUTE RULE: NEVER use else -> in evolve(). Every sealed subtype MUST have an explicit branch — even no-ops (is SomeEvent -> state). Before writing evolve():
- Find and read the bounded context's sealed event interface (e.g.,
{Context}Event) - List ALL concrete subtypes that implement it
- Write an explicit
isbranch for EVERY subtype — mutating branches withstate.copy(...), no-op branches
with -> state and a comment explaining why
This ensures compile-time safety: adding a new event to the sealed interface breaks every slice using that type, forcing a deliberate update.
@EventSourcingHandler is ONLY added for events that actually mutate state — no-op branches (-> state) must NOT have a corresponding handler. When any branch mutates state, add a test for that transition (see Step 6).
Exception: else -> IS allowed for non-sealed interfaces. Cross-module slices subscribing to events from multiple bounded contexts via a non-sealed root event interface cannot use exhaustive when. In this case, else -> state is the correct fallback. However, every subscribed event type must still be reviewed manually when new events are added to any of the participating modules.
Value Objects with Kotlin value class
When command or event properties represent constrained domain concepts, prefer wrapping them in @JvmInline value class types with validation in the init block:
@JvmInline
value class Quantity(val raw: Int) {
init { require(raw >= 0) { "Quantity must be non-negative, got $raw" } }
}
When to introduce a value class:
- The property has validation constraints (range, format, non-blank)
- The same concept appears in command, event, and state — avoids duplicating validation
- Using primitives would allow invalid states
Add domain operations (next(), isLast, plus(), etc.) to value classes so that decide() works entirely with value objects and never unwraps to .raw. Reserve .raw for REST layer, cross-context mapping, serialization.
Step 3b: Command Handler Registration
Check the target project's convention first — scan existing slices for @CommandHandler, @InjectEntity, and CommandHandlingModule to determine the established pattern.
If no clear convention exists, ask the user: > Which command-handler registration style does this project use? > - Separate @Component class + @CommandHandler method + @InjectEntity (Spring Boot auto-discovery, default) > - Handler method colocated on the @EventSourced entity > - Explicit registration via CommandHandlingModule in a @Configuration (enables non-Spring unit tests)
See [references/command-handler-styles.md](references/command-handler-styles.md) for full examples of each style.
The Application section of the slice file hosts the entity and handler:
////////////////////////////////////////////
////////// Application
///////////////////////////////////////////
// 5. @EventSourced entity class (wraps State, has @EventSourcingHandler methods)
// 6. Command handler (style depends on Step 3b)
// 7. @Configuration if Explicit Registration pattern
In @EventCriteriaBuilder methods, .andBeingOneOfTypes(...) MUST use "Namespace.Name" strings (e.g., "Ordering.OrderPlaced"), NEVER ClassName::class.java.getName(). The type name is the @Event annotation's namespace + "." + name.
Step 4: REST API Exposure (Optional)
Check the target project's convention first — does it expose commands via REST (@RestController presence)?
If no convention is established, ask the user: > How will this command be triggered? > - REST API — exposed via HTTP endpoint (add Presentation section + REST API test) > - Automation only — dispatched internally by an event handler (no REST, no Presentation section) > - Both — exposed via REST API and also dispatched by automations
If REST is chosen, add the Presentation section:
////////////////////////////////////////////
////////// Presentation (only if REST API trigger)
///////////////////////////////////////////
// 8. @RestController (Body DTO, sends command via CommandGateway)
See [references/rest-api-patterns.md](references/rest-api-patterns.md) for REST controller and RestAssured test examples.
Step 4a: Ensure Events Exist
Before implementing the slice, check the bounded context's events package. If events don't exist yet, create them first — the slice file depends on them.
Event Hierarchy
Recommended hierarchy (check what the target project already uses):
DomainEvent ← root marker (project-defined, e.g. in sdk/shared module)
└─ {Context}Event ← sealed interface per bounded context ({context}/events/)
└─ {ConcreteEvent} ← data class ({context}/events/)
DomainEvent is a simple project-defined marker interface (not AF5 itself). Encourage its use in new projects.
Context Event Interface (if it doesn't exist)
// File: {context}/events/{Context}Event.kt
sealed interface {Context}Event : DomainEvent {
@get:EventTag(EventTags.{TAG_CONSTANT})
val {tagProperty}: {IdType}
}
The @get:EventTag on the sealed interface means all implementing events automatically inherit the tag. Also ensure the tag constant exists in the project's EventTags object.
Concrete Event Classes
// File: {context}/events/{EventName}.kt
@Event(namespace = "{Context}", name = "{EventName}", version = "1.0.0")
data class {EventName}(
override val {tagProperty}: {IdType}, // inherited from sealed interface
val property1: ValueType1
) : {Context}Event
Key rules:
@Event(namespace, name, version)— import fromorg.axonframework.messaging.eventhandling.annotation.Eventnamespace= bounded context name,name= class name,version="1.0.0"for new events- Use value object types for properties; the tag property is
override val
Additional Tags on Events (DCB)
When an event participates in a Dynamic Consistency Boundary spanning multiple streams, add extra @EventTag annotations on the concrete event's properties for cross-stream filtering.
Step 5: Feature Flags (Optional)
Check the target project's convention first — scan existing slices for @ConditionalOnProperty, @Profile, or custom feature-flag integrations.
If no clear convention exists, ask the user: > How should slice-level feature flags be managed? > - @ConditionalOnProperty (Spring Boot default) > - Custom flag library (FF4J, Unleash, LaunchDarkly, etc.) > - No feature flags — ship all slices unconditionally
See [references/feature-flag-patterns.md](references/feature-flag-patterns.md) for the full @ConditionalOnProperty example (entity, handler, REST controller, application.yaml, additional-spring-configuration-metadata.json) and alternatives.
Step 6: Implement Tests
The AxonTestFixture Kotlin DSL (Given { } When { } Then { }) must be copied into the project's test sources — it is not yet published as a standalone library. See [references/axon-test-fixture-kotlin-dsl.md](references/axon-test-fixture-kotlin-dsl.md) for the full source and instructions.
For AxonMetadata — use the typealias from [references/kotlin-extensions.md](references/kotlin-extensions.md) to avoid name collisions.
6a. Slice Tests (domain logic via Given-When-Then)
Two approaches:
- Spring Boot test — uses
@AxonSpringBootTest(org.axonframework.extension.springboot.test.AxonSpringBootTest)
with AxonTestFixture injected via constructor. Check if the project defines a meta-annotation that wraps @AxonSpringBootTest with shared config (@ActiveProfiles, @Import for Testcontainers, etc.); if one exists, use it. Otherwise use @AxonSpringBootTest directly.
- Non-Spring Boot test — uses
axonTestFixture(configSlice { ... }). No Spring context needed.
See [references/af5-write-slice-patterns.md](references/af5-write-slice-patterns.md) for complete test examples.
Cover these scenarios:
- Happy path: no prior state, command produces expected events
- Idempotency: duplicate command produces no events
- Rule violations: invalid state returns
CommandHandlerResult.Failure - State transitions: prior events change behavior
- All mutating evolve branches: for every event that mutates state in
evolve(), add at least one test
⚠️ CRITICAL: implement ALL GWT scenarios from the slice details, not just the command's own events.
Mapping Event Model GWT Scenarios to Tests
| GWT Element | Test Code | |---|---| | Scenario name (e.g., ### 1. place first order) | Test method name: ` given no prior order, when place, then placed | | NOTHING in Given | noPriorActivity() | | :::element event in Given | event(EventClass(...), metadata) in Given { } block | | :::element command in When | command(CommandClass(...), metadata) in When { } block | | :::element event in Then | events(EventClass(...)) + resultMessagePayload(Success) | | :::element hotspot in Then | resultMessagePayload(Failure("message")) | | NOTHING in Then | noEvents() + resultMessagePayload(Success)` — idempotent |
Metadata is MANDATORY on every event() and command() call. Always define a metadata object at class level and pass it to every call:
private val metadata = AxonMetadata.with("correlationId", UUID.randomUUID().toString())
.and("tenantId", UUID.randomUUID().toString())
Property mapping: GWT properties are rule-relevant only. Fill remaining constructor params with test fixture values. When the same property value appears in Given and When, use the same variable to make the relationship explicit.
6b. REST API Tests (only if REST API chosen in Step 4)
Tests the REST controller in isolation — mocked CommandGateway, no Axon Server, no event store.
See [references/rest-api-patterns.md](references/rest-api-patterns.md) for full RestAssured + @WebMvcTest examples.
References
- [AF5 Write Slice Patterns](references/af5-write-sl
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: proophboard
- Source: proophboard/skills
- License: MIT
- Homepage: https://skills.prooph-board.com/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.