{"id":31558046,"url":"https://github.com/abaddon/kcqrs-core","last_synced_at":"2026-05-17T00:32:08.057Z","repository":{"id":43372323,"uuid":"421411715","full_name":"abaddon/kcqrs-core","owner":"abaddon","description":"Kotlin CQRS library","archived":false,"fork":false,"pushed_at":"2025-10-03T10:28:23.000Z","size":585,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-03T11:22:10.475Z","etag":null,"topics":["cqrs","cqrs-es","ddd","eventstoredb","gradle","kotlin"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/abaddon.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2021-10-26T12:16:24.000Z","updated_at":"2025-10-03T10:28:26.000Z","dependencies_parsed_at":"2025-10-03T11:15:52.632Z","dependency_job_id":"a77c3da6-0c97-4ac8-8d67-fe44db528e0b","html_url":"https://github.com/abaddon/kcqrs-core","commit_stats":null,"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"purl":"pkg:github/abaddon/kcqrs-core","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abaddon%2Fkcqrs-core","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abaddon%2Fkcqrs-core/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abaddon%2Fkcqrs-core/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abaddon%2Fkcqrs-core/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/abaddon","download_url":"https://codeload.github.com/abaddon/kcqrs-core/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abaddon%2Fkcqrs-core/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278391429,"owners_count":25978988,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-04T02:00:05.491Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cqrs","cqrs-es","ddd","eventstoredb","gradle","kotlin"],"created_at":"2025-10-05T00:12:42.334Z","updated_at":"2025-10-05T00:12:43.771Z","avatar_url":"https://github.com/abaddon.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# kotlin-cqrs (Kcqrs)\n![Maven Central Version](https://img.shields.io/maven-central/v/io.github.abaddon.kcqrs/kcqrs-core?versionPrefix=0.\u0026style=flat\u0026label=version\u0026color=green)\n[![Java CI with Gradle](https://github.com/abaddon/kotlin-cqrs/actions/workflows/gradle.yml/badge.svg)](https://github.com/abaddon/kotlin-cqrs/actions/workflows/gradle.yml)\n[![codecov](https://codecov.io/gh/abaddon/kcqrs-core/branch/main/graph/badge.svg?token=1N8KGK99QV)](https://codecov.io/gh/abaddon/kcqrs-core)\n\nA Kotlin CQRS library based on [C# Muflone library](https://github.com/CQRS-Muflone/Muflone)\n\n### Libraries\n- [kcqrs-core](https://github.com/abaddon/kcqrs-core) It contains the main entities like:\n  - Entity\n  - Aggregate\n  - Event\n  - Command\n  - Projection\n  - Command and Projection handlers\n  - Aggregate and Projection repositories\n- [kcqrs-EventStoreDB](https://github.com/abaddon/kcqrs-EventStoreDB)  EventstoreDB implementation of event store repository and projection handler\n- [kcqrs-test](https://github.com/abaddon/kcqrs-test) it offers a simple test suite to test easily: commands, aggregate, and events\n- [kcqrs-example](https://github.com/abaddon/kcqrs-example)  Simple examples of how use KCQRS libs\n\n### Architecture\n![kcqrs-schema](docs/kcqrs-schema.jpg)\n\n### Getting started\n\n#### Define an Aggregate\nThe scope of this aggregate is increase or decrease an internal field called `counter` following these business logics:\n- counter has to be \u003e 0\n- counter has to be \u003c Int.MAX_VALUE\n\nThe allowed operation are:\n- initialise the counter with the value received\n- increase the counter of the value received\n- decrease the counter of the value received\nThe value received has to be a value between 0 and Int.MAX_VALUE, or it will be rejected\n\n```kotlin\n/**  Aggregate Identity **/\ndata class CounterAggregateId(val value: UUID) : IIdentity {\n    constructor (): this(UUID.randomUUID())\n    override fun valueAsString(): String {\n        return value.toString()\n    }\n}\n\ndata class CounterAggregateRoot constructor(\n    override val id: CounterAggregateId,\n    override val version: Long,\n    val counter: Int,\n    override val uncommittedEvents: MutableCollection\u003cIDomainEvent\u003e\n) : AggregateRoot() {\n    private val log = LoggerFactory.getLogger(this::class.simpleName)\n\n    companion object {\n        fun initialiseCounter(id: CounterAggregateId, initialValue: Int): CounterAggregateRoot {\n            /** Initialisation an empty aggregate **/\n            val emptyAggregate = CounterAggregateRoot(id, 0L, 0, ArrayList\u003cIDomainEvent\u003e())\n            return try {\n                /** Validate incrementValue **/\n                check(initialValue \u003e= 0 \u0026\u0026 initialValue \u003c Int.MAX_VALUE) { \"Value $initialValue not valid, it has to be \u003e= 0 and \u003c ${Int.MAX_VALUE}\" }\n                /** Raise the event CounterInitialisedEvent if the initial value is right. The empty aggregate is used only as container for the event generated. **/\n                emptyAggregate.raiseEvent(CounterInitialisedEvent(id, initialValue)) as CounterAggregateRoot\n            } catch (e: Exception) {\n                /** In case of error an error event is generated **/\n                emptyAggregate.raiseEvent(DomainErrorEvent(id, e)) as CounterAggregateRoot\n            }\n        }\n    }\n    \n    fun increaseCounter(incrementValue: Int): CounterAggregateRoot {\n        return try {\n            /** Validate incrementValue **/\n            check(incrementValue \u003e= 0 \u0026\u0026 incrementValue \u003c Int.MAX_VALUE) { \"Value $incrementValue not valid, it has to be \u003e= 0 and \u003c ${Int.MAX_VALUE}\" }\n            val updatedCounter = counter + incrementValue\n            /**  Validate updatedCounter **/\n            check(updatedCounter \u003c Int.MAX_VALUE) { \"Aggregate value $updatedCounter is not valid, it has to be \u003c ${Int.MAX_VALUE}\" }\n            /** Raise the event CounterIncreasedEvent if the updatedCounter value is right. **/  \n            raiseEvent(CounterIncreasedEvent(id, incrementValue)) as CounterAggregateRoot\n        } catch (e: Exception) {\n            raiseEvent(DomainErrorEvent(id, e)) as CounterAggregateRoot\n        }\n    }\n\n    fun decreaseCounter(decrementValue: Int): CounterAggregateRoot {\n        return try {\n            check(decrementValue \u003e= 0 \u0026\u0026 decrementValue \u003c Int.MAX_VALUE) { \"Value $decrementValue not valid, it has to be \u003e= 0 and \u003c ${Int.MAX_VALUE}\" }\n            val updatedCounter = counter - decrementValue\n            check(updatedCounter \u003e= 0) { \"Aggregate value $updatedCounter is not valid, it has to be \u003e= 0\" }\n            /** Raise the event CounterDecreaseEvent if the updatedCounter value is right. **/\n            raiseEvent(CounterDecreaseEvent(id, decrementValue)) as CounterAggregateRoot\n        } catch (e: HandlerForDomainEventNotFoundException) {\n            raiseEvent(DomainErrorEvent(id, e)) as CounterAggregateRoot\n        }\n    }\n\n    /**\n     * AggregateRoot use a EventRouter based on the function name, so you don't need to register for each event the proper function to call.\n     * All functions called apply(...) are automatically registered in the Event Route.\n     * \n     * The previous methods don't apply any changes on the aggregate, they trigger events only.\n     * Events triggered are then apply to the Aggregate using the functions apply(...) below.\n     * One apply function for each event to apply to the Aggregate. \n     * The apply function is pretty simple, and it doesn't contain any validation because the events are the source of true.\n     **/\n    private fun apply(event: CounterInitialisedEvent): CounterAggregateRoot {\n        return copy(id = event.aggregateId, version = version + 1, counter = event.value)\n    }\n\n    private fun apply(event: CounterIncreasedEvent): CounterAggregateRoot {\n        val newCounter = counter + event.value;\n        return copy(counter = newCounter, version = version + 1)\n    }\n\n    private fun apply(event: CounterDecreaseEvent): CounterAggregateRoot {\n        val newCounter = counter - event.value;\n        return copy(counter = newCounter, version = version + 1)\n    }\n\n    private fun apply(event: DomainErrorEvent): CounterAggregateRoot {\n        return copy(version = version + 1)\n    }\n\n}\n```\n#### Define commands\nCommands are entity used to perform an operation on its Aggregate root.\nA Command has to extend the abstract class Command\u003cTAggregateRoot\u003e.\nEach command class has to implement the method `execute(aggregate:TAggregateRoot?)`. \nThis method contain the business logic of the command, leaving the CommandHandler simpler and generic.\n\n```kotlin\ndata class InitialiseCounterCommand(\n    override val aggregateID: CounterAggregateId, //aggregate identity. Each command has to be linked to only one aggregate instance  \n    val value: Int // the values / parameters that the command needs\n): Command\u003cCounterAggregateRoot\u003e(aggregateID) {\n    /**\n     * This method receive the existing aggregate, if exist and then perform the operation on the aggregate\n     * In this case the command create a new aggregate, so currentAggregate is null and it's not used \n     */\n    override fun execute(currentAggregate: CounterAggregateRoot?): CounterAggregateRoot {\n        return CounterAggregateRoot.initialiseCounter(aggregateID, value)\n    }\n}\n\ndata class IncreaseCounterCommand(\n    override val aggregateID: CounterAggregateId,\n    val value: Int\n): Command\u003cCounterAggregateRoot\u003e(aggregateID) {\n    /**\n     * In this case the command want to increase the aggregate value, so the currentAggregate has to exist. If it's missing an exception is raised\n     * After the currentAggregate validation, the method execute the aggregate method to increase the counter.\n     */\n    override fun execute(currentAggregate: CounterAggregateRoot?): CounterAggregateRoot {\n        requireNotNull(currentAggregate)\n        return currentAggregate.increaseCounter(value)\n    }\n}\n\n\n```\n\n#### Define an AggregateHandler\nThe Command Handler scope is to receive a command and execute it.\nSimpleAggregateCommandHandler is a Command Handler that should be suitable in most of the cases. \nIn the other situation you can implement directly the IAggregateCommandHandler.\n\nSimpleAggregateCommandHandler everytime a new command is coming, try to rehydrate the aggregate calling the repository using as key the aggregateIdentity in the command.\nThe rehydrate aggregate is used to execute the command.\nThe output of the execution is a new aggregate that will be saved on the repository.\n\n```kotlin\nclass SimpleAggregateCommandHandler\u003cTAggregate : IAggregate\u003e(\n  override val repository: IAggregateRepository\u003cTAggregate\u003e,\n) : IAggregateCommandHandler\u003cTAggregate\u003e {\n  \n  override suspend fun handle(\n    command: ICommand\u003cTAggregate\u003e,\n    updateHeaders: () -\u003e Map\u003cString, String\u003e\n  ): Result\u003cException, TAggregate\u003e =\n    when (val actualAggregateResult = repository.getById(command.aggregateID)) {\n      is Result.Valid -\u003e {\n        val newAggregate = command.execute(actualAggregateResult.value)\n        repository.save(newAggregate, UUID.randomUUID(), updateHeaders)\n      }\n      is Result.Invalid -\u003e actualAggregateResult\n    }\n\n  override suspend fun handle(command: ICommand\u003cTAggregate\u003e): Result\u003cException, TAggregate\u003e =\n    handle(command) { mapOf\u003cString, String\u003e() }\n}\n```\n\n#### Define a DomainEvent\nA domain Event is what we persist to rehydrate an aggregate and represent the source of true.\nEach event has to extend the interface IDomainEvent.\n\nEach event has to be related to an aggregate identity (`aggregateId`).\nThe field `aggregateType` has to contain the name of the aggregateRoot class\n\n```kotlin\ndata class CounterIncreasedEvent(\n    override val messageId: UUID,\n    override val aggregateId: CounterAggregateId,\n    override val version: Int = 1,\n    override val aggregateType: String,\n    override val header: EventHeader,\n    val value: Int,\n) : IDomainEvent {\n    constructor(aggregateId: CounterAggregateId, value: Int) : this(UUID.randomUUID(), aggregateId, 1, \"CounterAggregateRoot\", EventHeader.create(\"CounterAggregateRoot\"),value)\n\n\n}\n```\n#### Define a DomainEvent Repository\n A domain repository is created extending the `IAggregateRepository` interface.\nThe interface contains basically two main functions to implement:\n- `fun getById(aggregateId: IIdentity):TAggregate?` used to retrieve the aggregate  \n- `fun save(aggregate: TAggregate, commitID: UUID)` used to persiste the aggregate\n\nThe interface `IAggregateRepository` allow you to implement any type of repository.\nThe abstract class `EventStoreRepository` implement some logic to manage the repository as an event store to implement the event sourcing pattern.\n\nBelow the implementation of an in memory eventStore repository. A different implementation that use [EventStoreDB](https://www.eventstore.com/eventstoredb) is [kcqrs-EventStoreDB](https://github.com/abaddon/kcqrs-EventStoreDB) \n\n\n```kotlin\nclass InMemoryEventStoreRepository\u003cTAggregate : IAggregate\u003e(\n  /** It's the root of the stream. Each aggregate has its dedicated stream. */\n  private val _streamNameRoot: String,\n  /** It's the function used to create an empty aggregate. It's used during the aggregate rehydration  */\n  private val _emptyAggregate: (aggregateId: IIdentity) -\u003e TAggregate\n) : EventStoreRepository\u003cTAggregate\u003e() {\n\n  /** In memory storage, a map with all aggregate. The key is the stream name and the value a list of DomainEvent  */\n  private val storage = mutableMapOf\u003cString, MutableList\u003cIDomainEvent\u003e\u003e()\n  /** list of the projection handler subscribed to the event store.  */\n  private val projectionHandlers = mutableListOf\u003cIProjectionHandler\u003c*\u003e\u003e()\n\n  override val log: Logger = LoggerFactory.getLogger(this.javaClass.simpleName)\n  \n  /** it's the logic to create the stream name  */\n  override fun aggregateIdStreamName(aggregateId: IIdentity): String =\n    \"${_streamNameRoot}.${aggregateId.valueAsString()}\"\n\n  /**\n   * This method should be used only for testing purpose.\n   * It allows saving events directly to the Events store without using the aggregate\n   */\n  fun addEventsToStorage(aggregateId: IIdentity, events: List\u003cIDomainEvent\u003e) {\n    persist(aggregateIdStreamName(aggregateId), events, mapOf(), 0)\n  }\n  \n  /**\n   * This method should be used only for testing purpose.\n   * It allows getting events directly from the Events store\n   */\n  fun loadEventsFromStorage(aggregateId: IIdentity): List\u003cIDomainEvent\u003e =\n    load(aggregateIdStreamName(aggregateId))\n\n  /** \n   * Persist method receive the list of events uncommitted and contain the logic to save the event on the in memory storage.\n   * If you want to change the place where store the events you have to change the persist method \n   */\n  override fun persist(\n    streamName: String,\n    uncommittedEvents: List\u003cIDomainEvent\u003e,\n    header: Map\u003cString, String\u003e,\n    currentVersion: Long\n  ) {\n    val currentEvents = storage.getOrDefault(streamName, listOf()).toMutableList()\n    currentEvents.addAll(uncommittedEvents.toMutableList())\n    storage[streamName] = currentEvents\n  }\n\n  /**\n   *  Load method return the list of events available in the storage related to as specific aggregate.\n   */\n  override fun load(streamName: String, startFrom: Long): List\u003cIDomainEvent\u003e =\n    storage.getOrDefault(streamName, listOf())\n\n  /**\n   * The Subscribe method is used to subscribe a projectionHandler allowing it to receive the events published that could be used to update the projections/views\n   */\n  override fun \u003cTProjection : IProjection\u003e subscribe(projectionHandler: IProjectionHandler\u003cTProjection\u003e) {\n    projectionHandlers.add(projectionHandler)\n  }\n\n  override fun emptyAggregate(aggregateId: IIdentity): TAggregate = _emptyAggregate(aggregateId)\n\n  override fun publish(events: List\u003cIDomainEvent\u003e) {\n    projectionHandlers.forEach{projectionHandlers -\u003e projectionHandlers.onEvents(events)}\n  }\n}\n```\n\n#### Define a Projection\n\nA projection is like a SQL view. Often the data that we have to publish/show following a data structure completely different from our aggregate structure. As consequence, we should implement complex query to reorganise the data in the format that we need.\nProjections or Views help to reduce this complexity. A projection is a like a SQL view, where we include only the data that we need to publish. The projection is populated by events generated by the aggregate.\nEach view contain internally the business logic that explain how update itself everytime an event come. Each Projection should have a clear and defined purpose. It helps to maintain the logic lean and clean.\n\nIn the example the scope of the projection is count each type of event received.\nEach projection has to implement the interface `IProjection`.\n\n```kotlin\ndata class EventTypesCounterProjection(\n    override val key: EventTypesCounterProjectionKey, // key of the projection\n    val numIncreasedEvent: Int, // num of IncreasedEvent received\n    val numDecreaseEvent: Int,  // num of DecreaseEvent received\n) : IProjection {\n    private val log: Logger = LoggerFactory.getLogger(this.javaClass.simpleName)\n\n  /**\n   * The method applyEvent, is triggered every time a new event is persisted on the repository.\n   * The projection has to identify the event type and change the projection following the business rules \n   */\n    override fun applyEvent(event: IDomainEvent): IProjection {\n        log.info(\"applying event with messageId: ${event.messageId}\")\n        return when (event) {\n            is CounterIncreasedEvent -\u003e copy(numIncreasedEvent = this.numIncreasedEvent + 1)\n            is CounterDecreaseEvent -\u003e copy(numDecreaseEvent = this.numDecreaseEvent + 1)\n            else -\u003e this\n        }\n    }\n}\n```\n\n#### Define a Projection Repository\n\nA projection repository is used to persist projections, avoiding having to generate them from the beginning to each new event.\nThis class has to implement the interface IProjectionRepository.\n\nIn the example the repository is in memory\n\n```kotlin\nclass InMemoryProjectionRepository\u003cTProjection : IProjection\u003e(\n  private val _emptyProjection: (key: IProjectionKey)-\u003e TProjection  //function used to create an empty projection if it doesn't exist yet \n) : IProjectionRepository\u003cTProjection\u003e {\n    private val inMemoryStorage = mutableMapOf\u003cIProjectionKey, TProjection\u003e()\n    var offsetStorage: Long = 0\n\n\n    override suspend fun getByKey(key: IProjectionKey): TProjection? {\n        return inMemoryStorage[key]\n    }\n\n    override suspend fun save(projection: TProjection, offset: Long) {\n        inMemoryStorage[projection.key] = projection\n        offsetStorage = if (offset \u003e offsetStorage) offset else offsetStorage\n    }\n\n    override fun emptyProjection(key: IProjectionKey): TProjection =_emptyProjection(key)\n\n}\n```\n\n#### Define a Projection Handler\nThe Projection Handler's purpose is to receive the events published by the EventStore repository, send them the projection and persist the updated projection. \nIf the projection doesn't exist, it will be created and then the event is applied to it.\n\n```kotlin\ninterface IProjectionHandler\u003cTProjection:IProjection\u003e {\n    val log: Logger\n    val repository: IProjectionRepository\u003cTProjection\u003e\n\n    val projectionKey: IProjectionKey\n\n\n    @Suppress(\"UNCHECKED_CAST\")\n    fun onEvent(event: IDomainEvent) {\n        runBlocking {\n            try {\n                val updatedProjection = (repository.getByKey(projectionKey)\n                    ?: repository.emptyProjection(projectionKey)).applyEvent(event) as TProjection\n                repository.save(updatedProjection, 0)\n            }catch(ex : Exception){\n                log.error(\"Event not applied\",ex)\n            }\n        }\n    }\n\n    fun onEvents(events: List\u003cIDomainEvent\u003e) {\n        events.forEach{ domainEvent -\u003e\n            onEvent(domainEvent)\n        }\n    }\n\n}\n```\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fabaddon%2Fkcqrs-core","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fabaddon%2Fkcqrs-core","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fabaddon%2Fkcqrs-core/lists"}