axon5kotlin-automation-slice
v1.0.0Axon Framework 5 Kotlin Automation Slice Skill — generate Kotlin automation slices (Event → CommandDispatcher) from prooph board Event Modeling slices. Covers stateless and read-model-backed automations, CommandDispatcher usage, SequencingPolicy, and Spring Boot integration tests with AxonTestFixture.
Axon Framework 5 — Automation Slice
Implement automation slices (Event to Command) using Axon Framework 5 and Vertical Slice Architecture.
Overview
This skill teaches AI agents how to implement automation slices in Axon Framework 5 projects using Vertical Slice Architecture. An automation is an event handler that reacts to an event by dispatching a command through the CommandDispatcher.
Automations represent the orange stripe in Event Modeling — automated actors that respond to events without human intervention.
What This Skill Covers
- Stateless Automations — Direct event-to-command mapping with no stored state needed
- Automations with Read Model — When the automation needs to look up data from a private read model (JPA entity + repository) before dispatching commands
- Axon Framework 5 Patterns — Proper use of
CommandDispatcher,@EventHandler, metadata propagation, andCompletableFuturereturn types - Migration from AF4 — Converting Axon Framework 4
@EventHandlerandCommandGatewaypatterns to AF5
Why This Skill
- Vertical Slice Architecture — Each automation is a self-contained slice with its own tests and configuration
- AF5 Best Practices — Uses AF5's preferred
CommandDispatcher(ProcessingContext-scoped) instead of singletonCommandGateway - Event-Driven Design — Implements proper event-to-command flows with correlation ID propagation
- Testable by Design — Includes Spring Boot integration test patterns using
AxonTestFixtureKotlin DSL
When to Use
| ✅ Use This Skill | ❌ Don't Use It |
|---|---|
| Implementing a new automation/event-to-command reactor in an AF5 project | Implementing command handlers (use write-slice instead) |
| Migrating/porting automations from Axon Framework 4 (Java or Kotlin) to AF5 | Building read models for queries (use read-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 Event Modeling artifacts, GWT scenarios, or natural language descriptions
- Ensure Events Exist — Create missing event classes following the project's hierarchy conventions
- Implement the Automation — Generate processor classes with proper
CommandDispatcherusage - Add Feature Flags (Optional) — Apply project-specific feature flag patterns
- Write Tests — Create Spring Boot integration tests using
AxonTestFixtureKotlin DSL
Stateless Automation Example
@Component
private class OrderShippedNotificationProcessor(
private val notificationStrategy: NotificationStrategy
) {
@EventHandler
fun react(
event: OrderShipped,
@MetadataValue("tenantId") tenantId: String,
commandDispatcher: CommandDispatcher
) {
if (event.customerWantsNotification) {
val command = SendNotification(
customerId = event.customerId,
message = notificationStrategy.buildMessage(event)
)
val metadata = AxonMetadata.with("tenantId", tenantId)
commandDispatcher.send(command, metadata)
}
}
}
Automation with Read Model Example
When the automation needs to iterate over entities or look up data not in the trigger event:
@Component
@SequencingPolicy(type = MetadataSequencingPolicy::class, parameters = ["tenantId"])
private class MonthlyReportProcessor(
private val repository: ReportSubscriptionRepository
) {
@EventHandler
fun react(
event: MonthEnded,
@MetadataValue("tenantId") tenantId: String,
commandDispatcher: CommandDispatcher
): CompletableFuture<Void> {
val futures = repository.findAllByTenantId(tenantId)
.map { subscription ->
val command = GenerateReport(subscription.reportType, event.month)
commandDispatcher.send(command, AxonMetadata.with("tenantId", tenantId))
.resultMessage
}
return CompletableFuture.allOf(*futures.toTypedArray())
}
@EventHandler
fun on(event: ReportSubscribed, @MetadataValue("tenantId") tenantId: String) {
repository.save(ReportSubscription(tenantId, event.subscriptionId, event.reportType))
}
}
Prerequisites
- Familiarity with Axon Framework 5 messaging concepts (events, commands, command dispatcher)
- Understanding of Vertical Slice Architecture principles
- Knowledge of Event Modeling (especially automation/orange stripe patterns)
- Basic Spring Boot and Kotlin experience
Related Skills
| Skill | Purpose |
|---|---|
| write-slice | Implement command handlers and aggregates |
| read-slice | Build query-side read models and projections |
| slice-scenarios | Write Given-When-Then scenarios for slice documentation |
| event-modeling | Core Event Modeling rules and element types |
Install these alongside automation-slice for complete vertical slice implementations.
| name | axon5kotlin-automation-slice |
|---|---|
| description | > |
Axon Framework 5 — Automation Slice
An automation reacts to an event by dispatching a command. In Event Modeling: the orange stripe.
There are two kinds:
- Stateless: Direct event-to-command mapping — no stored state needed
- With read model: Needs a private read model (JPA entity + repository) to look up data required for command
construction (e.g., iterate over all entities matching a category)
Relationship to prooph board Event Modeling
This skill implements the automation slice (orange stripe) from a prooph board Event Modeling board.
It consumes:
- The slice's
## Scenarios (GWTs)section (written with theslice-scenariosskill) — GWT format for automations
isGiven (events) → Then (command | hotspot | NOTHING). Events in Given include read-model-building events
first, trigger event last. Command in Then is the dispatched command. Hotspot or NOTHING means no command. - The slice's optional
## Implementation Guidelines— technical requirements beyond the standard pattern
Step 0: Discover Target Project Conventions
Read the target project's context file (e.g., CLAUDE.md, AGENTS.md, .cursorrules) and explore existing slices.
Look for:
- File splitting conventions (single
.Slice.ktvs separate files per class) - Visibility modifiers (
privateon processor/configuration classes) - Metadata handling — how correlation IDs are attached (see
references/kotlin-extensions.md forAxonMetadatahelpers) - Feature flag patterns (Step 4 — optional)
- YAML config files (
application.yaml,application-test.yaml) - Spring Boot test annotation: check if the project defines a meta-annotation for
@AxonSpringBootTest
Step 1: Understand the Input
Extract these elements regardless of input format:
| Element | What to extract |
|---|---|
| Trigger event | Which event triggers the automation, and which condition filters it |
| Target command | Which command to dispatch, with what properties |
| Mapping logic | How event properties map to command properties |
| Strategy/calculator | Any injectable strategy for deriving command properties from event data |
| Metadata | Which metadata keys to propagate from event to command |
| Read model needed? | Does the automation need to look up data not in the event itself? |
If the Event Modeling artifact includes slice details with ## Scenarios (GWTs), use them to derive test cases.
If the slice details contain ## Implementation Guidelines, follow them.
Stateless vs With Read Model Decision
Choose with read model when:
- The automation needs data that is NOT in the trigger event (e.g., "find all entities of type X")
- The automation must iterate over a collection of entities to dispatch multiple commands
- The automation combines data from multiple event types (one builds the read model, another triggers commands)
Choose stateless when:
- All data needed for the command is in the trigger event + metadata
- The mapping is direct or uses a pure calculation/strategy
Input: Axon Framework 4 Code
AF4 AF5
───────────────────────────────── ────────────────────────────────────
@ProcessingGroup("name") (not needed — Spring Boot auto-config)
@DisallowReplay (not needed in AF5)
@Component @Component
@EventHandler @EventHandler (different package)
commandGateway.sendAndWait(cmd, m) commandDispatcher.send(cmd, metadata)
CommandGateway (constructor-inject) CommandDispatcher (method parameter)
@MetaDataValue("key") @MetadataValue("key")
Function<A, B> interface fun interface Name : (A) -> B
If requirements are unclear, ask the user before proceeding.
Step 2: Ensure Events Exist
Before implementing the automation, verify that all events the processor will handle exist in the codebase.
If they don't, create them first.
Event Hierarchy
Recommended hierarchy (check what the target project already uses):
DomainEvent ← root marker (project-defined)
└─ {Context}Event ← sealed interface per bounded context ({context}/events/)
└─ {ConcreteEvent} ← data class ({context}/events/)
Context Event Interface (if it doesn't exist)
sealed interface {Context}Event : DomainEvent {
@get:EventTag(EventTags.{TAG_CONSTANT})
val {tagProperty}: {IdType}
}
Concrete Event Classes
@Event(namespace = "{Context}", name = "{EventName}", version = "1.0.0")
data class {EventName}(
override val {tagProperty}: {IdType},
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
Step 3: Implement the Automation
CommandDispatcher vs CommandGateway
Always use CommandDispatcher to dispatch commands from within @EventHandler methods:
CommandDispatcheris AF5's preferred way to send commands from within message handlers- It is ProcessingContext-scoped — inject it as a method parameter on the
@EventHandler, NOT as a
constructor parameter CommandGatewayis a singleton intended for external callers (REST controllers, etc.)
// CORRECT: CommandDispatcher as method parameter
@EventHandler
fun react(event: {TriggerEvent}, commandDispatcher: CommandDispatcher) {
commandDispatcher.send(command, metadata)
}
// WRONG: CommandGateway as constructor parameter
class MyProcessor(private val commandGateway: CommandGateway) {
@EventHandler
fun react(event: {TriggerEvent}) {
commandGateway.send(command, metadata) // Don't do this
}
}
Error Propagation with CompletableFuture
When the automation dispatches commands, return a CompletableFuture from the @EventHandler method so AF5 awaits
command completion. If a command fails, the event handler fails too, and the event processor will retry.
commandDispatcher.send(command, metadata)returns aCommandResultCommandResult.resultMessagereturnsCompletableFuture- For multiple commands: use
CompletableFuture.allOf()to await all
AF5's MessageStreamResolverUtils handles CompletableFuture return types via MessageStream.fromFuture().
Stateless Automation
An automation slice has up to 3 files (adapt to project conventions on splitting):
File 1: Strategy Interface (if needed)
A fun interface for injectable logic deriving command properties from event data:
fun interface {StrategyName} : ({InputType}) -> {OutputType}
Skip if the mapping from event to command is trivial/direct.
File 2: Configuration (if strategy exists)
@ConditionalOnProperty(...) // if using feature flags
@Configuration
private class {AutomationName}Configuration {
@Bean
fun {strategyName}(): {StrategyName} =
{StrategyName} { input -> /* default implementation */ }
}
File 3: Processor
@ConditionalOnProperty(...) // if using feature flags
@Component
private class {AutomationName}Processor(
private val strategy: {StrategyName} // if strategy exists
) {
@EventHandler
fun react(
event: {TriggerEvent},
@MetadataValue("tenantId") tenantId: String, // use the project's correlation key name
commandDispatcher: CommandDispatcher
) {
if ({condition}) {
val command = {TargetCommand}(...)
val metadata = AxonMetadata.with("tenantId", tenantId)
commandDispatcher.send(command, metadata)
}
}
}
Automation with Read Model
When the automation needs to look up data, create a dedicated read model within the same slice.
Slices are independent — never reuse another slice's read model.
Everything goes in a single .Slice.kt file:
JPA Entity (Read Model)
@Entity
@Table(
name = "{context}_automation_{readmodel_name}",
indexes = [Index(name = "idx_{context}_{readmodel_name}_{columns}", columnList = "tenantId, categoryId")]
)
internal data class {ReadModelName}(
val tenantId: String,
@Id
val primaryId: String,
val filterField: String
)
Key rules:
- Composite index on the columns used in
WHEREclause — filter at DB level, not client-side - Table name prefixed with context to avoid collisions
internalvisibility — only the processor in this slice should access it
Repository
@ConditionalOnProperty(...) // if using feature flags
@Repository
private interface {ReadModelName}Repository : JpaRepository<{ReadModelName}, String> {
fun findAllByTenantIdAndFilterField(tenantId: String, filterField: String): List<{ReadModelName}>
}
Key rules:
- DB-level filtering — use Spring Data derived queries that filter on all relevant columns, not
findAll()+
client-side filter privatevisibility- Same
@ConditionalOnPropertyas the processor (if using feature flags)
Processor
@ConditionalOnProperty(...) // if using feature flags
@Component
@SequencingPolicy(type = MetadataSequencingPolicy::class, parameters = ["tenantId"])
private class {AutomationName}Processor(
private val repository: {ReadModelName}Repository
) {
@EventHandler
fun react(
event: {TriggerEvent},
@MetadataValue("tenantId") tenantId: String,
commandDispatcher: CommandDispatcher
): CompletableFuture<Void> {
val futures = repository.findAllByTenantIdAndFilterField(tenantId, event.filterValue)
.map { entity -> dispatchCommand(entity, event, tenantId, commandDispatcher) }
return CompletableFuture.allOf(*futures.toTypedArray())
}
private fun dispatchCommand(
entity: {ReadModelName},
event: {TriggerEvent},
tenantId: String,
commandDispatcher: CommandDispatcher
): CompletableFuture<out Any?> {
val command = {TargetCommand}(
// map entity fields + event data to command properties
)
val metadata = AxonMetadata.with("tenantId", tenantId)
return commandDispatcher.send(command, metadata).resultMessage
}
@EventHandler
fun on(event: {BuildingEvent}, @MetadataValue("tenantId") tenantId: String) {
repository.save(
{ReadModelName}(
tenantId = tenantId,
primaryId = event.entityId.raw,
filterField = event.filterValue.raw
)
)
}
}
Key rules:
@SequencingPolicy(MetadataSequencingPolicy, "tenantId")— ensures events for the same tenant/correlation unit
are processed sequentially, preventing race conditions on the read modelCommandDispatcheras method parameter — ProcessingContext-scoped, not constructor-injectedCompletableFuture<Void>return —CompletableFuture.allOf()awaits all dispatched commands; if any fails,
the event handler fails and the processor retriescommandDispatcher.send(command, metadata).resultMessage— returnsCompletableFuturefor the command result- Two
@EventHandlermethods in one class: one builds the read model, the other reacts by dispatching commands - Repository is constructor-injected (Spring bean),
CommandDispatcheris method-injected (ProcessingContext)
Step 4: 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 (processor, repository, application.yaml, additional-spring-configuration-metadata.json) and alternatives.
Step 5: Implement Tests
Spring Boot integration test with AxonTestFixture Kotlin DSL.
Critical: Enable BOTH the automation AND its target write slice in the feature flag settings.
If the automation uses a strategy, override it with a deterministic @TestConfiguration bean.
The AxonTestFixture Kotlin DSL must be copied into the project's test sources.
See references/axon-test-fixture-kotlin-dsl.md.
For AxonMetadata — use the typealias from references/kotlin-extensions.md.
AF5 Imports
import org.axonframework.messaging.commandhandling.gateway.CommandDispatcher
import org.axonframework.messaging.core.annotation.MetadataValue
import org.axonframework.messaging.eventhandling.annotation.EventHandler
import org.axonframework.messaging.eventhandling.annotation.SequencingPolicy
import org.axonframework.messaging.eventhandling.sequencing.MetadataSequencingPolicy
AxonTestFixture API for Automations
Assert exact command dispatched (stateless automation — single command):
fixture.Scenario {
Given {
event({TriggerEvent}(...), metadata)
} Then {
await({ it.commands(expectedCommand) })
}
}
Assert multiple commands in any order (automation with read model — multiple commands):
fixture.Scenario {
Given {
event({BuildingEvent}(...), metadata) // builds read model
event({TriggerEvent}(...), metadata) // triggers commands
} Then {
await({
it.commandsSatisfy { commands ->
val relevantPayloads = commands.map { cmd -> cmd.payload() }
.filterIsInstance<{TargetCommand}>()
.filter { cmd -> cmd.entityId in testEntityIds }
assertThat(relevantPayloads).containsExactlyInAnyOrder(
{TargetCommand}(...),
{TargetCommand}(...)
)
}
})
}
}
Assert no commands (condition not met):
fixture.Scenario {
Given {
event({TriggerEvent}(... /* condition not met */), metadata)
} Then {
await({ it.noCommands() })
}
}
Testing Automations with Read Model — Important Notes
RecordingCommandBusaccumulates commands across test methods in the same Spring context. It is NOT reset
between tests when usingGiven { } Then { }(only theWhen { }phase resets it).Isolate tests using entity ID filtering: Generate unique IDs per test method, then filter assertions to only
check commands relevant to the current test's IDs:val testEntityIds = setOf(entity1, entity2) // ... in assertion: .filter { cmd -> cmd.entityId in testEntityIds }Use
commandsSatisfy+containsExactlyInAnyOrder: JPA/DB returns results in unpredictable order, so
commands may be dispatched in any order. Don't usecommands(cmd1, cmd2)which asserts strict ordering.Put all events in a single
Givenblock: For temporal ordering tests, interleave building events and trigger
events in a singleGivenblock and assert all expected commands at once.
Test Cases to Cover
Stateless automations:
- Happy path: Event matching condition → expected command dispatched
- Condition not met: Event not matching condition → no commands dispatched
Automations with read model:
- Happy path: Build read model entries + trigger event → commands for matching entries only
- Non-matching entries: Build entries of different types + trigger for one type → only matching type gets commands
- Temporal ordering: Build some entries, trigger, build more entries, trigger again → each trigger only affects
entries that existed at that point
Mapping Event Model GWT Scenarios to Tests
| GWT Element | Test Code |
|---|---|
:::element event in Given |
Given { event(EventClass(...), metadata) } |
| Multiple events in Given | Multiple event(...) calls — read-model-building events first, trigger event last |
:::element command in Then |
Then { await({ it.commands(expectedCommand) }) } |
:::element hotspot in Then |
Then { await({ it.noCommands() }) } — exception/failure |
NOTHING in Then |
Then { await({ it.noCommands() }) } — no reaction |
Properties in :::element blocks are rule-relevant only — fill remaining constructor params with test fixture values.
References
- Stateless Automation Test Example — Complete working test with
deterministic strategy override - Automation with Read Model Test Example — Complete
working test with private read model and multi-command assertions - Feature Flag Patterns —
@ConditionalOnPropertyand alternatives - Kotlin Extensions —
AxonMetadatatypealias and helper functions - AxonTestFixture Kotlin DSL — Given-When-Then DSL source to copy into the project