axon5kotlin-write-slice
v1.0.0Axon Framework 5 Kotlin Write Slice Skill — generate Kotlin write slices (Command → decide → Events → evolve → State) from prooph board Event Modeling slices. Covers Spring Boot and Explicit Registration patterns, single-tag and multi-tag DCB, value objects, feature flags, and Given-When-Then tests with AxonTestFixture. Supports migrating AF4 aggregates to AF5.
Axon Framework 5 — Write Slice
Implement Event Sourcing write slices using Axon Framework 5 and Vertical Slice Architecture.
Overview
This skill teaches AI agents how to implement write slices in Axon Framework 5 projects using Vertical Slice Architecture and Event Sourcing. A write slice follows the pattern: Command → decide → Events → evolve → State.
Write slices represent the blue stripe in Event Modeling — commands that change persistent state and the events they produce.
What This Skill Covers
- Event Sourced Entities — Using
@EventSourcedannotation for single-tag or@EventSourcedEntitywith@EventCriteriaBuilderfor multi-tag (Dynamic Consistency Boundaries) - Command Handlers — Implementing command handling logic with business rule validation
- Event Evolution — Managing state through event replay and evolution
- REST API Exposure — Optional Spring MVC controllers for HTTP command submission
- Axon Framework 5 Patterns — Proper use of
CommandGateway, event sourcing, and consistency boundaries - Migration from AF4 — Converting Axon Framework 4 aggregate patterns to AF5 entity-based architecture
Why This Skill
- Vertical Slice Architecture — Each write slice is self-contained with its own entity, handler, and tests
- Event Sourcing Best Practices — Teaches proper event-driven state management and consistency boundaries
- Testable by Design — Includes both Spring Boot integration tests and unit test patterns
- DCB Support — Handles both single-tag streams and multi-tag Dynamic Consistency Boundaries
When to Use
| ✅ Use This Skill | ❌ Don't Use It |
|---|---|
| Implementing a new write slice / command handler in an AF5 project | Building read models for queries (use read-slice instead) |
| Migrating/porting write slices from Axon Framework 4 (Java or Kotlin) to AF5 | Implementing event handlers that dispatch commands (use automation-slice instead) |
Usage
Once installed, your AI agent will know how to:
- Discover Target Project Conventions — Read context files (
CLAUDE.md,AGENTS.md) and explore existing slices - Understand Input Formats — Parse specifications, Event Modeling artifacts, GWT scenarios, or AF4 code
- Choose AF5 Pattern — Select Spring Boot auto-discovery vs explicit registration based on project conventions
- Implement the Write Slice — Generate entity, handler, commands, and events
- Add Feature Flags (Optional) — Apply project-specific feature flag patterns
- Write Tests — Create Spring Boot integration tests or unit tests using
axonTestFixture
Example Write Slice Structure
// Command
@Command(namespace = "Orders", name = "CreateOrder", version = "1.0.0")
data class CreateOrder(
val tenantId: TenantId,
val customerId: CustomerId,
val items: List<OrderItem>,
val shippingAddress: Address
)
// Events
@Event(namespace = "Orders", name = "OrderCreated", version = "1.0.0")
data class OrderCreated(
val orderId: OrderId,
val tenantId: TenantId,
val customerId: CustomerId,
val items: List<OrderItem>,
val totalAmount: Money,
val status: OrderStatus
) : OrdersEvent
@Event(namespace = "Orders", name = "OrderCancelled", version = "1.0.0")
data class OrderCancelled(
val orderId: OrderId,
val reason: String,
val cancelledAt: Instant
) : OrdersEvent
// Event-Sourced Entity
@EventSourced(tagKey = "orderId")
internal data class Order private constructor(
val orderId: OrderId,
val tenantId: TenantId,
val customerId: CustomerId,
val items: List<OrderItem>,
val totalAmount: Money,
val status: OrderStatus
) {
companion object {
fun create(command: CreateOrder): Pair<EventStream, Order> {
val event = OrderCreated(
orderId = OrderId.next(),
tenantId = command.tenantId,
customerId = command.customerId,
items = command.items,
totalAmount = command.calculateTotal(),
status = OrderStatus.CREATED
)
return EventStream.of(event) to Order.fromEvent(event)
}
}
fun cancel(reason: String): EventStream {
require(status == OrderStatus.CREATED) { "Can only cancel created orders" }
return EventStream.of(
OrderCancelled(orderId, reason, Instant.now())
)
}
private fun apply(event: OrderCreated) = copy(
orderId = event.orderId,
tenantId = event.tenantId,
customerId = event.customerId,
items = event.items,
totalAmount = event.totalAmount,
status = event.status
)
private fun apply(event: OrderCancelled) = copy(
status = OrderStatus.CANCELLED
)
}
// Command Handler
@Component
private class CreateOrderHandler {
@CommandHandler
suspend fun handle(command: CreateOrder, eventStore: EventStore): OrderId {
val (eventStream, entity) = Order.create(command)
val orderId = entity.orderId
eventStore.append(orderId.raw, eventStream)
return orderId
}
}
REST Controller (Optional)
@RestController
@RequestMapping("/api/orders")
private class OrdersController(
private val commandGateway: CommandGateway
) {
@PostMapping
suspend fun createOrder(@RequestBody command: CreateOrder): ResponseEntity<OrderId> {
val result = commandGateway.send(command).resultMessage.join()
return ResponseEntity.status(HttpStatus.CREATED).body(result as OrderId)
}
}
Prerequisites
- Familiarity with Axon Framework 5 messaging concepts (commands, events, event sourcing)
- Understanding of Vertical Slice Architecture principles
- Knowledge of Event Modeling (especially write slice/blue stripe patterns)
- Basic Spring Boot and Kotlin experience
- Understanding of CQRS and Event Sourcing patterns
Related Skills
| Skill | Purpose |
|---|---|
| read-slice | Build query-side read models and projections |
| automation-slice | Implement event-to-command automations |
| slice-scenarios | Write Given-When-Then scenarios for slice documentation |
| event-modeling | Core Event Modeling rules and element types |
Install these alongside write-slice for complete vertical slice implementations.
| name | axon5kotlin-write-slice |
|---|---|
| description | > |
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 forAxonMetadatahelpers that avoid name
collisions with otherMetadatatypes 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 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 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<Event> -- 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 = "<BoundedContext>", name = "<CommandName>", 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-> stateand 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, andCommandHandlingModule to determine the established pattern.
If no clear convention exists, ask the user:
Which command-handler registration style does this project use?
- Separate
@Componentclass +@CommandHandlermethod +@InjectEntity(Spring Boot auto-discovery, default)- Handler method colocated on the
@EventSourcedentity- Explicit registration via
CommandHandlingModulein a@Configuration(enables non-Spring unit tests)
See 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 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 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
for the full source and instructions.
For AxonMetadata — use the typealias from 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)
withAxonTestFixtureinjected via constructor. Check if the project defines a meta-annotation that wraps@AxonSpringBootTestwith shared config (@ActiveProfiles,@Importfor Testcontainers, etc.); if one exists,
use it. Otherwise use@AxonSpringBootTestdirectly. - Non-Spring Boot test — uses
axonTestFixture(configSlice { ... }). No Spring context needed.
See 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 for full RestAssured + @WebMvcTest examples.
References
- AF5 Write Slice Patterns — Complete examples (Spring Boot + Explicit
Registration, single-tag and multi-tag DCB) with full code and testing - AF4 Input Mapping — When input is Axon Framework 4 code: concept-by-concept
translation guide - Command Handler Styles — All three handler-registration styles with examples
- REST API Patterns — REST controller and RestAssured test examples
- Feature Flag Patterns —
@ConditionalOnPropertyand alternatives - Kotlin Extensions —
AxonMetadatatypealias and helper functions - AxonTestFixture Kotlin DSL — Given-When-Then DSL source to copy into the project