with one click
state-view-slice
Knows how to build state change slices
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Knows how to build state change slices
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
| name | state view slice |
| description | Knows how to build state change slices |
A STATE_VIEW slice represents a read-only projection/view of data. It does NOT contain commands - only read models that are populated by events from other slices. This is the "Query" side of CQRS.
{
"sliceType": "STATE_VIEW",
"title": "slice: <Name>",
"context": "<BoundedContext>",
"commands": [], // Always empty for STATE_VIEW
"events": [], // Always empty for STATE_VIEW
"readmodels": [...],
"screens": [...],
"specifications": [...]
}
Events (from STATE_CHANGE slices) → @EventHandler Projector → ReadModel Entity → @QueryHandler → Screen
Look at the readmodel's dependencies with type: "INBOUND" and elementType: "EVENT":
"dependencies": [
{
"type": "INBOUND",
"title": "Law Firm Created",
"elementType": "EVENT"
},
{
"type": "INBOUND",
"title": "Law Firm Updated",
"elementType": "EVENT"
}
]
These events must already exist (from STATE_CHANGE slices).
Before creating entities, determine which persistence pattern to use based on the readmodel's fields:
Decision Criteria:
| Scenario | Pattern | When to Use |
|---|---|---|
Single idAttribute: true field | Single Key | Entity represents one unique aggregate/record |
Multiple idAttribute: true fields | Multi Key (Composite) | Entity represents a combination (e.g., assignment, membership) |
listElement: true on readmodel | Multi Key (Composite) | ReadModel returns a list of related entities |
| Has parent/child relationship | Multi Key (Composite) | Entity belongs to another entity (e.g., person-to-case) |
Use when the readmodel has exactly ONE idAttribute: true field (typically aggregateId or similar).
Location: src/main/kotlin/de/alex/<module>/<readmodelname>/<ReadModelName>ReadModel.kt
package de.alex.<module>.<readmodelname>
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table
import java.util.UUID
data class <ReadModelName>ReadModelQuery(val aggregateId: UUID)
@Table(name = "<table_name>_read_model_entity", schema = "public")
@Entity
class <ReadModelName>ReadModelEntity {
@Id
@Column(name = "<id_field_name>")
var <idFieldName>: UUID? = null
// Add fields from readmodel definition (non-id fields)
@Column(name = "<field_name>")
var <fieldName>: <Type>? = null
// Common: lawfirmId for multi-tenancy
@Column(name = "lawfirm_id")
var lawfirmId: UUID? = null
}
data class <ReadModelName>ReadModel(val data: <ReadModelName>ReadModelEntity)
Real Example (CaseDetailsReadModel):
package de.alex.cases.casedetails
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table
import java.math.BigDecimal
import java.util.UUID
data class CaseDetailsReadModelQuery(val aggregateId: UUID)
@Table(name = "case_details_read_model_entity", schema = "public")
@Entity
class CaseDetailsReadModelEntity {
@Id @Column(name = "caseId") var caseId: UUID? = null
@Column(name = "ablagenummer") var ablagenummer: String? = null
@Column(name = "description") var description: String? = null
@Column(name = "disputeAmount") var disputeAmount: BigDecimal? = null
@Column(name = "active_case") var activeCase: Boolean? = null
@Column(name = "lawfirm_id") var lawfirmId: UUID? = null
}
data class CaseDetailsReadModel(val data: CaseDetailsReadModelEntity)
Location: src/main/kotlin/de/alex/<module>/<readmodelname>/internal/<ReadModelName>ReadModelRepository.kt (or inline in Projector)
package de.alex.<module>.<readmodelname>.internal
import de.alex.<module>.<readmodelname>.<ReadModelName>ReadModelEntity
import org.springframework.data.jpa.repository.JpaRepository
import java.util.UUID
interface <ReadModelName>ReadModelRepository : JpaRepository<<ReadModelName>ReadModelEntity, UUID> {
// Optional: query methods for filtering
fun findByLawfirmId(lawfirmId: UUID): List<<ReadModelName>ReadModelEntity>
}
package de.alex.<module>.<readmodelname>.internal
import de.alex.<module>.ProcessingGroups
import de.alex.<module>.<readmodelname>.<ReadModelName>ReadModelEntity
import de.alex.events.*
import org.axonframework.config.ProcessingGroup
import org.axonframework.eventhandling.EventHandler
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Component
import java.util.UUID
interface <ReadModelName>ReadModelRepository : JpaRepository<<ReadModelName>ReadModelEntity, UUID>
@ProcessingGroup(ProcessingGroups.<MODULE>)
@Component
class <ReadModelName>ReadModelProjector(var repository: <ReadModelName>ReadModelRepository) {
@EventHandler
fun on(event: <CreatedOrUpdatedEvent>) {
// findById returns Optional, use orElse for upsert pattern
val entity = repository.findById(event.aggregateId).orElse(<ReadModelName>ReadModelEntity())
entity.apply {
<idFieldName> = event.aggregateId
<fieldName> = event.<eventFieldName>
}.also { repository.save(it) }
}
@EventHandler
fun on(event: <DeleteEvent>) {
// Simple delete by primary key
repository.deleteById(event.aggregateId)
}
}
package de.alex.<module>.<readmodelname>.internal
import de.alex.<module>.<readmodelname>.<ReadModelName>ReadModel
import de.alex.<module>.<readmodelname>.<ReadModelName>ReadModelQuery
import org.axonframework.queryhandling.QueryHandler
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Component
@Component
class <ReadModelName>ReadModelQueryHandler(
private val repository: <ReadModelName>ReadModelRepository
) {
@QueryHandler
fun handleQuery(query: <ReadModelName>ReadModelQuery): <ReadModelName>ReadModel? {
val entity = repository.findByIdOrNull(query.aggregateId) ?: return null
return <ReadModelName>ReadModel(entity)
}
}
CREATE TABLE IF NOT EXISTS public.<table_name>_read_model_entity (
"<id_column>" UUID NOT NULL,
"<field_column>" <SQL_TYPE>,
"lawfirm_id" UUID,
PRIMARY KEY ("<id_column>")
);
CREATE INDEX IF NOT EXISTS idx_<table>_lawfirmid ON public.<table_name>_read_model_entity("lawfirm_id");
Use when the readmodel has MULTIPLE idAttribute: true fields or represents a many-to-many/parent-child relationship.
There are two common approaches for composite keys:
Option 1: Simple class with @NoArgConstructor (preferred for simple cases)
import de.alex.common.NoArgConstructor
@NoArgConstructor
class <ReadModelName>Key(
var <firstIdField>: UUID,
var <secondIdField>: UUID
)
Option 2: @Embeddable data class (when using embedded queries)
import jakarta.persistence.Embeddable
@Embeddable
data class <ReadModelName>Key(
var <firstIdField>: UUID,
var <secondIdField>: <Type> // Can be UUID, String, etc.
)
Location: src/main/kotlin/de/alex/<module>/<readmodelname>/<ReadModelName>ReadModel.kt
package de.alex.<module>.<readmodelname>
import de.alex.common.NoArgConstructor
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.IdClass
import jakarta.persistence.Table
import java.util.UUID
class <ReadModelName>ReadModelQuery() // Often empty for list queries
@NoArgConstructor
class <ReadModelName>Key(
var <firstIdField>: UUID,
var <secondIdField>: UUID
)
@IdClass(<ReadModelName>Key::class)
@Table(name = "<table_name>_read_model_entity", schema = "public")
@Entity
class <ReadModelName>ReadModelEntity {
@Id @Column(name = "<first_id>")
var <firstIdField>: UUID? = null
@Id @Column(name = "<second_id>")
var <secondIdField>: UUID? = null
// Add remaining non-id fields
@Column(name = "<field_name>")
var <fieldName>: <Type>? = null
@Column(name = "lawfirmId")
var lawfirmId: UUID? = null
}
// For composite keys, ReadModel typically wraps a List
data class <ReadModelName>ReadModel(val data: List<<ReadModelName>ReadModelEntity>)
Real Example (CaseAssignmentsReadModel):
package de.alex.cases.caseassignments
import de.alex.common.NoArgConstructor
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.IdClass
import jakarta.persistence.Table
import java.util.UUID
class CaseAssignmentsReadModelQuery()
@NoArgConstructor
class CaseAssignmentsKey(var caseId: UUID, var assignedPersonId: UUID)
@IdClass(CaseAssignmentsKey::class)
@Table(name = "case_assignments_read_model_entity", schema = "public")
@Entity
class CaseAssignmentsReadModelEntity {
@Id @Column(name = "caseId") var caseId: UUID? = null
@Id @Column(name = "assigned_person") var assignedPersonId: UUID? = null
@Column(name = "assigned_role") var assignedRole: String? = null
@Column(name = "lawfirmId") var lawfirmId: UUID? = null
}
data class CaseAssignmentsReadModel(val data: List<CaseAssignmentsReadModelEntity>)
Real Example with @Embeddable (TaskReadModel):
package de.alex.tasks.tasks
import jakarta.persistence.Column
import jakarta.persistence.Embeddable
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.IdClass
import jakarta.persistence.Table
import java.util.UUID
@Embeddable
data class TaskReadModelKey(var taskId: UUID, var caseId: UUID)
@IdClass(TaskReadModelKey::class)
@Table(name = "tasks_read_model_entity", schema = "public")
@Entity
class TaskReadModelEntity {
@Id @Column(name = "taskId") var taskId: UUID? = null
@Id @Column(name = "caseId") var caseId: UUID? = null
@Column(name = "title") var title: String? = null
@Column(name = "lawfirm_id") var lawFirmId: UUID? = null
}
data class TaskReadModel(val data: TaskReadModelEntity)
package de.alex.<module>.<readmodelname>.internal
import de.alex.<module>.<readmodelname>.<ReadModelName>Key
import de.alex.<module>.<readmodelname>.<ReadModelName>ReadModelEntity
import org.springframework.data.jpa.repository.JpaRepository
import java.util.UUID
interface <ReadModelName>ReadModelRepository :
JpaRepository<<ReadModelName>ReadModelEntity, <ReadModelName>Key> {
// Query by first part of composite key
fun findBy<FirstIdField>(<firstIdField>: UUID): List<<ReadModelName>ReadModelEntity>
// Query by second part of composite key
fun findBy<SecondIdField>(<secondIdField>: UUID): List<<ReadModelName>ReadModelEntity>
// Query by lawfirmId for multi-tenancy
fun findByLawfirmId(lawfirmId: UUID): List<<ReadModelName>ReadModelEntity>
}
package de.alex.<module>.<readmodelname>.internal
import de.alex.<module>.ProcessingGroups
import de.alex.<module>.<readmodelname>.<ReadModelName>Key
import de.alex.<module>.<readmodelname>.<ReadModelName>ReadModelEntity
import de.alex.events.*
import org.axonframework.config.ProcessingGroup
import org.axonframework.eventhandling.EventHandler
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Component
import java.util.UUID
interface <ReadModelName>ReadModelRepository :
JpaRepository<<ReadModelName>ReadModelEntity, <ReadModelName>Key>
@ProcessingGroup(ProcessingGroups.<MODULE>)
@Component
class <ReadModelName>ReadModelProjector(var repository: <ReadModelName>ReadModelRepository) {
@EventHandler
fun on(event: <AddedEvent>) {
// Construct composite key for lookup
val key = <ReadModelName>Key(event.<firstIdField>, event.<secondIdField>)
val entity = repository.findById(key).orElse(<ReadModelName>ReadModelEntity())
entity.apply {
<firstIdField> = event.<firstIdField>
<secondIdField> = event.<secondIdField>
<fieldName> = event.<eventFieldName>
lawfirmId = event.lawFirmId
}.also { repository.save(it) }
}
@EventHandler
fun on(event: <RemovedEvent>) {
// Delete using composite key
val key = <ReadModelName>Key(event.<firstIdField>, event.<secondIdField>)
repository.deleteById(key)
}
}
Real Example (CaseAssignmentsProjector):
@ProcessingGroup(ProcessingGroups.CASES)
@Component
class CaseAssignmentsReadModelProjector(var repository: CaseAssignmentsReadModelRepository) {
@EventHandler
fun on(event: CasePersonDeletedEvent) {
repository.deleteById(CaseAssignmentsKey(event.aggregateId, event.personId))
}
@EventHandler
fun on(event: CasePersonAddedEvent) {
val entity = repository
.findById(CaseAssignmentsKey(event.aggregateId, event.personId))
.orElse(CaseAssignmentsReadModelEntity())
entity.apply {
caseId = event.aggregateId
assignedPersonId = event.personId
assignedRole = event.role
lawfirmId = event.lawFirmId
}.also { repository.save(it) }
}
}
package de.alex.<module>.<readmodelname>.internal
import de.alex.<module>.<readmodelname>.<ReadModelName>ReadModel
import de.alex.<module>.<readmodelname>.<ReadModelName>ReadModelQuery
import org.axonframework.queryhandling.QueryHandler
import org.springframework.stereotype.Component
@Component
class <ReadModelName>ReadModelQueryHandler(
private val repository: <ReadModelName>ReadModelRepository
) {
@QueryHandler
fun handleQuery(query: <ReadModelName>ReadModelQuery): <ReadModelName>ReadModel {
// For composite keys, typically return a list
val entities = repository.findBy<FirstIdField>(query.<firstIdField>)
return <ReadModelName>ReadModel(entities)
}
}
CREATE TABLE IF NOT EXISTS public.<table_name>_read_model_entity (
"<first_id_column>" UUID NOT NULL,
"<second_id_column>" UUID NOT NULL,
"<field_column>" <SQL_TYPE>,
"lawfirmId" UUID,
PRIMARY KEY ("<first_id_column>", "<second_id_column>")
);
-- Indexes for querying by individual parts of composite key
CREATE INDEX IF NOT EXISTS idx_<table>_<first_id> ON public.<table_name>_read_model_entity("<first_id_column>");
CREATE INDEX IF NOT EXISTS idx_<table>_<second_id> ON public.<table_name>_read_model_entity("<second_id_column>");
CREATE INDEX IF NOT EXISTS idx_<table>_lawfirmid ON public.<table_name>_read_model_entity("lawfirmId");
Field Mapping Rules:
| Slice Type | Kotlin Type |
|---|---|
| UUID | UUID |
| String | String |
| Date | LocalDate |
| Integer | Int |
| Boolean | Boolean |
| Decimal | BigDecimal |
| Multiple cardinality | List |
| optional: true | Nullable (T?) |
| idAttribute: true | Use as @Id field |
Location: src/main/kotlin/de/alex/<module>/<readmodelname>/internal/<ReadModelName>ReadModelRepository.kt
package de.alex.<module>.<readmodelname>.internal
import de.alex.<module>.<readmodelname>.<ReadModelName>ReadModelEntity
import org.springframework.data.jpa.repository.JpaRepository
import java.util.UUID
interface <ReadModelName>ReadModelRepository : JpaRepository<<ReadModelName>ReadModelEntity, UUID> {
// Add custom query methods based on screen requirements
fun findByLawfirmId(lawfirmId: UUID): List<<ReadModelName>ReadModelEntity>
fun findByAggregateId(aggregateId: UUID): <ReadModelName>ReadModelEntity?
}
Location: src/main/kotlin/de/alex/<module>/<readmodelname>/internal/<ReadModelName>ReadModelProjector.kt
package de.alex.<module>.<readmodelname>.internal
import de.alex.<module>.ProcessingGroups
import de.alex.events.* // Import source events
import org.axonframework.eventhandling.EventHandler
import org.axonframework.config.ProcessingGroup
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Component
@ProcessingGroup(ProcessingGroups.<MODULE>)
@Component
class <ReadModelName>ReadModelProjector(
private val repository: <ReadModelName>ReadModelRepository
) {
@EventHandler
fun on(event: <SourceEvent1>Event) {
val entity = repository.findByIdOrNull(event.aggregateId)
?: <ReadModelName>ReadModelEntity()
entity.apply {
aggregateId = event.aggregateId
// Map event fields to entity fields
// Use field.mapping if specified to map different field names
<fieldName> = event.<eventFieldName>
}.also { repository.save(it) }
}
@EventHandler
fun on(event: <SourceEvent2>Event) {
val entity = repository.findByIdOrNull(event.aggregateId)
?: return // Or create new if appropriate
entity.apply {
// Update only the fields this event provides
<fieldName> = event.<eventFieldName>
}.also { repository.save(it) }
}
// Handle delete events if applicable
@EventHandler
fun on(event: <DeleteEvent>Event) {
repository.deleteById(event.aggregateId)
}
}
Location: src/main/kotlin/de/alex/<module>/<readmodelname>/<ReadModelName>ReadModelQuery.kt
package de.alex.<module>.<readmodelname>
import java.util.UUID
data class <ReadModelName>ReadModelQuery(
val aggregateId: UUID? = null,
val lawFirmId: UUID? = null
// Add query parameters based on how screens need to filter data
)
Location: src/main/kotlin/de/alex/<module>/<readmodelname>/internal/<ReadModelName>ReadModelQueryHandler.kt
package de.alex.<module>.<readmodelname>.internal
import de.alex.<module>.<readmodelname>.<ReadModelName>ReadModel
import de.alex.<module>.<readmodelname>.<ReadModelName>ReadModelQuery
import org.axonframework.queryhandling.QueryHandler
import org.springframework.stereotype.Component
@Component
class <ReadModelName>ReadModelQueryHandler(
private val repository: <ReadModelName>ReadModelRepository
) {
@QueryHandler
fun handleQuery(query: <ReadModelName>ReadModelQuery): <ReadModelName>ReadModel? {
// For single entity lookup
val entity = repository.findByIdOrNull(query.aggregateId) ?: return null
return <ReadModelName>ReadModel(entity)
// OR for list queries
// return <ReadModelName>ReadModel(repository.findByLawfirmId(query.lawFirmId))
}
}
Location: src/main/resources/db/migration/V<version>__create_<table_name>.sql
CREATE TABLE IF NOT EXISTS public.<table_name> (
"aggregateid" UUID NOT NULL,
-- Add columns for each field in readmodel
"<column_name>" <SQL_TYPE>,
PRIMARY KEY ("aggregateid")
-- Or for composite key:
-- PRIMARY KEY ("aggregateid", "<secondary_id>")
);
-- Add indexes for frequently queried fields
CREATE INDEX IF NOT EXISTS idx_<table>_lawfirmid ON public.<table_name>("lawfirmid");
If the readmodel has an apiEndpoint field, create a REST controller:
Location: src/main/kotlin/de/alex/<module>/<readmodelname>/<ReadModelName>Controller.kt
package de.alex.<module>.<readmodelname>
import org.axonframework.queryhandling.QueryGateway
import org.springframework.web.bind.annotation.*
import java.util.UUID
@RestController
@RequestMapping("<apiEndpoint base>")
class <ReadModelName>Controller(
private val queryGateway: QueryGateway
) {
@GetMapping("/{aggregateId}")
fun get(@PathVariable aggregateId: UUID): <ReadModelName>ReadModel? {
return queryGateway.query(
<ReadModelName>ReadModelQuery(aggregateId),
<ReadModelName>ReadModel::class.java
).join()
}
}
STATE_VIEW specifications use Given/Then format (no When since no commands):
Location: src/test/kotlin/de/alex/<module>/<ReadModelName>ProjectionTest.kt
package de.alex.<module>
import de.alex.BaseIntegrationTest
import de.alex.ProjectionFixtureConfiguration
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
class <ReadModelName>ProjectionTest : BaseIntegrationTest() {
@Autowired
lateinit var projectionFixture: ProjectionFixtureConfiguration
@Autowired
lateinit var repository: <ReadModelName>ReadModelRepository
@Test
fun `<spec title>`() {
// Given: Apply events from specification
projectionFixture.apply(<SourceEvent>(...)) // From spec.given
// Then: Verify read model state
awaitUntilAsserted {
val result = repository.findById(<id>).orElse(null)
assertThat(result).isNotNull
assertThat(result.<field>).isEqualTo(<expectedValue>) // From spec.then
}
}
}
Input:
{
"sliceType": "STATE_VIEW",
"title": "slice: Law Firm Details for Auth",
"readmodels": [{
"title": "Law Firm Details for Auth",
"fields": [
{"name": "aggregateId", "type": "UUID", "idAttribute": true},
{"name": "name", "type": "String"}
],
"dependencies": [
{"type": "INBOUND", "title": "Law Firm Created", "elementType": "EVENT"},
{"type": "INBOUND", "title": "Law Firm Updated", "elementType": "EVENT"}
]
}],
"screens": [...]
}
Generated Files:
src/main/kotlin/de/alex/lawfirm/lawfirmdetailsforauth/LawFirmDetailsForAuthReadModel.kt (Entity + DTO)src/main/kotlin/de/alex/lawfirm/lawfirmdetailsforauth/internal/LawFirmDetailsForAuthReadModelRepository.ktsrc/main/kotlin/de/alex/lawfirm/lawfirmdetailsforauth/internal/LawFirmDetailsForAuthReadModelProjector.ktsrc/main/kotlin/de/alex/lawfirm/lawfirmdetailsforauth/LawFirmDetailsForAuthReadModelQuery.ktsrc/main/kotlin/de/alex/lawfirm/lawfirmdetailsforauth/internal/LawFirmDetailsForAuthReadModelQueryHandler.ktsrc/main/resources/db/migration/V<n>__create_law_firm_details_for_auth.sqlWhen a readmodel field has a mapping attribute:
{
"name": "lawfirmId",
"type": "UUID",
"mapping": "aggregateId"
}
This means the lawfirmId field in the readmodel should be populated from the aggregateId field in the source event.
Screens in STATE_VIEW slices typically:
This separation keeps the query side clean while allowing screens to trigger write operations.