├── mkdocs ├── docs │ ├── concepts │ │ ├── cqrs │ │ │ └── index.md │ │ ├── README.md │ │ └── events │ │ │ └── index.md │ ├── CNAME │ ├── images │ │ ├── logo.png │ │ └── favicon.png │ ├── tutorials │ │ ├── 01_setup │ │ │ ├── intellij_01.png │ │ │ ├── intellij_02.png │ │ │ ├── intellij_03.png │ │ │ └── spring_initializr_01.png │ │ └── README.md │ ├── js │ │ └── open_external_in_new_tab.js │ ├── reference │ │ ├── test_support │ │ │ └── index.md │ │ ├── core_components │ │ │ └── index.md │ │ └── README.md │ ├── howto │ │ └── README.md │ └── index.md ├── includes │ └── glossary.md └── main.py ├── .envrc ├── banner.png ├── SETUP.md ├── esdb-client ├── src │ ├── test │ │ ├── resources │ │ │ └── application.yml │ │ └── java │ │ │ └── com │ │ │ └── opencqrs │ │ │ └── esdb │ │ │ └── client │ │ │ └── TestApplication.java │ └── main │ │ └── java │ │ └── com │ │ └── opencqrs │ │ └── esdb │ │ └── client │ │ ├── package-info.java │ │ ├── jackson │ │ └── package-info.java │ │ ├── eventql │ │ ├── package-info.java │ │ ├── EventQuery.java │ │ ├── EventQueryBuilder.java │ │ ├── EventQueryProcessingError.java │ │ ├── EventQueryErrorHandler.java │ │ └── EventQueryRowHandler.java │ │ ├── IdUtil.java │ │ ├── Health.java │ │ ├── EventCandidate.java │ │ ├── Util.java │ │ ├── Event.java │ │ └── Precondition.java └── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── framework ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── opencqrs │ │ │ └── framework │ │ │ ├── package-info.java │ │ │ ├── command │ │ │ ├── package-info.java │ │ │ ├── cache │ │ │ │ ├── package-info.java │ │ │ │ ├── NoStateRebuildingCache.java │ │ │ │ └── LruInMemoryStateRebuildingCache.java │ │ │ ├── CommandSubjectDoesNotExistException.java │ │ │ ├── CommandSubjectAlreadyExistsException.java │ │ │ ├── SourcingMode.java │ │ │ ├── StateRebuildingHandlerDefinition.java │ │ │ ├── CommandSubjectConditionViolatedException.java │ │ │ ├── CommandHandlerDefinition.java │ │ │ ├── CommandEventCapturer.java │ │ │ ├── Command.java │ │ │ └── Util.java │ │ │ ├── metadata │ │ │ ├── package-info.java │ │ │ ├── PropagationUtil.java │ │ │ └── PropagationMode.java │ │ │ ├── types │ │ │ ├── package-info.java │ │ │ ├── EventTypeResolutionException.java │ │ │ ├── EventTypeResolver.java │ │ │ ├── ClassNameEventTypeResolver.java │ │ │ └── PreconfiguredAssignableClassEventTypeResolver.java │ │ │ ├── upcaster │ │ │ ├── package-info.java │ │ │ ├── NoEventUpcaster.java │ │ │ ├── TypeChangingEventUpcaster.java │ │ │ ├── EventUpcaster.java │ │ │ └── AbstractEventDataMarshallingEventUpcaster.java │ │ │ ├── eventhandler │ │ │ ├── package-info.java │ │ │ ├── progress │ │ │ │ ├── package-info.java │ │ │ │ ├── Progress.java │ │ │ │ ├── ProgressTracker.java │ │ │ │ └── InMemoryProgressTracker.java │ │ │ ├── partitioning │ │ │ │ ├── package-info.java │ │ │ │ ├── PerSubjectEventSequenceResolver.java │ │ │ │ ├── NoEventSequenceResolver.java │ │ │ │ ├── PartitionKeyResolver.java │ │ │ │ ├── DefaultPartitionKeyResolver.java │ │ │ │ ├── PerConfigurableLevelSubjectEventSequenceResolver.java │ │ │ │ └── EventSequenceResolver.java │ │ │ ├── EventHandlerDefinition.java │ │ │ └── BackOff.java │ │ │ ├── persistence │ │ │ ├── package-info.java │ │ │ ├── EventSource.java │ │ │ ├── EventCapturer.java │ │ │ ├── CapturedEvent.java │ │ │ ├── EventPublisher.java │ │ │ └── ImmediateEventPublisher.java │ │ │ ├── client │ │ │ ├── package-info.java │ │ │ ├── ConcurrencyException.java │ │ │ └── ClientInterruptedException.java │ │ │ ├── serialization │ │ │ ├── package-info.java │ │ │ ├── EventData.java │ │ │ ├── JacksonEventDataMarshaller.java │ │ │ └── EventDataMarshaller.java │ │ │ └── CqrsFrameworkException.java │ └── test │ │ ├── java │ │ └── com │ │ │ └── opencqrs │ │ │ └── framework │ │ │ ├── MyEvent.java │ │ │ ├── Book.java │ │ │ ├── BookBorrowedEvent.java │ │ │ ├── BookReturnedEvent.java │ │ │ ├── BookPageDamagedEvent.java │ │ │ ├── BookAddedEvent.java │ │ │ ├── TestApplication.java │ │ │ ├── AddBookCommand.java │ │ │ ├── BorrowBookCommand.java │ │ │ ├── ReturnBookCommand.java │ │ │ ├── ArchiveBookCommand.java │ │ │ ├── eventhandler │ │ │ └── partitioning │ │ │ │ └── PerConfigurableLevelSubjectEventSequenceResolverTest.java │ │ │ ├── command │ │ │ └── cache │ │ │ │ └── LruInMemoryStateRebuildingCacheConcurrentUpdateTest.java │ │ │ └── metadata │ │ │ └── PropagationUtilTest.java │ │ └── resources │ │ └── application.yml └── build.gradle.kts ├── .gitignore ├── framework-test ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── spring │ │ │ │ └── com.opencqrs.framework.command.CommandHandlingTest.imports │ │ └── java │ │ │ └── com │ │ │ └── opencqrs │ │ │ └── framework │ │ │ └── command │ │ │ └── CommandHandlingTestExcludeFilter.java │ └── test │ │ └── java │ │ └── com │ │ └── opencqrs │ │ └── framework │ │ ├── State.java │ │ ├── MyEvent.java │ │ ├── command │ │ ├── MyCommand1.java │ │ ├── MyCommand2.java │ │ ├── MyCommand3.java │ │ ├── MyCommand4.java │ │ ├── CommandHandlingApplication.java │ │ ├── CommandHandlingTestSliceTest.java │ │ └── CommandHandlingConfiguration.java │ │ └── DummyConfiguration.java └── build.gradle.kts ├── gradle.properties ├── framework-spring-boot-autoconfigure ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── opencqrs │ │ │ │ └── framework │ │ │ │ ├── transaction │ │ │ │ ├── package-info.java │ │ │ │ ├── TransactionOperationsAdapter.java │ │ │ │ ├── NoTransactionOperationsAdapter.java │ │ │ │ └── SpringTransactionOperationsAdapter.java │ │ │ │ ├── reflection │ │ │ │ ├── package-info.java │ │ │ │ ├── AutowiredParameter.java │ │ │ │ └── AutowiredParameterResolver.java │ │ │ │ ├── eventhandler │ │ │ │ ├── EventHandlingProcessorLifecycleController.java │ │ │ │ ├── EventHandlingProcessorLifecycleRegistration.java │ │ │ │ ├── SmartLifecycleEventHandlingProcessorLifecycleController.java │ │ │ │ └── LeaderElectionEventHandlingProcessorLifecycleController.java │ │ │ │ ├── upcaster │ │ │ │ └── EventUpcasterAutoConfiguration.java │ │ │ │ ├── command │ │ │ │ ├── CommandHandlerConfiguration.java │ │ │ │ ├── cache │ │ │ │ │ └── CommandHandlingCacheProperties.java │ │ │ │ ├── StateRebuilding.java │ │ │ │ └── CommandHandling.java │ │ │ │ ├── metadata │ │ │ │ └── MetaDataPropagationProperties.java │ │ │ │ ├── types │ │ │ │ └── ClassNameEventTypeResolverAutoConfiguration.java │ │ │ │ ├── serialization │ │ │ │ └── JacksonEventDataMarshallerAutoConfiguration.java │ │ │ │ └── persistence │ │ │ │ └── EventPersistenceAutoConfiguration.java │ │ └── resources │ │ │ └── META-INF │ │ │ └── spring │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ └── test │ │ ├── resources │ │ └── schema.sql │ │ └── java │ │ └── com │ │ └── opencqrs │ │ └── framework │ │ └── TestApplication.java └── build.gradle.kts ├── esdb-client-spring-boot-starter └── build.gradle.kts ├── example-application ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── opencqrs │ │ │ │ └── example │ │ │ │ ├── domain │ │ │ │ ├── book │ │ │ │ │ ├── Page.java │ │ │ │ │ ├── api │ │ │ │ │ │ ├── BookLentEvent.java │ │ │ │ │ │ ├── BookPurchasedEvent.java │ │ │ │ │ │ ├── BookReturnedEvent.java │ │ │ │ │ │ ├── BookNotLentException.java │ │ │ │ │ │ ├── BookAlreadyLentException.java │ │ │ │ │ │ ├── BookCommand.java │ │ │ │ │ │ ├── BookNeedsReplacementException.java │ │ │ │ │ │ ├── ReturnBookCommand.java │ │ │ │ │ │ ├── MarkBookPageDamagedCommand.java │ │ │ │ │ │ ├── BookPageCommand.java │ │ │ │ │ │ ├── BorrowBookCommand.java │ │ │ │ │ │ ├── PurchaseBookCommand.java │ │ │ │ │ │ └── BookPageDamagedEvent.java │ │ │ │ │ ├── Book.java │ │ │ │ │ ├── PageHandling.java │ │ │ │ │ └── BookHandling.java │ │ │ │ └── reader │ │ │ │ │ ├── api │ │ │ │ │ ├── ReaderRegisteredEvent.java │ │ │ │ │ ├── NoSuchReaderException.java │ │ │ │ │ ├── ReaderCommand.java │ │ │ │ │ └── RegisterReaderCommand.java │ │ │ │ │ └── ReaderHandling.java │ │ │ │ ├── projection │ │ │ │ ├── book │ │ │ │ │ └── verifier │ │ │ │ │ │ ├── BookRepository.java │ │ │ │ │ │ ├── BookEntity.java │ │ │ │ │ │ ├── BookVerifying.java │ │ │ │ │ │ └── BookVerifier.java │ │ │ │ ├── reader │ │ │ │ │ ├── ReaderRepository.java │ │ │ │ │ ├── ReaderEntity.java │ │ │ │ │ └── ReaderProjector.java │ │ │ │ └── statistics │ │ │ │ │ ├── StatisticsHandling.java │ │ │ │ │ ├── EventStatisticsProjector.java │ │ │ │ │ └── BookStatisticsProjector.java │ │ │ │ ├── LibraryApplication.java │ │ │ │ ├── rest │ │ │ │ ├── ReaderController.java │ │ │ │ ├── BookController.java │ │ │ │ └── ExceptionControllerAdvice.java │ │ │ │ └── configuration │ │ │ │ └── CqrsConfiguration.java │ │ └── resources │ │ │ ├── schema.sql │ │ │ └── application.yml │ └── test │ │ └── java │ │ └── com │ │ └── opencqrs │ │ └── example │ │ └── domain │ │ ├── book │ │ └── PageHandlingTest.java │ │ └── reader │ │ └── ReaderHandlingTest.java ├── nginx.conf ├── docker-compose.yml └── build.gradle.kts ├── framework-spring-boot-starter └── build.gradle.kts ├── esdb-client-spring-boot-autoconfigure ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── spring │ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── java │ │ │ └── com │ │ │ └── opencqrs │ │ │ └── esdb │ │ │ └── client │ │ │ ├── EsdbHealthIndicator.java │ │ │ ├── EsdbProperties.java │ │ │ ├── JacksonMarshallerAutoConfiguration.java │ │ │ ├── EsdbHealthContributorAutoConfiguration.java │ │ │ └── EsdbClientAutoConfiguration.java │ └── test │ │ └── java │ │ └── com │ │ └── opencqrs │ │ └── esdb │ │ └── client │ │ ├── TestApplication.java │ │ └── EsdbHealthIndicatorTest.java └── build.gradle.kts ├── jreleaser.yml ├── flake.nix ├── settings.gradle.kts ├── MAINTAINER.md ├── .github └── workflows │ ├── documentation.yml │ ├── release.yml │ └── qa.yml └── flake.lock /mkdocs/docs/concepts/cqrs/index.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mkdocs/docs/CNAME: -------------------------------------------------------------------------------- 1 | docs.opencqrs.com 2 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use_flake 2 | source_env ../.envrc.local 3 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-cqrs/opencqrs/HEAD/banner.png -------------------------------------------------------------------------------- /mkdocs/docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-cqrs/opencqrs/HEAD/mkdocs/docs/images/logo.png -------------------------------------------------------------------------------- /mkdocs/docs/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-cqrs/opencqrs/HEAD/mkdocs/docs/images/favicon.png -------------------------------------------------------------------------------- /SETUP.md: -------------------------------------------------------------------------------- 1 | # IntelliJ IDEA 2 | 3 | 1. install plugin "palantir-java-format" 4 | 2. enable plugin in options after restart -------------------------------------------------------------------------------- /esdb-client/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | esdb: 2 | server: 3 | uri: http://unknown 4 | api-token: not-used -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-cqrs/opencqrs/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /mkdocs/docs/tutorials/01_setup/intellij_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-cqrs/opencqrs/HEAD/mkdocs/docs/tutorials/01_setup/intellij_01.png -------------------------------------------------------------------------------- /mkdocs/docs/tutorials/01_setup/intellij_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-cqrs/opencqrs/HEAD/mkdocs/docs/tutorials/01_setup/intellij_02.png -------------------------------------------------------------------------------- /mkdocs/docs/tutorials/01_setup/intellij_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-cqrs/opencqrs/HEAD/mkdocs/docs/tutorials/01_setup/intellij_03.png -------------------------------------------------------------------------------- /esdb-client/src/main/java/com/opencqrs/esdb/client/package-info.java: -------------------------------------------------------------------------------- 1 | /** Base package for the ESDB client. */ 2 | package com.opencqrs.esdb.client; 3 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/package-info.java: -------------------------------------------------------------------------------- 1 | /** Base package for CQRS framework components. */ 2 | package com.opencqrs.framework; 3 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/command/package-info.java: -------------------------------------------------------------------------------- 1 | /** Components for command handling. */ 2 | package com.opencqrs.framework.command; 3 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/metadata/package-info.java: -------------------------------------------------------------------------------- 1 | /** Contains meta-data related classes. */ 2 | package com.opencqrs.framework.metadata; 3 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/types/package-info.java: -------------------------------------------------------------------------------- 1 | /** Contains event type resolution components. */ 2 | package com.opencqrs.framework.types; 3 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/upcaster/package-info.java: -------------------------------------------------------------------------------- 1 | /** Contains event upcasting components. */ 2 | package com.opencqrs.framework.upcaster; 3 | -------------------------------------------------------------------------------- /mkdocs/docs/tutorials/01_setup/spring_initializr_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-cqrs/opencqrs/HEAD/mkdocs/docs/tutorials/01_setup/spring_initializr_01.png -------------------------------------------------------------------------------- /esdb-client/src/main/java/com/opencqrs/esdb/client/jackson/package-info.java: -------------------------------------------------------------------------------- 1 | /** Contains Jackson specific components. */ 2 | package com.opencqrs.esdb.client.jackson; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .direnv 2 | .gradle 3 | .idea 4 | *.iml 5 | build 6 | out 7 | */build 8 | */out 9 | .cache 10 | mkdocs/site 11 | mkdocs/.cache 12 | mkdocs/__pycache__/ 13 | -------------------------------------------------------------------------------- /framework-test/src/main/resources/META-INF/spring/com.opencqrs.framework.command.CommandHandlingTest.imports: -------------------------------------------------------------------------------- 1 | com.opencqrs.framework.command.CommandHandlingTestAutoConfiguration -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.daemon=false 2 | org.gradle.parallel=true 3 | org.gradle.configureondemand=true 4 | org.gradle.jvmargs=-Xms1024m -Xmx2048m -Dfile.encoding=UTF-8 5 | -------------------------------------------------------------------------------- /framework-test/src/test/java/com/opencqrs/framework/State.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework; 3 | 4 | public record State() {} 5 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/eventhandler/package-info.java: -------------------------------------------------------------------------------- 1 | /** Contains event handler and processing components. */ 2 | package com.opencqrs.framework.eventhandler; 3 | -------------------------------------------------------------------------------- /framework/src/test/java/com/opencqrs/framework/MyEvent.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework; 3 | 4 | public interface MyEvent {} 5 | -------------------------------------------------------------------------------- /framework-test/src/test/java/com/opencqrs/framework/MyEvent.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework; 3 | 4 | public record MyEvent() {} 5 | -------------------------------------------------------------------------------- /framework/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring.application.name: test-app 2 | 3 | cqrs.event-handling: 4 | standard: 5 | retry: 6 | policy: fixed 7 | initial-interval: 5 -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/command/cache/package-info.java: -------------------------------------------------------------------------------- 1 | /** Contains cache components supporting command execution. */ 2 | package com.opencqrs.framework.command.cache; 3 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/transaction/package-info.java: -------------------------------------------------------------------------------- 1 | /** Transaction support for event handlers. */ 2 | package com.opencqrs.framework.transaction; 3 | -------------------------------------------------------------------------------- /esdb-client-spring-boot-starter/build.gradle.kts: -------------------------------------------------------------------------------- 1 | description = "Spring Boot starter for the ESDB client SDK" 2 | 3 | dependencies { 4 | api(project(":esdb-client-spring-boot-autoconfigure")) 5 | } 6 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/persistence/package-info.java: -------------------------------------------------------------------------------- 1 | /** Persistence layer for abstracting event consumption and publication. */ 2 | package com.opencqrs.framework.persistence; 3 | -------------------------------------------------------------------------------- /framework/src/test/java/com/opencqrs/framework/Book.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework; 3 | 4 | public record Book(String isbn, Boolean lent) {} 5 | -------------------------------------------------------------------------------- /framework/src/test/java/com/opencqrs/framework/BookBorrowedEvent.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework; 3 | 4 | public record BookBorrowedEvent() {} 5 | -------------------------------------------------------------------------------- /framework/src/test/java/com/opencqrs/framework/BookReturnedEvent.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework; 3 | 4 | public record BookReturnedEvent() {} 5 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/client/package-info.java: -------------------------------------------------------------------------------- 1 | /** Internal helper classes for accessing the {@link com.opencqrs.esdb.client.EsdbClient}. */ 2 | package com.opencqrs.framework.client; 3 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/eventhandler/progress/package-info.java: -------------------------------------------------------------------------------- 1 | /** Contains progress tracking components for event handling. */ 2 | package com.opencqrs.framework.eventhandler.progress; 3 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/reflection/package-info.java: -------------------------------------------------------------------------------- 1 | /** Contains internal helper classes to support reflection. */ 2 | package com.opencqrs.framework.reflection; 3 | -------------------------------------------------------------------------------- /framework/src/test/java/com/opencqrs/framework/BookPageDamagedEvent.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework; 3 | 4 | public record BookPageDamagedEvent(int page) {} 5 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/eventhandler/partitioning/package-info.java: -------------------------------------------------------------------------------- 1 | /** Contains components for dealing with partitioned event handling. */ 2 | package com.opencqrs.framework.eventhandler.partitioning; 3 | -------------------------------------------------------------------------------- /framework/src/test/java/com/opencqrs/framework/BookAddedEvent.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework; 3 | 4 | public record BookAddedEvent(String isbn) implements MyEvent {} 5 | -------------------------------------------------------------------------------- /esdb-client/src/main/java/com/opencqrs/esdb/client/eventql/package-info.java: -------------------------------------------------------------------------------- 1 | /** Contains classes related to EventQL. */ 2 | package com.opencqrs.esdb.client.eventql; 3 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/book/Page.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.book; 3 | 4 | public record Page(Long page, boolean damaged) {} 5 | -------------------------------------------------------------------------------- /framework-spring-boot-starter/build.gradle.kts: -------------------------------------------------------------------------------- 1 | description = "Spring Boot Starter for OpenCQRS framework" 2 | 3 | dependencies { 4 | api(project(":esdb-client-spring-boot-starter")) 5 | api(project(":framework-spring-boot-autoconfigure")) 6 | } 7 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/book/api/BookLentEvent.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.book.api; 3 | 4 | import java.util.UUID; 5 | 6 | public record BookLentEvent(String isbn, UUID reader) {} 7 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/book/api/BookPurchasedEvent.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.book.api; 3 | 4 | public record BookPurchasedEvent(String isbn, String author, String title, long numPages) {} 5 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/book/api/BookReturnedEvent.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.book.api; 3 | 4 | import java.util.UUID; 5 | 6 | public record BookReturnedEvent(String isbn, UUID reader) {} 7 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/reader/api/ReaderRegisteredEvent.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.reader.api; 3 | 4 | import java.util.UUID; 5 | 6 | public record ReaderRegisteredEvent(UUID id, String name) {} 7 | -------------------------------------------------------------------------------- /framework/src/test/java/com/opencqrs/framework/TestApplication.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework; 3 | 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class TestApplication {} 8 | -------------------------------------------------------------------------------- /esdb-client/src/test/java/com/opencqrs/esdb/client/TestApplication.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client; 3 | 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class TestApplication {} 8 | -------------------------------------------------------------------------------- /example-application/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | 3 | events { 4 | worker_connections 1000; 5 | } 6 | http { 7 | server { 8 | listen 8080; 9 | location / { 10 | proxy_pass http://application:8080; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/test/resources/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS TEST_PROGRESS ( 2 | GROUP_KEY VARCHAR(100) NOT NULL, 3 | PARTITION_ID BIGINT NOT NULL, 4 | EVENT_ID VARCHAR(100) NOT NULL, 5 | constraint TEST_PROGRESS_PK primary key (GROUP_KEY, PARTITION_ID) 6 | ); 7 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/serialization/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains components for {@link com.opencqrs.esdb.client.EventCandidate#data()} or 3 | * {@link com.opencqrs.esdb.client.Event#data()} s erialization and deserialization. 4 | */ 5 | package com.opencqrs.framework.serialization; 6 | -------------------------------------------------------------------------------- /esdb-client-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | com.opencqrs.esdb.client.EsdbClientAutoConfiguration 2 | com.opencqrs.esdb.client.EsdbHealthContributorAutoConfiguration 3 | com.opencqrs.esdb.client.JacksonMarshallerAutoConfiguration 4 | -------------------------------------------------------------------------------- /esdb-client-spring-boot-autoconfigure/src/test/java/com/opencqrs/esdb/client/TestApplication.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client; 3 | 4 | import org.springframework.boot.SpringBootConfiguration; 5 | 6 | @SpringBootConfiguration 7 | public class TestApplication {} 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-rc-1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/test/java/com/opencqrs/framework/TestApplication.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework; 3 | 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class TestApplication {} 8 | -------------------------------------------------------------------------------- /framework-test/src/test/java/com/opencqrs/framework/command/MyCommand1.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | public class MyCommand1 implements Command { 5 | @Override 6 | public String getSubject() { 7 | return "irrelevant"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /framework-test/src/test/java/com/opencqrs/framework/command/MyCommand2.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | public class MyCommand2 implements Command { 5 | @Override 6 | public String getSubject() { 7 | return "irrelevant"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /framework-test/src/test/java/com/opencqrs/framework/command/MyCommand3.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | public class MyCommand3 implements Command { 5 | @Override 6 | public String getSubject() { 7 | return "irrelevant"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /framework-test/src/test/java/com/opencqrs/framework/command/MyCommand4.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | public class MyCommand4 implements Command { 5 | @Override 6 | public String getSubject() { 7 | return "irrelevant"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /framework-test/src/test/java/com/opencqrs/framework/command/CommandHandlingApplication.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class CommandHandlingApplication {} 8 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/projection/book/verifier/BookRepository.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.projection.book.verifier; 3 | 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | public interface BookRepository extends CrudRepository {} 7 | -------------------------------------------------------------------------------- /esdb-client/src/main/java/com/opencqrs/esdb/client/eventql/EventQuery.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client.eventql; 3 | 4 | /** 5 | * Encapsulates an EventQL query. 6 | * 7 | * @param queryString the EventQL query as string 8 | * @see EventQueryBuilder 9 | */ 10 | public record EventQuery(String queryString) {} 11 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/projection/reader/ReaderRepository.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.projection.reader; 3 | 4 | import java.util.UUID; 5 | import org.springframework.data.repository.CrudRepository; 6 | 7 | public interface ReaderRepository extends CrudRepository {} 8 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/projection/reader/ReaderEntity.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.projection.reader; 3 | 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.Id; 6 | import java.util.UUID; 7 | 8 | @Entity 9 | public class ReaderEntity { 10 | @Id 11 | public UUID id; 12 | } 13 | -------------------------------------------------------------------------------- /framework/src/test/java/com/opencqrs/framework/AddBookCommand.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework; 3 | 4 | import com.opencqrs.framework.command.Command; 5 | 6 | public record AddBookCommand(String isbn) implements Command { 7 | 8 | @Override 9 | public String getSubject() { 10 | return "/books/" + isbn; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /framework/src/test/java/com/opencqrs/framework/BorrowBookCommand.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework; 3 | 4 | import com.opencqrs.framework.command.Command; 5 | 6 | public record BorrowBookCommand(String isbn) implements Command { 7 | 8 | @Override 9 | public String getSubject() { 10 | return "/books/" + isbn; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /framework/src/test/java/com/opencqrs/framework/ReturnBookCommand.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework; 3 | 4 | import com.opencqrs.framework.command.Command; 5 | 6 | public record ReturnBookCommand(String isbn) implements Command { 7 | 8 | @Override 9 | public String getSubject() { 10 | return "/books/" + isbn; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /framework/src/test/java/com/opencqrs/framework/ArchiveBookCommand.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework; 3 | 4 | import com.opencqrs.framework.command.Command; 5 | 6 | public record ArchiveBookCommand(String isbn) implements Command { 7 | 8 | @Override 9 | public String getSubject() { 10 | return "/books/" + isbn; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/book/api/BookNotLentException.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.book.api; 3 | 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.web.bind.annotation.ResponseStatus; 6 | 7 | @ResponseStatus(HttpStatus.CONFLICT) 8 | public class BookNotLentException extends RuntimeException {} 9 | -------------------------------------------------------------------------------- /mkdocs/docs/js/open_external_in_new_tab.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | const links = document.querySelectorAll("a[href^='http']"); 3 | links.forEach(function (link) { 4 | if (!link.href.includes(window.location.hostname)) { 5 | link.setAttribute("target", "_blank"); 6 | link.setAttribute("rel", "noopener"); 7 | } 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /mkdocs/includes/glossary.md: -------------------------------------------------------------------------------- 1 | *[CQRS]: Command Query Responsibility Segregation 2 | *[ES]: Event Sourcing 3 | *[ESDB]: EventSourcingDB 4 | *[IDE]: Integrated Development Environment 5 | *[JPA]: Jakarta Persistence API 6 | *[JDK]: Java Development Kit 7 | *[JVM]: Java Virtual Machine 8 | *[SDK]: Software Development Kit 9 | *[SQL]: Structured Query Language 10 | *[DDD]: Domain Driven Design 11 | *[DTO]: Data Transfer Object 12 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/book/api/BookAlreadyLentException.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.book.api; 3 | 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.web.bind.annotation.ResponseStatus; 6 | 7 | @ResponseStatus(HttpStatus.CONFLICT) 8 | public class BookAlreadyLentException extends RuntimeException {} 9 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/reader/api/NoSuchReaderException.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.reader.api; 3 | 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.web.bind.annotation.ResponseStatus; 6 | 7 | @ResponseStatus(HttpStatus.NOT_FOUND) 8 | public class NoSuchReaderException extends RuntimeException {} 9 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/book/api/BookCommand.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.book.api; 3 | 4 | import com.opencqrs.framework.command.Command; 5 | 6 | public interface BookCommand extends Command { 7 | 8 | String isbn(); 9 | 10 | @Override 11 | default String getSubject() { 12 | return "/book/" + isbn(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/book/api/BookNeedsReplacementException.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.book.api; 3 | 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.web.bind.annotation.ResponseStatus; 6 | 7 | @ResponseStatus(HttpStatus.FAILED_DEPENDENCY) 8 | public class BookNeedsReplacementException extends RuntimeException {} 9 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/persistence/EventSource.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.persistence; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | 6 | /** 7 | * Encapsulates a configurable {@link Event#source()} for event publication. 8 | * 9 | * @param source the source identifier to be used when publishing events 10 | */ 11 | public record EventSource(String source) {} 12 | -------------------------------------------------------------------------------- /framework-test/src/test/java/com/opencqrs/framework/DummyConfiguration.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework; 3 | 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | @Configuration 8 | public class DummyConfiguration { 9 | 10 | @Bean 11 | public Object notInitialized() { 12 | return new Object(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/book/api/ReturnBookCommand.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.book.api; 3 | 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record ReturnBookCommand(@NotBlank String isbn) implements BookCommand { 7 | 8 | @Override 9 | public SubjectCondition getSubjectCondition() { 10 | return SubjectCondition.EXISTS; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/reader/api/ReaderCommand.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.reader.api; 3 | 4 | import com.opencqrs.framework.command.Command; 5 | import java.util.UUID; 6 | 7 | public interface ReaderCommand extends Command { 8 | 9 | UUID id(); 10 | 11 | @Override 12 | default String getSubject() { 13 | return "/reader/" + id(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/book/api/MarkBookPageDamagedCommand.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.book.api; 3 | 4 | import java.util.UUID; 5 | 6 | public record MarkBookPageDamagedCommand(String isbn, Long page, UUID reader) implements BookPageCommand { 7 | 8 | @Override 9 | public SubjectCondition getSubjectCondition() { 10 | return SubjectCondition.PRISTINE; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/book/api/BookPageCommand.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.book.api; 3 | 4 | import com.opencqrs.framework.command.Command; 5 | 6 | public interface BookPageCommand extends Command { 7 | 8 | String isbn(); 9 | 10 | Long page(); 11 | 12 | @Override 13 | default String getSubject() { 14 | return "/book/" + isbn() + "/page/" + page(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/LibraryApplication.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example; 3 | 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | @SpringBootApplication 8 | public class LibraryApplication { 9 | 10 | public static void main(String[] args) { 11 | SpringApplication.run(LibraryApplication.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /esdb-client/src/main/java/com/opencqrs/esdb/client/IdUtil.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client; 3 | 4 | /** Static helper methods related to {@link Event#id()}. */ 5 | public final class IdUtil { 6 | 7 | /** 8 | * Converts an {@link Event#id()} to a number. 9 | * 10 | * @param id the event id 11 | * @return the long number 12 | */ 13 | public static Long fromEventId(String id) { 14 | return Long.valueOf(id); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/command/CommandSubjectDoesNotExistException.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | /** Exception thrown if {@link Command.SubjectCondition#EXISTS} is violated. */ 5 | public class CommandSubjectDoesNotExistException extends CommandSubjectConditionViolatedException { 6 | 7 | public CommandSubjectDoesNotExistException(String message, Command command) { 8 | super(message, command, Command.SubjectCondition.EXISTS); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /mkdocs/docs/reference/test_support/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Test Support 3 | --- 4 | 5 | {{ custom.framework_name }} offers built-in test support for the following aspects of CQRS applications: 6 | 7 | * [Command Handling Tests](command_handling_test_fixture/index.md) support test creation and execution covering the complete [command handling workflow](../extension_points/index.md#command-handling) covering [command handlers](../extension_points/command_handler/index.md) as well as [state rebuilding handlers](../extension_points/state_rebuilding_handler/index.md) 8 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/command/CommandSubjectAlreadyExistsException.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | /** Exception thrown if {@link Command.SubjectCondition#PRISTINE} is violated. */ 5 | public class CommandSubjectAlreadyExistsException extends CommandSubjectConditionViolatedException { 6 | 7 | public CommandSubjectAlreadyExistsException(String message, Command command) { 8 | super(message, command, Command.SubjectCondition.PRISTINE); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example-application/src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS EVENTHANDLER_LOCK ( 2 | LOCK_KEY CHAR(36) NOT NULL, 3 | REGION VARCHAR(100) NOT NULL, 4 | CLIENT_ID CHAR(36), 5 | CREATED_DATE TIMESTAMP NOT NULL, 6 | constraint EVENTHANDLER_LOCK_PK primary key (LOCK_KEY, REGION) 7 | ); 8 | 9 | CREATE TABLE IF NOT EXISTS EVENTHANDLER_PROGRESS ( 10 | GROUP_KEY VARCHAR(100) NOT NULL, 11 | PARTITION_ID BIGINT NOT NULL, 12 | EVENT_ID VARCHAR(100) NOT NULL, 13 | constraint EVENTHANDLER_PROGRESS_PK primary key (GROUP_KEY, PARTITION_ID) 14 | ); -------------------------------------------------------------------------------- /framework-test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | description = "OpenCQRS Framework Test Support" 2 | 3 | dependencies { 4 | api(project(":framework")) 5 | implementation("org.springframework.boot:spring-boot-starter-test") 6 | 7 | implementation(project(":framework-spring-boot-autoconfigure")) 8 | testImplementation(project(":framework-spring-boot-starter")) 9 | testImplementation("com.fasterxml.jackson.core:jackson-databind") 10 | // https://github.com/gradle/gradle/issues/33950 11 | testRuntimeOnly("org.junit.platform:junit-platform-launcher") 12 | } 13 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/book/api/BorrowBookCommand.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.book.api; 3 | 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | import java.util.UUID; 7 | 8 | public record BorrowBookCommand(@NotBlank String isbn, @NotNull UUID reader) implements BookCommand { 9 | 10 | @Override 11 | public SubjectCondition getSubjectCondition() { 12 | return SubjectCondition.EXISTS; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/projection/book/verifier/BookEntity.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.projection.book.verifier; 3 | 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.Id; 6 | 7 | @Entity 8 | public class BookEntity { 9 | 10 | @Id 11 | public String isbn; 12 | 13 | public Long pages; 14 | 15 | public BookEntity() {} 16 | 17 | public BookEntity(String isbn, Long pages) { 18 | this.isbn = isbn; 19 | this.pages = pages; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /esdb-client/src/main/java/com/opencqrs/esdb/client/Health.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client; 3 | 4 | import java.util.Map; 5 | 6 | /** 7 | * Represents the current health status of the configured {@linkplain EsdbClient ESDB}. 8 | * 9 | * @param status the overall status 10 | * @param checks DB internal checks status 11 | */ 12 | public record Health(Status status, Map checks) { 13 | 14 | /** Health status. */ 15 | public enum Status { 16 | pass, 17 | warn, 18 | fail, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/reader/api/RegisterReaderCommand.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.reader.api; 3 | 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | import java.util.UUID; 7 | 8 | public record RegisterReaderCommand(@NotNull UUID id, @NotBlank String name) implements ReaderCommand { 9 | 10 | @Override 11 | public SubjectCondition getSubjectCondition() { 12 | return SubjectCondition.PRISTINE; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/transaction/TransactionOperationsAdapter.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.transaction; 3 | 4 | /** 5 | * Internal interface encapsulating {@link org.springframework.transaction.annotation.Transactional} method execution. 6 | */ 7 | public interface TransactionOperationsAdapter { 8 | 9 | /** 10 | * Executes the given runnable with transactional semantics. 11 | * 12 | * @param runnable the runnable to execute 13 | */ 14 | void execute(Runnable runnable); 15 | } 16 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/projection/book/verifier/BookVerifying.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.projection.book.verifier; 3 | 4 | import com.opencqrs.framework.eventhandler.EventHandling; 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @EventHandling("book-verifier") 13 | public @interface BookVerifying {} 14 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/book/api/PurchaseBookCommand.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.book.api; 3 | 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | 7 | public record PurchaseBookCommand( 8 | @NotBlank String isbn, @NotBlank String author, @NotBlank String title, @NotNull long numPages) 9 | implements BookCommand { 10 | 11 | @Override 12 | public SubjectCondition getSubjectCondition() { 13 | return SubjectCondition.PRISTINE; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/projection/statistics/StatisticsHandling.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.projection.statistics; 3 | 4 | import com.opencqrs.framework.eventhandler.EventHandling; 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @EventHandling("statistics") 13 | public @interface StatisticsHandling {} 14 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/client/ConcurrencyException.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.client; 3 | 4 | import com.opencqrs.framework.CqrsFrameworkException; 5 | 6 | /** 7 | * Exception class representing concurrency errors, usually caused by violated preconditions, when publishing events. 8 | * 9 | * @see ClientRequestErrorMapper 10 | */ 11 | public class ConcurrencyException extends CqrsFrameworkException.TransientException { 12 | 13 | public ConcurrencyException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/eventhandler/partitioning/PerSubjectEventSequenceResolver.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.eventhandler.partitioning; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | 6 | /** 7 | * {@link EventSequenceResolver.ForRawEvent} implementation which uses {@link Event#subject()} as the event sequence 8 | * identifier. 9 | */ 10 | public class PerSubjectEventSequenceResolver implements EventSequenceResolver.ForRawEvent { 11 | 12 | @Override 13 | public String sequenceIdFor(Event rawEvent) { 14 | return rawEvent.subject(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/eventhandler/partitioning/NoEventSequenceResolver.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.eventhandler.partitioning; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | 6 | /** 7 | * {@link EventSequenceResolver.ForRawEvent} implementation to be used if no event sequencing is 8 | * required, i.e. all events may be processed in any order. 9 | */ 10 | public class NoEventSequenceResolver implements EventSequenceResolver.ForRawEvent { 11 | 12 | @Override 13 | public String sequenceIdFor(Event rawEvent) { 14 | return rawEvent.id(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /esdb-client/src/main/java/com/opencqrs/esdb/client/eventql/EventQueryBuilder.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client.eventql; 3 | 4 | /** Builder for {@link EventQuery} instances */ 5 | public class EventQueryBuilder { 6 | 7 | private EventQueryBuilder() {} 8 | 9 | /** 10 | * Constructs an {@link EventQuery} from a query string without further checks. 11 | * 12 | * @param queryString the EventQL query represented as string 13 | * @return the created {@link EventQuery} 14 | */ 15 | public static EventQuery fromEventQlString(String queryString) { 16 | return new EventQuery(queryString); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/client/ClientInterruptedException.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.client; 3 | 4 | import com.opencqrs.esdb.client.ClientException; 5 | import com.opencqrs.framework.CqrsFrameworkException; 6 | 7 | /** 8 | * Exception class representing thread interruption signalled by {@link ClientException.InterruptedException}. 9 | * 10 | * @see ClientRequestErrorMapper 11 | */ 12 | public class ClientInterruptedException extends CqrsFrameworkException.TransientException { 13 | 14 | public ClientInterruptedException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/command/cache/NoStateRebuildingCache.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command.cache; 3 | 4 | import java.util.Map; 5 | import java.util.function.Function; 6 | 7 | /** {@link StateRebuildingCache} implementation that does not cache anything. */ 8 | public final class NoStateRebuildingCache implements StateRebuildingCache { 9 | 10 | @Override 11 | public CacheValue fetchAndMerge(CacheKey key, Function, CacheValue> mergeFunction) { 12 | var noCachedValue = new CacheValue(null, null, Map.of()); 13 | return mergeFunction.apply(noCachedValue); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/types/EventTypeResolutionException.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.types; 3 | 4 | import com.opencqrs.framework.CqrsFrameworkException; 5 | 6 | /** 7 | * {@link CqrsFrameworkException.NonTransientException} exception capturing an {@link EventTypeResolver} resolution 8 | * error. 9 | */ 10 | public class EventTypeResolutionException extends CqrsFrameworkException.NonTransientException { 11 | 12 | public EventTypeResolutionException(String message) { 13 | super(message); 14 | } 15 | 16 | public EventTypeResolutionException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/command/SourcingMode.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | import com.opencqrs.esdb.client.Option; 5 | 6 | /** The type of sourcing affecting which events will be sourced for {@link CommandHandler}s. */ 7 | public enum SourcingMode { 8 | 9 | /** No events will be fetched for the {@link Command#getSubject()}. */ 10 | NONE, 11 | 12 | /** Events will be fetched for the {@link Command#getSubject()} non-recursively. */ 13 | LOCAL, 14 | 15 | /** 16 | * Events will be fetched for the {@link Command#getSubject()} recursively. 17 | * 18 | * @see Option.Recursive 19 | */ 20 | RECURSIVE 21 | } 22 | -------------------------------------------------------------------------------- /jreleaser.yml: -------------------------------------------------------------------------------- 1 | project: 2 | name: opencqrs 3 | languages: 4 | java: 5 | groupId: com.opencqrs 6 | 7 | deploy: 8 | maven: 9 | mavenCentral: 10 | sonatype: 11 | active: ALWAYS 12 | sign: false 13 | url: https://central.sonatype.com/api/v1/publisher 14 | stagingRepositories: 15 | - esdb-client/build/staging-deploy 16 | - esdb-client-spring-boot-autoconfigure/build/staging-deploy 17 | - esdb-client-spring-boot-starter/build/staging-deploy 18 | - framework/build/staging-deploy 19 | - framework-test/build/staging-deploy 20 | - framework-spring-boot-autoconfigure/build/staging-deploy 21 | - framework-spring-boot-starter/build/staging-deploy 22 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/book/api/BookPageDamagedEvent.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.book.api; 3 | 4 | import com.fasterxml.jackson.annotation.JsonSubTypes; 5 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 6 | import java.util.UUID; 7 | 8 | @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", include = JsonTypeInfo.As.PROPERTY) 9 | @JsonSubTypes(@JsonSubTypes.Type(value = BookPageDamagedEvent.ByReader.class, name = "by-reader")) 10 | public sealed interface BookPageDamagedEvent { 11 | 12 | String isbn(); 13 | 14 | Long page(); 15 | 16 | record ByReader(String isbn, Long page, UUID reader) implements BookPageDamagedEvent {} 17 | } 18 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/transaction/NoTransactionOperationsAdapter.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.transaction; 3 | 4 | import com.opencqrs.framework.eventhandler.EventHandling; 5 | 6 | /** 7 | * Implementation of {@link TransactionOperationsAdapter} used if Spring TX is not available on the class-path or 8 | * {@link EventHandling} methods have not been annotated using 9 | * {@link org.springframework.transaction.annotation.Transactional}. 10 | */ 11 | public class NoTransactionOperationsAdapter implements TransactionOperationsAdapter { 12 | 13 | @Override 14 | public void execute(Runnable runnable) { 15 | runnable.run(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/command/StateRebuildingHandlerDefinition.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | /** 5 | * {@link StateRebuildingHandler} definition suitable for event-sourcing an instance prior to {@link CommandHandler 6 | * command handling}. 7 | * 8 | * @param instanceClass the instance type being event-sourced 9 | * @param eventClass the event type to be applied to any prior instance (state) 10 | * @param handler the actual state rebuilding handler 11 | * @param the instance type 12 | * @param the event type to be sourced 13 | */ 14 | public record StateRebuildingHandlerDefinition( 15 | Class instanceClass, Class eventClass, StateRebuildingHandler handler) {} 16 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/eventhandler/partitioning/PartitionKeyResolver.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.eventhandler.partitioning; 3 | 4 | /** 5 | * Interface for implementations able to consistently derive a numeric partition key from an event sequence identifier. 6 | * 7 | * @see EventSequenceResolver 8 | */ 9 | @FunctionalInterface 10 | public interface PartitionKeyResolver { 11 | 12 | /** 13 | * Deterministically resolves a partition number for the given sequence identifier. 14 | * 15 | * @param sequenceId the event sequence identifier, as derived from {@link EventSequenceResolver} implementations 16 | * @return the partition number 17 | */ 18 | long resolve(String sequenceId); 19 | } 20 | -------------------------------------------------------------------------------- /example-application/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring.application.name: library 2 | 3 | 4 | esdb: 5 | server: 6 | uri: http://localhost:3000 7 | api-token: secret 8 | 9 | opencqrs: 10 | event-handling: 11 | standard: 12 | life-cycle.partitions: 2 13 | groups: 14 | logging: 15 | sequence.resolution: no-sequence 16 | retry.policy: none 17 | statistics: 18 | life-cycle: 19 | partitions: 1 20 | controller: application-context 21 | progress.tracking: in-memory 22 | metadata: 23 | propagation: 24 | keys: 25 | - "request-uri" 26 | command-handling: 27 | cache: 28 | type: in_memory 29 | 30 | management: 31 | endpoint: 32 | health: 33 | show-components: always 34 | #show-details: always 35 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | com.opencqrs.framework.command.CommandHandlingAnnotationProcessingAutoConfiguration 2 | com.opencqrs.framework.command.CommandRouterAutoConfiguration 3 | com.opencqrs.framework.command.StateRebuildingAnnotationProcessingAutoConfiguration 4 | com.opencqrs.framework.eventhandler.EventHandlingAnnotationProcessingAutoConfiguration 5 | com.opencqrs.framework.eventhandler.EventHandlingProcessorAutoConfiguration 6 | com.opencqrs.framework.persistence.EventPersistenceAutoConfiguration 7 | com.opencqrs.framework.serialization.JacksonEventDataMarshallerAutoConfiguration 8 | com.opencqrs.framework.types.ClassNameEventTypeResolverAutoConfiguration 9 | com.opencqrs.framework.upcaster.EventUpcasterAutoConfiguration 10 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/eventhandler/EventHandlingProcessorLifecycleController.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.eventhandler; 3 | 4 | /** 5 | * Interface to be implemented by beans responsible for managing the life-cycle of an {@link EventHandlingProcessor} 6 | * bean. 7 | * 8 | * @see EventHandlingProcessor#start() 9 | * @see EventHandlingProcessor#stop() 10 | */ 11 | public interface EventHandlingProcessorLifecycleController { 12 | 13 | /** 14 | * States whether the associated {@link EventHandlingProcessor} is currently 15 | * {@linkplain EventHandlingProcessor#run()} running. 16 | * 17 | * @return {@code true} if it is running, {@code false} otherwise 18 | */ 19 | boolean isRunning(); 20 | } 21 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/reflection/AutowiredParameter.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.reflection; 3 | 4 | import java.lang.reflect.Parameter; 5 | 6 | /** 7 | * Represents an {@link org.springframework.beans.factory.annotation.Autowired} parameter within an annotated handler 8 | * definition. 9 | * 10 | * @param parameter the parameter identified as 11 | * {@linkplain org.springframework.beans.factory.annotation.ParameterResolutionDelegate#isAutowirable(Parameter, 12 | * int) autowirable} 13 | * @param index the position within its {@link java.lang.reflect.Method} 14 | * @param containingClass the containing class 15 | */ 16 | public record AutowiredParameter(Parameter parameter, int index, Class containingClass) {} 17 | -------------------------------------------------------------------------------- /mkdocs/docs/howto/README.md: -------------------------------------------------------------------------------- 1 | # Guides 2 | 3 | This section provides step-by-step guides that demonstrate how to apply the [concepts](../concepts/README.md) in practice. 4 | They focus on practical implementation details and best practices for working with the {{ custom.framework_name }}. 5 | 6 | The following guides are currently available: 7 | 8 | - [Registering Explicit Event Types](explicit_type_registration/index.md) – Learn how to declare and register event types so that the system can reliably identify, serialize, and deserialize them. 9 | - [Upcasting Events](upcasting_events/index.md) – Understand how to evolve existing events to new versions, ensuring compatibility as your domain and event schemas change over time. 10 | 11 | Additional guides will be added over time to cover more advanced usage patterns and common scenarios. 12 | -------------------------------------------------------------------------------- /mkdocs/docs/reference/core_components/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Core Components 3 | --- 4 | 5 | {{ custom.framework_name }} offers the following built-in core components for building CQRS applications: 6 | 7 | * [ESDB Client](esdb_client/index.md) provides an SDK for direct access to the {{ esdb_ref() }} 8 | * [Event Repository](event_repository/index.md) supports the mapping of Java classes to ESDB events and vice versa 9 | * [Command Router](command_router/index.md) provides support for command execution, including [reconstruction of write models](../../concepts/event_sourcing/index.md#reconstructing-the-write-model) and publication of new events 10 | * [Event Handling Processor](event_handling_processor/index.md) supports asynchronous event processing for [read model projection](../../concepts/event_sourcing/index.md#projecting-a-read-model) 11 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/eventhandler/EventHandlerDefinition.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.eventhandler; 3 | 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | 7 | /** 8 | * {@link EventHandler} definition suitable for being processed by an event processor. 9 | * 10 | * @param group group identifier for {@link EventHandler} belonging to the same processing group 11 | * @param eventClass the Java event type to be handled, may be {@link Object} to handle all events 12 | * @param handler the actual event handler 13 | * @param the generic Java event type 14 | */ 15 | public record EventHandlerDefinition( 16 | @NotBlank String group, @NotNull Class eventClass, @NotNull EventHandler handler) {} 17 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/upcaster/NoEventUpcaster.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.upcaster; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | import java.util.stream.Stream; 6 | 7 | /** 8 | * {@link EventUpcaster} implementation that drops an {@link Event} if the {@link Event#type()} matches the configured 9 | * type. 10 | */ 11 | public class NoEventUpcaster implements EventUpcaster { 12 | 13 | private final String type; 14 | 15 | public NoEventUpcaster(String type) { 16 | this.type = type; 17 | } 18 | 19 | @Override 20 | public boolean canUpcast(Event event) { 21 | return event.type().equals(type); 22 | } 23 | 24 | @Override 25 | public Stream upcast(Event event) { 26 | return Stream.empty(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/eventhandler/BackOff.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.eventhandler; 3 | 4 | /** Interface to be implemented by classes providing back off semantics, e.g. fixed or exponential back off. */ 5 | @FunctionalInterface 6 | public interface BackOff { 7 | 8 | /** 9 | * Starts a back off {@link Execution}. 10 | * 11 | * @return a newly started back off 12 | */ 13 | Execution start(); 14 | 15 | /** Encapsulates back off execution state. */ 16 | @FunctionalInterface 17 | interface Execution { 18 | 19 | /** 20 | * Yields the number of milliseconds to back off. 21 | * 22 | * @return number of milliseconds to back off, or {@code -1L} if back off is exhausted 23 | */ 24 | long next(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /esdb-client/build.gradle.kts: -------------------------------------------------------------------------------- 1 | description = "Client SDK for the EventSourcingDB" 2 | 3 | dependencies { 4 | compileOnly("jakarta.validation:jakarta.validation-api") 5 | implementation("com.fasterxml.jackson.core:jackson-databind") 6 | testImplementation(project(":esdb-client-spring-boot-starter")) 7 | testImplementation("org.springframework.boot:spring-boot-starter-validation") 8 | testImplementation("org.springframework.boot:spring-boot-starter-web") 9 | testImplementation("org.springframework.boot:spring-boot-starter-test") 10 | testImplementation("org.springframework.boot:spring-boot-testcontainers") 11 | testImplementation("org.testcontainers:junit-jupiter") 12 | testImplementation("org.awaitility:awaitility:4.3.0") 13 | // https://github.com/gradle/gradle/issues/33950 14 | testRuntimeOnly("org.junit.platform:junit-platform-launcher") 15 | } 16 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/book/Book.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.book; 3 | 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | import java.util.UUID; 7 | 8 | public record Book(String isbn, long numPages, Set damagedPages, Lending lending) { 9 | 10 | public sealed interface Lending { 11 | record Lent(UUID reader) implements Lending {} 12 | 13 | record Available() implements Lending {} 14 | } 15 | 16 | public Book with(Lending lending) { 17 | return new Book(isbn(), numPages(), damagedPages(), lending); 18 | } 19 | 20 | public Book withDamagedPage(Long page) { 21 | var damaged = new HashSet<>(damagedPages); 22 | damaged.add(page); 23 | 24 | return new Book(isbn(), numPages(), damaged, lending); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/upcaster/EventUpcasterAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.upcaster; 3 | 4 | import java.util.List; 5 | import org.springframework.boot.autoconfigure.AutoConfiguration; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 7 | import org.springframework.context.annotation.Bean; 8 | 9 | /** 10 | * {@linkplain org.springframework.boot.autoconfigure.EnableAutoConfiguration Auto-configuration} for 11 | * {@link EventUpcasters}. 12 | */ 13 | @AutoConfiguration 14 | public class EventUpcasterAutoConfiguration { 15 | 16 | @Bean 17 | @ConditionalOnMissingBean 18 | public EventUpcasters openCqrsEventUpcasterChain(List eventUpcasters) { 19 | return new EventUpcasters(eventUpcasters); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /mkdocs/docs/reference/README.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | This chapter provides a detailed overview of the framework’s internal structure, its core building blocks, and the extension mechanisms available to developers. 4 | 5 | The following sections are included: 6 | 7 | - [Modules](modules/index.md) describing the framework's internal structure 8 | - [Event Representations](events/index.md) describing how events are represented within {{ custom.framework_name }} 9 | - [Exception Handling](exceptions/index.md) describing how errors are handled and mapped to appropriate exceptions 10 | - [Core Components](core_components/index.md) describing the built-in components required to develop CQRS/ES applications 11 | - [Extension Points](extension_points/index.md) describing how to develop custom command handling logic and read model projections 12 | - [Test Support](test_support/index.md) describing the built-in test support to verify command handler logic 13 | -------------------------------------------------------------------------------- /esdb-client-spring-boot-autoconfigure/build.gradle.kts: -------------------------------------------------------------------------------- 1 | description = "Spring Boot auto configurations for the ESDB client SDK" 2 | 3 | dependencies { 4 | api(project(":esdb-client")) 5 | compileOnly("com.fasterxml.jackson.core:jackson-databind") 6 | compileOnly("org.springframework.boot:spring-boot-starter-validation") 7 | compileOnly("org.springframework.boot:spring-boot-autoconfigure") 8 | compileOnly("org.springframework.boot:spring-boot-starter-actuator") 9 | 10 | testImplementation("org.springframework.boot:spring-boot-starter-test") 11 | testImplementation("org.springframework.boot:spring-boot-starter-actuator") 12 | testImplementation("com.fasterxml.jackson.core:jackson-databind") 13 | // https://github.com/gradle/gradle/issues/33950 14 | testRuntimeOnly("org.junit.platform:junit-platform-launcher") 15 | 16 | annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") 17 | } 18 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs.nixpkgs.url = "github:nixos/nixpkgs"; 3 | inputs.flake-utils.url = "github:numtide/flake-utils"; 4 | 5 | outputs = { nixpkgs, flake-utils, ... }: 6 | flake-utils.lib.eachSystem [ "aarch64-darwin" "x86_64-darwin" "x86_64-linux" ] (system: 7 | let 8 | pkgs = import nixpkgs { inherit system; }; 9 | in 10 | { 11 | devShells.default = pkgs.mkShell { 12 | packages = [ 13 | pkgs.jdk21 14 | pkgs.gnupg 15 | pkgs.jreleaser-cli 16 | pkgs.python3Packages.mkdocs-material 17 | pkgs.python3Packages.cairosvg 18 | pkgs.python3Packages.pillow 19 | pkgs.python3Packages.mkdocs-macros 20 | # pkgs.python3Packages.mkdocs-autorefs 21 | # pkgs.python3Packages.mkdocs-redirects 22 | # pkgs.python3Packages.mkdocs-awesome-pages-plugin 23 | ]; 24 | }; 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/command/CommandHandlerConfiguration.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | import java.lang.annotation.*; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | /** 8 | * Annotation to be used for {@link Configuration}s containing {@link CommandHandling} annotated methods, 9 | * {@link CommandHandlerDefinition} {@link org.springframework.context.annotation.Bean}s, @{@link StateRebuilding} 10 | * annotated methods, and {@link StateRebuildingHandlerDefinition} {@link org.springframework.context.annotation.Bean}s 11 | * in order to be able to test them using {@link com.opencqrs.framework.command.CommandHandlingTest}. 12 | */ 13 | @Target(ElementType.TYPE) 14 | @Retention(RetentionPolicy.RUNTIME) 15 | @Documented 16 | @Inherited 17 | @Configuration 18 | public @interface CommandHandlerConfiguration {} 19 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/eventhandler/progress/Progress.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.eventhandler.progress; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | import com.opencqrs.framework.eventhandler.EventHandlingProcessor; 6 | 7 | /** 8 | * Sealed interface for subclasses representing the progress of {@linkplain EventHandlingProcessor 9 | * event handling} the event stream for a processing group. 10 | */ 11 | public sealed interface Progress { 12 | 13 | /** States no progress for the event processing group is known. */ 14 | record None() implements Progress {} 15 | 16 | /** 17 | * Represents the last {@link Event#id()} that has been successfully processed by the event 18 | * processing group. 19 | * 20 | * @param id the event id 21 | */ 22 | record Success(String id) implements Progress {} 23 | } 24 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/metadata/PropagationUtil.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.metadata; 3 | 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | /** Util class for meta-data propagation. */ 8 | public class PropagationUtil { 9 | 10 | public static Map propagateMetaData( 11 | Map metaData, Map propagationData, PropagationMode mode) { 12 | if (mode == PropagationMode.NONE) return metaData; 13 | 14 | var result = new HashMap(metaData); 15 | propagationData.forEach((key, value) -> result.merge(key, value, (o1, o2) -> switch (mode) { 16 | case KEEP_IF_PRESENT -> o1; 17 | case OVERRIDE_IF_PRESENT -> o2; 18 | default -> throw new IllegalStateException("Unexpected value: " + mode); 19 | })); 20 | 21 | return result; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/projection/reader/ReaderProjector.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.projection.reader; 3 | 4 | import com.opencqrs.example.domain.reader.api.ReaderRegisteredEvent; 5 | import com.opencqrs.framework.eventhandler.EventHandling; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.transaction.annotation.Propagation; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | @Component 12 | public class ReaderProjector { 13 | 14 | @EventHandling("reader") 15 | @Transactional(propagation = Propagation.MANDATORY) 16 | public void on(ReaderRegisteredEvent event, @Autowired ReaderRepository repository) { 17 | ReaderEntity entity = new ReaderEntity(); 18 | entity.id = event.id(); 19 | repository.save(entity); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/metadata/MetaDataPropagationProperties.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.metadata; 3 | 4 | import com.opencqrs.framework.command.CommandRouter; 5 | import com.opencqrs.framework.command.CommandRouterAutoConfiguration; 6 | import java.util.Set; 7 | import org.springframework.boot.context.properties.ConfigurationProperties; 8 | import org.springframework.boot.context.properties.bind.DefaultValue; 9 | 10 | /** 11 | * {@link ConfigurationProperties} for {@linkplain CommandRouterAutoConfiguration auto-configured} {@link CommandRouter} 12 | * meta-data propagation. 13 | * 14 | * @param mode The propagation mode to use. 15 | * @param keys The meta-data keys to propagate. 16 | */ 17 | @ConfigurationProperties("opencqrs.metadata.propagation") 18 | public record MetaDataPropagationProperties( 19 | @DefaultValue("keep_if_present") PropagationMode mode, @DefaultValue Set keys) {} 20 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.8/userguide/multi_project_builds.html in the Gradle documentation. 6 | */ 7 | 8 | gradle.beforeProject { 9 | extensions.extraProperties["frameworkVersions"] = mapOf( 10 | "esdb.version" to "1.2.0", 11 | "spring.boot.version" to "3.5.6", 12 | ) 13 | extensions.extraProperties["memorySettings"] = mapOf( 14 | "test.min-heap-size" to "2048m", 15 | "test.max-heap-size" to "4096m", 16 | ) 17 | } 18 | 19 | rootProject.name = "opencqrs" 20 | include( 21 | "esdb-client", 22 | "esdb-client-spring-boot-autoconfigure", 23 | "esdb-client-spring-boot-starter", 24 | "framework", 25 | "framework-test", 26 | "framework-spring-boot-autoconfigure", 27 | "framework-spring-boot-starter", 28 | "example-application", 29 | ) 30 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/book/PageHandling.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.book; 3 | 4 | import com.opencqrs.example.domain.book.api.BookPageDamagedEvent; 5 | import com.opencqrs.example.domain.book.api.MarkBookPageDamagedCommand; 6 | import com.opencqrs.framework.command.CommandEventPublisher; 7 | import com.opencqrs.framework.command.CommandHandlerConfiguration; 8 | import com.opencqrs.framework.command.CommandHandling; 9 | import com.opencqrs.framework.command.StateRebuilding; 10 | 11 | @CommandHandlerConfiguration 12 | public class PageHandling { 13 | 14 | @CommandHandling 15 | public void handle(MarkBookPageDamagedCommand command, CommandEventPublisher publisher) { 16 | publisher.publish(new BookPageDamagedEvent.ByReader(command.isbn(), command.page(), command.reader())); 17 | } 18 | 19 | @StateRebuilding 20 | public Page on(BookPageDamagedEvent event) { 21 | return new Page(event.page(), true); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /framework-test/src/main/java/com/opencqrs/framework/command/CommandHandlingTestExcludeFilter.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | import java.util.Set; 5 | import org.springframework.boot.test.autoconfigure.filter.StandardAnnotationCustomizableTypeExcludeFilter; 6 | 7 | /** 8 | * {@link StandardAnnotationCustomizableTypeExcludeFilter} implementation for {@link CommandHandlingTest}, which 9 | * includes beans defined within {@link CommandHandlerConfiguration}s. 10 | */ 11 | final class CommandHandlingTestExcludeFilter 12 | extends StandardAnnotationCustomizableTypeExcludeFilter { 13 | 14 | CommandHandlingTestExcludeFilter(Class testClass) { 15 | super(testClass); 16 | } 17 | 18 | @Override 19 | protected boolean isUseDefaultFilters() { 20 | return true; 21 | } 22 | 23 | @Override 24 | protected Set> getDefaultIncludes() { 25 | return Set.of(CommandHandlerConfiguration.class); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/command/CommandSubjectConditionViolatedException.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | import com.opencqrs.framework.CqrsFrameworkException; 5 | 6 | /** Exception thrown if {@link Command.SubjectCondition} is violated. */ 7 | public abstract class CommandSubjectConditionViolatedException extends CqrsFrameworkException.NonTransientException { 8 | 9 | private final Command command; 10 | private final Command.SubjectCondition subjectCondition; 11 | 12 | public CommandSubjectConditionViolatedException( 13 | String message, Command command, Command.SubjectCondition subjectCondition) { 14 | super(message); 15 | this.command = command; 16 | this.subjectCondition = subjectCondition; 17 | } 18 | 19 | public Command getCommand() { 20 | return command; 21 | } 22 | 23 | public Command.SubjectCondition getSubjectCondition() { 24 | return subjectCondition; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/upcaster/TypeChangingEventUpcaster.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.upcaster; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | import java.util.stream.Stream; 6 | 7 | /** 8 | * {@link EventUpcaster} implementation that changes the {@link Event#type()} to a new type if it matches the configured 9 | * source type. 10 | */ 11 | public class TypeChangingEventUpcaster implements EventUpcaster { 12 | 13 | private final String sourceType; 14 | private final String targetType; 15 | 16 | public TypeChangingEventUpcaster(String sourceType, String targetType) { 17 | this.sourceType = sourceType; 18 | this.targetType = targetType; 19 | } 20 | 21 | @Override 22 | public boolean canUpcast(Event event) { 23 | return event.type().equals(sourceType); 24 | } 25 | 26 | @Override 27 | public Stream upcast(Event event) { 28 | return Stream.of(new Result(targetType, event.data())); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /esdb-client-spring-boot-autoconfigure/src/main/java/com/opencqrs/esdb/client/EsdbHealthIndicator.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client; 3 | 4 | import org.springframework.boot.actuate.health.AbstractHealthIndicator; 5 | 6 | /** 7 | * {@link org.springframework.boot.actuate.health.HealthContributor} implementation based on 8 | * {@link EsdbClient#health()}. 9 | */ 10 | public class EsdbHealthIndicator extends AbstractHealthIndicator { 11 | 12 | private final EsdbClient client; 13 | 14 | public EsdbHealthIndicator(EsdbClient client) { 15 | this.client = client; 16 | } 17 | 18 | @Override 19 | protected void doHealthCheck(org.springframework.boot.actuate.health.Health.Builder builder) { 20 | Health health = client.health(); 21 | builder.withDetail("status", health.status()).withDetail("checks", health.checks()); 22 | 23 | switch (health.status()) { 24 | case pass, warn -> builder.up(); 25 | case fail -> builder.down(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/reader/ReaderHandling.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.reader; 3 | 4 | import com.opencqrs.example.domain.reader.api.ReaderRegisteredEvent; 5 | import com.opencqrs.example.domain.reader.api.RegisterReaderCommand; 6 | import com.opencqrs.framework.command.CommandEventPublisher; 7 | import com.opencqrs.framework.command.CommandHandlerConfiguration; 8 | import com.opencqrs.framework.command.CommandHandling; 9 | import com.opencqrs.framework.command.StateRebuilding; 10 | import java.util.UUID; 11 | 12 | @CommandHandlerConfiguration 13 | public class ReaderHandling { 14 | 15 | @CommandHandling 16 | public UUID register(RegisterReaderCommand command, CommandEventPublisher publisher) { 17 | publisher.publish(new ReaderRegisteredEvent(command.id(), command.name())); 18 | return command.id(); 19 | } 20 | 21 | @StateRebuilding 22 | public UUID on(ReaderRegisteredEvent event) { 23 | return event.id(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/persistence/EventCapturer.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.persistence; 3 | 4 | import com.opencqrs.esdb.client.Precondition; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | /** 10 | * In-memory implementation of {@link EventPublisher} using {@link CapturedEvent}s. 11 | * 12 | * @see #getEvents() 13 | */ 14 | public class EventCapturer implements EventPublisher { 15 | 16 | private final List events = new ArrayList<>(); 17 | 18 | /** 19 | * Retrieves the list of events captured by this {@link EventPublisher}. 20 | * 21 | * @return the list of captured events 22 | */ 23 | public List getEvents() { 24 | return events; 25 | } 26 | 27 | @Override 28 | public void publish(String subject, E event, Map metaData, List preconditions) { 29 | events.add(new CapturedEvent(subject, event, metaData, preconditions)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/build.gradle.kts: -------------------------------------------------------------------------------- 1 | description = "Spring Boot auto configurations for OpenCQRS framework" 2 | 3 | dependencies { 4 | api(project(":framework")) 5 | compileOnly("jakarta.validation:jakarta.validation-api") 6 | implementation("com.fasterxml.jackson.core:jackson-databind") 7 | compileOnly("org.springframework.boot:spring-boot-autoconfigure") 8 | compileOnly("org.springframework:spring-jdbc") 9 | compileOnly("org.springframework:spring-tx") 10 | compileOnly("org.springframework.integration:spring-integration-core") 11 | testImplementation("org.springframework.boot:spring-boot-starter-test") 12 | testImplementation("org.springframework.boot:spring-boot-starter-data-jdbc") 13 | testImplementation("org.springframework.integration:spring-integration-core") 14 | testRuntimeOnly("com.h2database:h2") 15 | // https://github.com/gradle/gradle/issues/33950 16 | testRuntimeOnly("org.junit.platform:junit-platform-launcher") 17 | 18 | annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") 19 | } 20 | -------------------------------------------------------------------------------- /example-application/src/test/java/com/opencqrs/example/domain/book/PageHandlingTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.book; 3 | 4 | import com.opencqrs.example.domain.book.api.BookPageDamagedEvent; 5 | import com.opencqrs.example.domain.book.api.MarkBookPageDamagedCommand; 6 | import com.opencqrs.framework.command.CommandHandlingTest; 7 | import com.opencqrs.framework.command.CommandHandlingTestFixture; 8 | import java.util.UUID; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | 12 | @CommandHandlingTest 13 | public class PageHandlingTest { 14 | 15 | @Test 16 | public void pageMarkedAsDamaged(@Autowired CommandHandlingTestFixture fixture) { 17 | fixture.givenNothing() 18 | .when(new MarkBookPageDamagedCommand("4711", 42L, UUID.randomUUID())) 19 | .expectSuccessfulExecution() 20 | .expectSingleEvent(event -> event.commandSubject().payloadType(BookPageDamagedEvent.ByReader.class)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /framework/build.gradle.kts: -------------------------------------------------------------------------------- 1 | description = "OpenCQRS Java CQRS/ES Core Framework" 2 | 3 | dependencies { 4 | api(project(":esdb-client")) 5 | compileOnly("jakarta.validation:jakarta.validation-api") 6 | implementation("com.fasterxml.jackson.core:jackson-databind") 7 | implementation("jakarta.annotation:jakarta.annotation-api:3.0.0") 8 | testImplementation(project(":framework-spring-boot-starter")) 9 | testImplementation("org.springframework.boot:spring-boot-starter-validation") 10 | testImplementation("org.springframework.boot:spring-boot-starter-web") 11 | testImplementation("org.springframework.boot:spring-boot-starter-test") 12 | testImplementation("org.springframework.boot:spring-boot-testcontainers") 13 | testImplementation("org.springframework.boot:spring-boot-starter-jdbc") 14 | testImplementation("com.h2database:h2") 15 | testImplementation("org.testcontainers:junit-jupiter") 16 | testImplementation("org.awaitility:awaitility:4.3.0") 17 | // https://github.com/gradle/gradle/issues/33950 18 | testRuntimeOnly("org.junit.platform:junit-platform-launcher") 19 | } 20 | -------------------------------------------------------------------------------- /example-application/src/test/java/com/opencqrs/example/domain/reader/ReaderHandlingTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.reader; 3 | 4 | import com.opencqrs.example.domain.reader.api.ReaderRegisteredEvent; 5 | import com.opencqrs.example.domain.reader.api.RegisterReaderCommand; 6 | import com.opencqrs.framework.command.CommandHandlingTest; 7 | import com.opencqrs.framework.command.CommandHandlingTestFixture; 8 | import java.util.UUID; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | 12 | @CommandHandlingTest 13 | public class ReaderHandlingTest { 14 | 15 | @Test 16 | public void canRegister(@Autowired CommandHandlingTestFixture fixture) { 17 | var readerId = UUID.randomUUID(); 18 | fixture.givenNothing() 19 | .when(new RegisterReaderCommand(readerId, "Hugo Tester")) 20 | .expectSuccessfulExecution() 21 | .expectSingleEvent(new ReaderRegisteredEvent(readerId, "Hugo Tester")) 22 | .expectResult(readerId); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /esdb-client/src/main/java/com/opencqrs/esdb/client/EventCandidate.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client; 3 | 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | /** 10 | * Candidate event for {@link EsdbClient#write(List, List) publication} to a Cloud Events Specification conforming event store. 12 | * 13 | * @param source identifies the originating source of publication 14 | * @param subject an absolute path identifying the subject that the event is related to 15 | * @param type uniquely identifies the event type, specifically for being able to interpret the contained data structure 16 | * @param data a generic map structure containing the event payload, which is going to be stored as JSON within the 17 | * event store 18 | * @see Event 19 | * @see EsdbClient#write(List, List) 20 | */ 21 | public record EventCandidate( 22 | @NotBlank String source, @NotBlank String subject, @NotBlank String type, @NotNull Map data) {} 23 | -------------------------------------------------------------------------------- /esdb-client-spring-boot-autoconfigure/src/main/java/com/opencqrs/esdb/client/EsdbProperties.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client; 3 | 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | import java.net.URI; 7 | import java.time.Duration; 8 | import org.springframework.boot.context.properties.ConfigurationProperties; 9 | import org.springframework.boot.context.properties.bind.DefaultValue; 10 | 11 | /** 12 | * {@link ConfigurationProperties} for {@linkplain EsdbClientAutoConfiguration auto-configured} {@link EsdbClient}. 13 | * 14 | * @param server server configuration 15 | * @param connectionTimeout maximum duration to establish connection with the server 16 | */ 17 | @ConfigurationProperties("esdb") 18 | public record EsdbProperties(@NotNull Server server, @NotNull @DefaultValue("PT5S") Duration connectionTimeout) { 19 | 20 | /** 21 | * Server configuration settings. 22 | * 23 | * @param uri Server connection URI. 24 | * @param apiToken API access token. 25 | */ 26 | public record Server(@NotNull URI uri, @NotBlank String apiToken) {} 27 | } 28 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/types/ClassNameEventTypeResolverAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.types; 3 | 4 | import org.springframework.beans.factory.BeanClassLoaderAware; 5 | import org.springframework.boot.autoconfigure.AutoConfiguration; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 7 | import org.springframework.context.annotation.Bean; 8 | 9 | /** 10 | * {@linkplain org.springframework.boot.autoconfigure.EnableAutoConfiguration Auto-configuration} for 11 | * {@link ClassNameEventTypeResolver}. 12 | */ 13 | @AutoConfiguration 14 | public class ClassNameEventTypeResolverAutoConfiguration implements BeanClassLoaderAware { 15 | 16 | private ClassLoader beanClassLoader; 17 | 18 | @Override 19 | public void setBeanClassLoader(ClassLoader classLoader) { 20 | this.beanClassLoader = classLoader; 21 | } 22 | 23 | @Bean 24 | @ConditionalOnMissingBean 25 | public EventTypeResolver openCqrsClassNameEventTypeResolver() { 26 | return new ClassNameEventTypeResolver(beanClassLoader); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/metadata/PropagationMode.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.metadata; 3 | 4 | import com.opencqrs.framework.command.Command; 5 | import com.opencqrs.framework.command.CommandRouter; 6 | import java.util.Map; 7 | 8 | /** 9 | * Specifies how meta-data is going to be propagated from source (for instance command meta-data) to destination (for 10 | * instance published event meta-data). 11 | * 12 | * @see PropagationUtil#propagateMetaData(Map, Map, PropagationMode) 13 | * @see CommandRouter#send(Command, Map) 14 | */ 15 | public enum PropagationMode { 16 | 17 | /** Specifies that no meta-data keys (and values) will be propagated from source to destination. */ 18 | NONE, 19 | 20 | /** 21 | * Specifies that meta-data will be propagated from source to destination, but meta-data keys already present within 22 | * the destination will not be overridden. 23 | */ 24 | KEEP_IF_PRESENT, 25 | 26 | /** 27 | * Specifies that meta-data will be propagated from source to destination, overriding any meta-data keys present in 28 | * both source and destination. 29 | */ 30 | OVERRIDE_IF_PRESENT, 31 | } 32 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/eventhandler/progress/ProgressTracker.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.eventhandler.progress; 3 | 4 | import java.util.function.Supplier; 5 | 6 | /** Interface specifying operations for tracking progress of event processing groups. */ 7 | public interface ProgressTracker { 8 | 9 | /** 10 | * Retrieves the current {@link Progress} for the specified event processing group and partition. 11 | * 12 | * @param group the processing group identifier 13 | * @param partition the partition number 14 | * @return the current progress 15 | */ 16 | Progress current(String group, long partition); 17 | 18 | /** 19 | * Proceeds the current {@link Progress} by executing the given {@link Supplier}, which in turn yields the new 20 | * progress for the specified event processing group and partition. 21 | * 22 | * @param group the processing group identifier 23 | * @param partition the partition number 24 | * @param execution the supplier returning the new progress, if executed successfully 25 | */ 26 | void proceed(String group, long partition, Supplier execution); 27 | } 28 | -------------------------------------------------------------------------------- /mkdocs/docs/concepts/README.md: -------------------------------------------------------------------------------- 1 | # Concepts 2 | 3 | This chapter introduces the core concepts that form the foundation of the design and architecture of CQRS/ES applications. 4 | Understanding these principles is essential for working effectively with {{ custom.framework_name }} and for building applications that are robust, 5 | scalable, and easy to evolve over time. 6 | 7 | - [Events](events/index.md) represent facts that have happened in the domain. They capture state changes in a way that is immutable, explicit, and traceable. 8 | - [Event Sourcing](event_sourcing/index.md) is a persistence pattern where the system’s state is derived by replaying the sequence of events that have occurred, rather than storing only the current state. This enables powerful features such as auditing, debugging, and the ability to reconstruct past states. 9 | - [Event Upcasting](upcasting/index.md) addresses the challenge of evolving event schemas over time. As the domain changes, existing events may need to be transformed into newer representations to remain compatible with the current application model. 10 | 11 | Together, these concepts provide the foundation for building systems that are event-driven, resilient to change, and transparent in their behavior. 12 | -------------------------------------------------------------------------------- /esdb-client/src/main/java/com/opencqrs/esdb/client/eventql/EventQueryProcessingError.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client.eventql; 3 | 4 | import jakarta.validation.constraints.NotNull; 5 | 6 | /** 7 | * Encapsulates a {@linkplain EventQueryErrorHandler#queryProcessingError(EventQueryProcessingError) query processing 8 | * error} caused by {@link com.opencqrs.esdb.client.EsdbClient#query(EventQuery, EventQueryRowHandler, 9 | * EventQueryErrorHandler)}. 10 | * 11 | * @param error error descriptions 12 | * @param startToken optional start token regarding the query input string 13 | * @param endToken optional end token regarding the query input string 14 | */ 15 | public record EventQueryProcessingError(@NotNull String error, Token startToken, Token endToken) { 16 | 17 | /** 18 | * Encapsulates a token describing the error position within a malformed or unprocessable query. 19 | * 20 | * @param line the line number 21 | * @param column the column number 22 | * @param text the encountered query text at that position 23 | * @param type the type of error 24 | */ 25 | public record Token(Integer line, Integer column, String text, String type) {} 26 | } 27 | -------------------------------------------------------------------------------- /esdb-client/src/main/java/com/opencqrs/esdb/client/Util.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client; 3 | 4 | import java.net.http.HttpHeaders; 5 | import java.nio.charset.Charset; 6 | import java.nio.charset.StandardCharsets; 7 | 8 | final class Util { 9 | 10 | private static final String CHARSET_KEY = "charset="; 11 | 12 | /** 13 | * Determines the {@link Charset} from the {@code Content-Type} {@link HttpHeaders}. 14 | * 15 | * @param headers the HTTP headers to inspect 16 | * @return the charset or {@linkplain StandardCharsets#UTF_8 UTF-8} as fall-back, if none (or an invalid one) was 17 | * specified 18 | */ 19 | public static Charset fromHttpHeaders(HttpHeaders headers) { 20 | return headers.firstValue("Content-Type") 21 | .map(it -> { 22 | int i = it.indexOf(CHARSET_KEY); 23 | if (i >= 0) { 24 | return it.substring(i + CHARSET_KEY.length()).split(";")[0]; 25 | } else { 26 | return null; 27 | } 28 | }) 29 | .map(name -> Charset.forName(name, StandardCharsets.UTF_8)) 30 | .orElse(StandardCharsets.UTF_8); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/types/EventTypeResolver.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.types; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | import com.opencqrs.esdb.client.EventCandidate; 6 | 7 | /** 8 | * Interface implemented for resolving Java {@link Class} to {@link EventCandidate#type()} or {@link Event#type()} and 9 | * vice versa. 10 | */ 11 | public interface EventTypeResolver { 12 | 13 | /** 14 | * Resolves the given event type for the given {@link Class}. 15 | * 16 | * @param clazz the Java event {@link Class} to be resolved 17 | * @return the event type to be used for {@link EventCandidate#type()} or {@link Event#type()} 18 | * @throws EventTypeResolutionException in case the type cannot be resolved 19 | */ 20 | String getEventType(Class clazz) throws EventTypeResolutionException; 21 | 22 | /** 23 | * Resolved the Java {@link Class} for the given event type. 24 | * 25 | * @param eventType the event type to be resolved 26 | * @return the Java event {@link Class} 27 | * @throws EventTypeResolutionException in case the {@link Class} cannot be resolved 28 | */ 29 | Class getJavaClass(String eventType) throws EventTypeResolutionException; 30 | } 31 | -------------------------------------------------------------------------------- /mkdocs/main.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | def define_env(env): 4 | 5 | @env.macro 6 | def esdb_name() -> str: 7 | return "EventSourcingDB" 8 | 9 | @env.macro 10 | def esdb_ref() -> str: 11 | return f"[{esdb_name()}](https://www.eventsourcingdb.io)" 12 | 13 | @env.macro 14 | def javadoc_class_ref(classname: str, base_url: str = "https://docs.opencqrs.com/javadoc") -> str: 15 | parts = classname.split(".") 16 | split_index = 0 17 | 18 | # Suche erstes Segment, das mit einem Großbuchstaben beginnt (Klassenanfang) 19 | for i, part in enumerate(parts): 20 | if part and part[0].isupper(): 21 | split_index = i 22 | break 23 | 24 | # Packages → durch Slashes verbinden 25 | package_path = "/".join(parts[:split_index]) 26 | # Klassenname + evtl. Inner Classes → bleiben mit Punkt verbunden 27 | class_path = ".".join(parts[split_index:]) 28 | 29 | # Zusammensetzen 30 | full_path = f"{package_path}/{class_path}" if package_path else class_path 31 | url = f"{base_url}/{full_path}.html" 32 | short_classname = parts[-1] 33 | 34 | return f'{short_classname}' 35 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/persistence/CapturedEvent.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.persistence; 3 | 4 | import com.opencqrs.esdb.client.Precondition; 5 | import jakarta.validation.constraints.NotBlank; 6 | import jakarta.validation.constraints.NotNull; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | /** 11 | * Record capturing an event publication intent. 12 | * 13 | * @param subject the subject the event is going to be published to 14 | * @param event the event object to be published 15 | * @param metaData the event meta-data to be published 16 | * @param preconditions the preconditions that must not be violated when publishing 17 | */ 18 | public record CapturedEvent( 19 | @NotBlank String subject, 20 | @NotNull Object event, 21 | @NotNull Map metaData, 22 | @NotNull List preconditions) { 23 | /** 24 | * Convenience constructor, if no meta-data or preconditions are needed. 25 | * 26 | * @param subject the subject the event is going to be published to 27 | * @param event the event object to be published 28 | */ 29 | public CapturedEvent(String subject, Object event) { 30 | this(subject, event, Map.of(), List.of()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MAINTAINER.md: -------------------------------------------------------------------------------- 1 | 2 | # Publication to Maven Central 3 | 4 | Make sure you have created and switched to an appropriate Git tag, before continuing! 5 | 6 | ## Environment 7 | 8 | Your environment needs to provide the following settings, preferably using `../.envrc.local`: 9 | ```shell 10 | # only upload to Central Sonatype, publication is done manually via Web-UI 11 | export JRELEASER_MAVENCENTRAL_STAGE=UPLOAD 12 | export JRELEASER_MAVENCENTRAL_USERNAME= 13 | export JRELEASER_MAVENCENTRAL_PASSWORD= 14 | export SIGNING_KEY= 15 | export SIGNING_PASSWORD 16 | ``` 17 | 18 | ## Build & Upload 19 | 20 | Build the project using the tagged version and upload it to Central Sonatype: 21 | ``` 22 | export VERSION_TAG=$(git describe --exact-match --tags 2> /dev/null || git rev-parse --short HEAD) 23 | # verify it's the correct tag 24 | echo $VERSION_TAG 25 | 26 | ./gradlew -Pversion=$VERSION_TAG clean build publishAllPublicationsToStagingRepository 27 | 28 | jreleaser-cli deploy --output-directory build -Djreleaser.project.version=$VERSION_TAG 29 | ``` 30 | 31 | ## Publish 32 | 33 | After having been validated, you may manually publish the bundle via [Central Sonatype](https://central.sonatype.com/publishing). -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/types/ClassNameEventTypeResolver.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.types; 3 | 4 | /** 5 | * {@link EventTypeResolver} implementation that maps {@link Class#getName()} to {@linkplain #getEventType(Class) event 6 | * type} and vice versa. 7 | * 8 | *

The use of this {@link EventTypeResolver} implementation is discouraged with respect to interoperability (with 9 | * non-Java applications operating on the same events) and refactoring. 10 | * 11 | * @see PreconfiguredAssignableClassEventTypeResolver 12 | */ 13 | public class ClassNameEventTypeResolver implements EventTypeResolver { 14 | 15 | private final ClassLoader classLoader; 16 | 17 | public ClassNameEventTypeResolver(ClassLoader classLoader) { 18 | this.classLoader = classLoader; 19 | } 20 | 21 | @Override 22 | public String getEventType(Class clazz) { 23 | return clazz.getName(); 24 | } 25 | 26 | @Override 27 | public Class getJavaClass(String eventType) { 28 | try { 29 | return classLoader.loadClass(eventType); 30 | } catch (ClassNotFoundException e) { 31 | throw new EventTypeResolutionException("failed to resolve java class for event type: " + eventType, e); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /framework/src/test/java/com/opencqrs/framework/eventhandler/partitioning/PerConfigurableLevelSubjectEventSequenceResolverTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.eventhandler.partitioning; 3 | 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | 6 | import com.opencqrs.esdb.client.Event; 7 | import org.junit.jupiter.params.ParameterizedTest; 8 | import org.junit.jupiter.params.provider.CsvSource; 9 | 10 | class PerConfigurableLevelSubjectEventSequenceResolverTest { 11 | 12 | @ParameterizedTest 13 | @CsvSource({ 14 | "2, /book/4711/pages/2444, /book/4711", 15 | "2, /book/4711, /book/4711", 16 | "3, /book/4711, /book/4711", 17 | "1, /book/4711, /book", 18 | "2, /book/4711/, /book/4711", 19 | "2, /, /", 20 | "2, ignored/book/4711/43, /book/4711", 21 | }) 22 | public void foo(int levelsToKeep, String subject, String sequenceId) { 23 | var resolver = new PerConfigurableLevelSubjectEventSequenceResolver(levelsToKeep); 24 | 25 | Event e = new Event(null, subject, null, null, null, null, null, null, null, null); 26 | 27 | assertThat(resolver.sequenceIdFor(e)).isEqualTo(sequenceId); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /esdb-client-spring-boot-autoconfigure/src/main/java/com/opencqrs/esdb/client/JacksonMarshallerAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client; 3 | 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.opencqrs.esdb.client.jackson.JacksonMarshaller; 6 | import org.springframework.boot.autoconfigure.AutoConfiguration; 7 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 11 | import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; 12 | import org.springframework.context.annotation.Bean; 13 | 14 | /** {@link EnableAutoConfiguration Auto-configuration} for {@link JacksonMarshaller}. */ 15 | @AutoConfiguration(after = JacksonAutoConfiguration.class) 16 | @ConditionalOnClass(ObjectMapper.class) 17 | @ConditionalOnBean(ObjectMapper.class) 18 | public class JacksonMarshallerAutoConfiguration { 19 | 20 | @Bean 21 | @ConditionalOnMissingBean(Marshaller.class) 22 | public JacksonMarshaller esdbJacksonMarshaller(ObjectMapper objectMapper) { 23 | return new JacksonMarshaller(objectMapper); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/serialization/JacksonEventDataMarshallerAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.serialization; 3 | 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.springframework.boot.autoconfigure.AutoConfiguration; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 9 | import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; 10 | import org.springframework.context.annotation.Bean; 11 | 12 | /** 13 | * {@linkplain org.springframework.boot.autoconfigure.EnableAutoConfiguration Auto-configuration} for 14 | * {@link JacksonEventDataMarshaller}. 15 | */ 16 | @AutoConfiguration(after = JacksonAutoConfiguration.class) 17 | @ConditionalOnClass(ObjectMapper.class) 18 | @ConditionalOnBean(ObjectMapper.class) 19 | public class JacksonEventDataMarshallerAutoConfiguration { 20 | 21 | @Bean 22 | @ConditionalOnMissingBean(EventDataMarshaller.class) 23 | public JacksonEventDataMarshaller openCqrsJacksonEventSerializer(ObjectMapper objectMapper) { 24 | return new JacksonEventDataMarshaller(objectMapper); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/eventhandler/EventHandlingProcessorLifecycleRegistration.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.eventhandler; 3 | 4 | import org.springframework.beans.factory.config.BeanDefinition; 5 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 6 | 7 | /** Interface to be implemented for registering {@link EventHandlingProcessorLifecycleController} beans. */ 8 | @FunctionalInterface 9 | public interface EventHandlingProcessorLifecycleRegistration { 10 | 11 | /** 12 | * Implementations are expected to {@linkplain BeanDefinitionRegistry#registerBeanDefinition(String, BeanDefinition) 13 | * register} an {@link EventHandlingProcessorLifecycleController} within the given {@link BeanDefinitionRegistry}, 14 | * if needed. 15 | * 16 | * @param registry the registry to be used for bean registration 17 | * @param eventHandlingProcessorBeanName the name of the {@link EventHandlingProcessor} bean to refer to for 18 | * life-cycle operations 19 | * @param processorSettings the processor settings 20 | */ 21 | void registerLifecycleBean( 22 | BeanDefinitionRegistry registry, 23 | String eventHandlingProcessorBeanName, 24 | EventHandlingProperties.ProcessorSettings processorSettings); 25 | } 26 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/command/cache/CommandHandlingCacheProperties.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command.cache; 3 | 4 | import com.opencqrs.framework.command.CommandRouterAutoConfiguration; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.boot.context.properties.bind.DefaultValue; 7 | 8 | /** 9 | * {@link ConfigurationProperties} for {@linkplain CommandRouterAutoConfiguration auto-configured} 10 | * {@link StateRebuildingCache}s. 11 | * 12 | * @param type The cache type to use, unless "ref" is specified. 13 | * @param capacity The cache capacity, if "in_memory" is used. 14 | * @param ref Custom cache to use. 15 | */ 16 | @ConfigurationProperties("opencqrs.command-handling.cache") 17 | public record CommandHandlingCacheProperties( 18 | @DefaultValue("none") Type type, @DefaultValue("1000") Integer capacity, String ref) { 19 | /** The pre-defined cache type. */ 20 | public enum Type { 21 | /** 22 | * No caching is used. 23 | * 24 | * @see NoStateRebuildingCache 25 | */ 26 | NONE, 27 | 28 | /** 29 | * In-memory caching is used. 30 | * 31 | * @see LruInMemoryStateRebuildingCache 32 | */ 33 | IN_MEMORY, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/eventhandler/partitioning/DefaultPartitionKeyResolver.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.eventhandler.partitioning; 3 | 4 | import java.nio.charset.StandardCharsets; 5 | import java.util.zip.CRC32; 6 | 7 | /** 8 | * Default implementation of {@link PartitionKeyResolver} which uses {@link CRC32} checksums and modulo operation to 9 | * derive partition numbers from event sequence identifiers. 10 | * 11 | *

This implementation is guaranteed to always yield the same partition number for the same event sequence 12 | * identifier, as long as the number of {@link #activePartitions} is constant. 13 | */ 14 | public final class DefaultPartitionKeyResolver implements PartitionKeyResolver { 15 | 16 | private final long activePartitions; 17 | 18 | public DefaultPartitionKeyResolver(long activePartitions) { 19 | if (activePartitions <= 0) { 20 | throw new IllegalArgumentException("partition num must be greater than zero"); 21 | } 22 | this.activePartitions = activePartitions; 23 | } 24 | 25 | @Override 26 | public long resolve(String sequenceId) { 27 | CRC32 checksum = new CRC32(); 28 | checksum.update(sequenceId.getBytes(StandardCharsets.UTF_8)); 29 | return checksum.getValue() % activePartitions; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/rest/ReaderController.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.rest; 3 | 4 | import com.opencqrs.example.domain.reader.api.RegisterReaderCommand; 5 | import com.opencqrs.framework.command.CommandRouter; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import java.net.URI; 8 | import java.util.Map; 9 | import java.util.UUID; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.validation.annotation.Validated; 13 | import org.springframework.web.bind.annotation.PostMapping; 14 | import org.springframework.web.bind.annotation.RequestBody; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | @RestController 19 | @RequestMapping("/api/reader/commands") 20 | public class ReaderController { 21 | 22 | @Autowired 23 | private CommandRouter commandRouter; 24 | 25 | @PostMapping("/register") 26 | public ResponseEntity purchase( 27 | @RequestBody @Validated RegisterReaderCommand body, HttpServletRequest request) { 28 | UUID id = commandRouter.send(body, Map.of("request-uri", request.getRequestURI())); 29 | return ResponseEntity.created(URI.create("/api/reader/" + id)).build(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /esdb-client-spring-boot-autoconfigure/src/test/java/com/opencqrs/esdb/client/EsdbHealthIndicatorTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client; 3 | 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | import static org.mockito.Mockito.doReturn; 6 | 7 | import java.util.Map; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.CsvSource; 11 | import org.mockito.InjectMocks; 12 | import org.mockito.Mock; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | 15 | @ExtendWith(MockitoExtension.class) 16 | public class EsdbHealthIndicatorTest { 17 | 18 | @Mock 19 | private EsdbClient client; 20 | 21 | @InjectMocks 22 | private EsdbHealthIndicator subject; 23 | 24 | @ParameterizedTest 25 | @CsvSource({ 26 | "pass, UP", 27 | "warn, UP", 28 | "fail, DOWN", 29 | }) 30 | public void esdbStatusProperlyMapped(Health.Status status, String healthStatus) { 31 | var checks = Map.of("foo", 42); 32 | doReturn(new Health(status, checks)).when(client).health(); 33 | 34 | assertThat(subject.health()).satisfies(h -> { 35 | assertThat(h.getStatus().getCode()).isEqualTo(healthStatus); 36 | assertThat(h.getDetails()).containsEntry("status", status).containsEntry("checks", checks); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/eventhandler/progress/InMemoryProgressTracker.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.eventhandler.progress; 3 | 4 | import com.opencqrs.framework.eventhandler.EventHandler; 5 | import java.util.Map; 6 | import java.util.Optional; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import java.util.concurrent.ConcurrentMap; 9 | import java.util.function.Supplier; 10 | 11 | /** 12 | * {@link ProgressTracker} implementation using an in-memory {@link Map}. This implementation is discouraged for 13 | * {@link EventHandler}s that rely on persistent progress while processing events, since the 14 | * {@linkplain #current(String, long)} current progress} is reset upon restart of the JVM. 15 | */ 16 | public class InMemoryProgressTracker implements ProgressTracker { 17 | 18 | private final ConcurrentMap ids = new ConcurrentHashMap<>(); 19 | 20 | @Override 21 | public Progress current(String group, long partition) { 22 | return Optional.ofNullable(ids.get(new GroupPartition(group, partition))) 23 | .orElseGet(Progress.None::new); 24 | } 25 | 26 | @Override 27 | public void proceed(String group, long partition, Supplier execution) { 28 | ids.put(new GroupPartition(group, partition), execution.get()); 29 | } 30 | 31 | record GroupPartition(String group, long partition) {} 32 | } 33 | -------------------------------------------------------------------------------- /esdb-client-spring-boot-autoconfigure/src/main/java/com/opencqrs/esdb/client/EsdbHealthContributorAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client; 3 | 4 | import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; 5 | import org.springframework.boot.actuate.health.HealthContributor; 6 | import org.springframework.boot.autoconfigure.AutoConfiguration; 7 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 11 | import org.springframework.context.annotation.Bean; 12 | 13 | /** {@link EnableAutoConfiguration Auto-configuration} for {@link EsdbHealthIndicator}. */ 14 | @AutoConfiguration(after = EsdbClientAutoConfiguration.class) 15 | @ConditionalOnClass({ 16 | HealthContributor.class, 17 | EsdbClient.class, 18 | }) 19 | @ConditionalOnBean(EsdbClient.class) 20 | @ConditionalOnEnabledHealthIndicator("esdb") 21 | public class EsdbHealthContributorAutoConfiguration { 22 | 23 | @Bean 24 | @ConditionalOnMissingBean(name = {"esdbHealthIndicator", "esdbHealthContributor"}) 25 | public HealthContributor esdbHealthContributor(EsdbClient client) { 26 | return new EsdbHealthIndicator(client); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/projection/statistics/EventStatisticsProjector.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.projection.statistics; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | import java.time.LocalDate; 6 | import java.time.ZoneId; 7 | import java.util.*; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @RestController 13 | @RequestMapping("/statistics/events") 14 | public class EventStatisticsProjector { 15 | 16 | private static final ZoneId zoneId = ZoneId.of("Europe/Berlin"); 17 | private final Map statistics = new HashMap<>(); 18 | 19 | @GetMapping 20 | public Map fetchStatistics() { 21 | return statistics; 22 | } 23 | 24 | @StatisticsHandling 25 | public void on(Event event) { 26 | Stats stats = statistics.computeIfAbsent(LocalDate.ofInstant(event.time(), zoneId), localDate -> new Stats()); 27 | stats.total++; 28 | stats.eventTypes.merge(event.type(), 1L, Long::sum); 29 | stats.subjects.merge(event.subject(), 1L, Long::sum); 30 | } 31 | 32 | public class Stats { 33 | public long total = 0; 34 | public SortedMap eventTypes = new TreeMap<>(); 35 | public SortedMap subjects = new TreeMap<>(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/command/CommandHandlerDefinition.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | /** 5 | * {@link CommandHandler} definition suitable for being processed by the {@link CommandRouter}. 6 | * 7 | * @param instanceClass instance type used for {@linkplain StateRebuildingHandler state rebuilding} 8 | * @param commandClass command type to be executed 9 | * @param handler command handler to be executed 10 | * @param sourcingMode the event-sourcing mode for the {@link CommandHandler} 11 | * @param the generic type of the instance to be event sourced before handling the command 12 | * @param the command type 13 | * @param the command execution result type 14 | */ 15 | public record CommandHandlerDefinition( 16 | Class instanceClass, Class commandClass, CommandHandler handler, SourcingMode sourcingMode) { 17 | 18 | /** 19 | * Convenience constructor using {@link SourcingMode#RECURSIVE}. 20 | * 21 | * @param instanceClazz instance type used for state rebuilding 22 | * @param commandClazz command type to be executed 23 | * @param commandHandler command handler to be executed 24 | */ 25 | public CommandHandlerDefinition( 26 | Class instanceClazz, Class commandClazz, CommandHandler commandHandler) { 27 | this(instanceClazz, commandClazz, commandHandler, SourcingMode.RECURSIVE); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /framework/src/test/java/com/opencqrs/framework/command/cache/LruInMemoryStateRebuildingCacheConcurrentUpdateTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command.cache; 3 | 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | 6 | import com.opencqrs.framework.Book; 7 | import com.opencqrs.framework.command.SourcingMode; 8 | import com.opencqrs.framework.command.cache.StateRebuildingCache.CacheKey; 9 | import com.opencqrs.framework.command.cache.StateRebuildingCache.CacheValue; 10 | import java.util.Map; 11 | import org.junit.jupiter.api.Test; 12 | 13 | public class LruInMemoryStateRebuildingCacheConcurrentUpdateTest { 14 | 15 | private final LruInMemoryStateRebuildingCache subject = new LruInMemoryStateRebuildingCache(5); 16 | 17 | @Test 18 | public void newestCacheValueBasedOnEventIdWinsIfConcurrentUpdate() { 19 | var cacheKey = new CacheKey<>("/books/4711", Book.class, SourcingMode.RECURSIVE); 20 | 21 | var expectedNewestCacheValue = 22 | new CacheValue<>("4", new Book("4711", true), Map.of("/books/4711", "2", "/books/4711/pages/42", "3")); 23 | 24 | subject.fetchAndMerge(cacheKey, ignored1 -> { 25 | subject.fetchAndMerge(cacheKey, ignored2 -> expectedNewestCacheValue); 26 | 27 | return new CacheValue<>("3", new Book("4711", true), Map.of("/books/4711", "2")); 28 | }); 29 | 30 | assertThat(subject.cache.get(cacheKey)).isEqualTo(expectedNewestCacheValue); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation Pipeline 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | docs: 12 | name: GitHub Pages Documentation 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - uses: cachix/install-nix-action@v31 20 | with: 21 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | - name: Deploy MkDocs 24 | run: nix develop --impure --command env -C mkdocs mkdocs gh-deploy -f mkdocs.yml --no-history 25 | 26 | - name: Aggregate JavaDoc 27 | run: nix develop --impure --command ./gradlew clean aggregateJavadoc 28 | 29 | - name: Deploy JavaDoc 30 | uses: peaceiris/actions-gh-pages@v4 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | publish_dir: build/javadoc 34 | publish_branch: gh-pages 35 | destination_dir: javadoc 36 | commit_message: "Deployed JavaDoc" 37 | 38 | - name: Generate Badges 39 | run: nix develop --impure --command ./gradlew clean generateBadges 40 | 41 | - name: Deploy Badges 42 | uses: peaceiris/actions-gh-pages@v4 43 | with: 44 | github_token: ${{ secrets.GITHUB_TOKEN }} 45 | publish_dir: build/badges 46 | publish_branch: gh-pages 47 | destination_dir: badges 48 | commit_message: "Deployed Shield.IO Badges" 49 | -------------------------------------------------------------------------------- /esdb-client/src/main/java/com/opencqrs/esdb/client/eventql/EventQueryErrorHandler.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client.eventql; 3 | 4 | import com.opencqrs.esdb.client.ClientException; 5 | 6 | /** 7 | * Callback interface that needs to be implement for receiving errors from calls to 8 | * {@link com.opencqrs.esdb.client.EsdbClient#query(EventQuery, EventQueryRowHandler, EventQueryErrorHandler)}. 9 | */ 10 | public interface EventQueryErrorHandler { 11 | 12 | /** 13 | * Signals a query processing error received from the underlying event store, while processing a row for the result 14 | * set. The error may have been caused both by an invalid query or by a projection error to the result row, e.g. 15 | * type conversion errors. 16 | * 17 | * @param error the error stating that a result row could not be processed successfully 18 | */ 19 | void queryProcessingError(EventQueryProcessingError error); 20 | 21 | /** 22 | * Signals a marshalling error for result rows successfully received from the underlying event store that could not 23 | * be properly deserialized or transformed to the desired target type by means of the requested 24 | * {@link EventQueryRowHandler}. 25 | * 26 | * @param exception the marshalling error that occurred 27 | * @param row the row returned from the event store which could not be unmarshalled 28 | */ 29 | void marshallingError(ClientException.MarshallingException exception, String row); 30 | } 31 | -------------------------------------------------------------------------------- /example-application/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | event-store: 3 | image: docker.io/thenativeweb/eventsourcingdb:1.1.0 4 | user: "root:root" 5 | volumes: 6 | - event-data:/events 7 | command: "run --api-token secret --data-directory /events --http-enabled=true --https-enabled=false --with-ui" 8 | ports: 9 | - "3000:3000" 10 | 11 | h2database: 12 | image: oscarfonts/h2:2.2.224 13 | ports: 14 | - "1521:1521" 15 | - "8081:81" 16 | environment: 17 | H2_OPTIONS: "-ifNotExists" 18 | 19 | application: 20 | image: "opencqrs/example-application:latest" 21 | ports: 22 | - "8080" 23 | environment: 24 | ESDB_SERVER_URI: http://event-store:3000 25 | SPRING_DATASOURCE_URL: jdbc:h2:tcp:h2database:1521/library 26 | SPRING_DATASOURCE_USERNAME: sa 27 | SPRING_DATASOURCE_PASSWORD: 28 | SPRING_SQL_INIT_MODE: always 29 | SPRING_JPA_HIBERNATE_DDL_AUTO: update 30 | SERVER_SHUTDOWN: graceful 31 | JAVA_TOOL_OPTIONS: "-XX:UseSVE=0" 32 | depends_on: 33 | - event-store 34 | - h2database 35 | deploy: 36 | replicas: 2 37 | 38 | nginx: 39 | image: nginx:latest 40 | volumes: 41 | - ./nginx.conf:/etc/nginx/nginx.conf:ro 42 | depends_on: 43 | - application 44 | ports: 45 | - "8080:8080" 46 | 47 | volumes: 48 | event-data: -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/eventhandler/SmartLifecycleEventHandlingProcessorLifecycleController.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.eventhandler; 3 | 4 | import org.springframework.context.SmartLifecycle; 5 | 6 | /** 7 | * {@link EventHandlingProcessorLifecycleController} implementation that implements {@link SmartLifecycle} to delegate 8 | * life-cycle handling to the Spring application context. 9 | */ 10 | class SmartLifecycleEventHandlingProcessorLifecycleController 11 | implements EventHandlingProcessorLifecycleController, SmartLifecycle { 12 | 13 | private boolean autoStartup = true; 14 | private boolean running = false; 15 | private final EventHandlingProcessor eventHandlingProcessor; 16 | 17 | SmartLifecycleEventHandlingProcessorLifecycleController(EventHandlingProcessor eventHandlingProcessor) { 18 | this.eventHandlingProcessor = eventHandlingProcessor; 19 | } 20 | 21 | @Override 22 | public boolean isAutoStartup() { 23 | return autoStartup; 24 | } 25 | 26 | public void setAutoStartup(boolean autoStartup) { 27 | this.autoStartup = autoStartup; 28 | } 29 | 30 | @Override 31 | public void start() { 32 | eventHandlingProcessor.start(); 33 | running = true; 34 | } 35 | 36 | @Override 37 | public void stop() { 38 | running = false; 39 | eventHandlingProcessor.stop(); 40 | } 41 | 42 | @Override 43 | public boolean isRunning() { 44 | return running; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /esdb-client-spring-boot-autoconfigure/src/main/java/com/opencqrs/esdb/client/EsdbClientAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client; 3 | 4 | import java.net.http.HttpClient; 5 | import org.springframework.boot.autoconfigure.AutoConfiguration; 6 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 9 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 10 | import org.springframework.context.annotation.Bean; 11 | 12 | /** {@link EnableAutoConfiguration Auto-configuration} for {@link EsdbClient}. */ 13 | @AutoConfiguration 14 | @ConditionalOnClass(EsdbClient.class) 15 | @EnableConfigurationProperties(EsdbProperties.class) 16 | public class EsdbClientAutoConfiguration { 17 | 18 | @Bean 19 | @ConditionalOnMissingBean(EsdbClient.class) 20 | public EsdbClient esdbClient( 21 | EsdbProperties properties, Marshaller marshaller, HttpClient.Builder httpClientBuilder) { 22 | return new EsdbClient( 23 | properties.server().uri(), 24 | properties.server().apiToken(), 25 | marshaller, 26 | httpClientBuilder.connectTimeout(properties.connectionTimeout())); 27 | } 28 | 29 | @Bean 30 | @ConditionalOnMissingBean(HttpClient.Builder.class) 31 | public HttpClient.Builder esdbHttpClientBuilder() { 32 | return HttpClient.newBuilder(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/serialization/EventData.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.serialization; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | import com.opencqrs.esdb.client.EventCandidate; 6 | import jakarta.validation.constraints.NotNull; 7 | import java.util.Map; 8 | 9 | /** 10 | * Represents unmarshalled {@link EventCandidate#data()} or {@link Event#data()}, respectively. The framework, in 11 | * contrast to the {@link com.opencqrs.esdb.client.EsdbClient}, distinguishes between an event's 12 | * {@linkplain EventData#payload() payload} and additional {@linkplain EventData#metaData() meta-data}. While the former 13 | * is used for storing actual Java objects, the latter one can store additional key-value pairs, if needed. 14 | * 15 | *

Implementations of this interface may choose to ignore the {@link EventData#metaData() meta-data} when 16 | * marshalling, if there is no need for meta-data support within the application. However, be aware of event 17 | * immutability which may render events non-marshallable, in case the {@link EventDataMarshaller} implementation needs 18 | * to be changed, after events have already been stored. 19 | * 20 | * @param metaData the meta-data stored within {@link EventCandidate#data()} or {@link Event#data()} 21 | * @param payload the Java object payload stored within {@link EventCandidate#data()} or {@link Event#data()} 22 | * @param the generic object type 23 | * @see EventDataMarshaller 24 | */ 25 | public record EventData(@NotNull Map metaData, @NotNull E payload) {} 26 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/projection/book/verifier/BookVerifier.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.projection.book.verifier; 3 | 4 | import com.opencqrs.example.domain.book.api.BookPurchasedEvent; 5 | import com.opencqrs.example.domain.book.api.BookReturnedEvent; 6 | import com.opencqrs.example.domain.book.api.MarkBookPageDamagedCommand; 7 | import com.opencqrs.framework.command.CommandRouter; 8 | import com.opencqrs.framework.command.CommandSubjectAlreadyExistsException; 9 | import java.util.Random; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | public class BookVerifier { 15 | 16 | private static final Random random = new Random(); 17 | 18 | @BookVerifying 19 | public void on(BookPurchasedEvent event, @Autowired BookRepository repository) { 20 | repository.save(new BookEntity(event.isbn(), event.numPages())); 21 | } 22 | 23 | @BookVerifying 24 | public void on( 25 | BookReturnedEvent event, @Autowired BookRepository repository, @Autowired CommandRouter commandRouter) { 26 | if (random.nextBoolean()) { 27 | var book = repository.findById(event.isbn()).get(); 28 | try { 29 | commandRouter.send( 30 | new MarkBookPageDamagedCommand(book.isbn, random.nextLong(book.pages), event.reader())); 31 | } catch (CommandSubjectAlreadyExistsException e) { 32 | // already marked as damaged 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/persistence/EventPersistenceAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.persistence; 3 | 4 | import com.opencqrs.esdb.client.EsdbClient; 5 | import com.opencqrs.framework.serialization.EventDataMarshaller; 6 | import com.opencqrs.framework.types.EventTypeResolver; 7 | import com.opencqrs.framework.upcaster.EventUpcasters; 8 | import org.springframework.boot.autoconfigure.AutoConfiguration; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.core.env.Environment; 12 | 13 | /** 14 | * {@linkplain org.springframework.boot.autoconfigure.EnableAutoConfiguration Auto-configuration} for 15 | * {@link EventRepository} and {@link EventSource}. 16 | */ 17 | @AutoConfiguration 18 | public class EventPersistenceAutoConfiguration { 19 | 20 | @Bean 21 | @ConditionalOnMissingBean 22 | public EventSource openCqrsEventSource(Environment environment) { 23 | return new EventSource("tag://" + environment.getProperty("spring.application.name")); 24 | } 25 | 26 | @Bean 27 | @ConditionalOnMissingBean 28 | public EventRepository openCqrsEventRepository( 29 | EsdbClient client, 30 | EventSource eventSource, 31 | EventTypeResolver eventTypeResolver, 32 | EventDataMarshaller eventDataMarshaller, 33 | EventUpcasters eventUpcasters) { 34 | return new EventRepository(client, eventSource, eventTypeResolver, eventDataMarshaller, eventUpcasters); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/eventhandler/partitioning/PerConfigurableLevelSubjectEventSequenceResolver.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.eventhandler.partitioning; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | import java.util.Arrays; 6 | 7 | /** 8 | * {@link ForRawEvent} implementation which reduced the {@link Event#subject()} path and reduces it to a configurable 9 | * level, i.e. reducing {@literal /book/4711/pages/42} to {@literal /book/4711} with level 2. 10 | */ 11 | public class PerConfigurableLevelSubjectEventSequenceResolver implements EventSequenceResolver.ForRawEvent { 12 | 13 | private final int keepLevels; 14 | 15 | public PerConfigurableLevelSubjectEventSequenceResolver(int keepLevels) { 16 | if (keepLevels < 1) { 17 | throw new IllegalArgumentException("at least one subject level must be kept: " + keepLevels); 18 | } 19 | this.keepLevels = keepLevels; 20 | } 21 | 22 | public int getKeepLevels() { 23 | return keepLevels; 24 | } 25 | 26 | private static String shortenSubject(String subject, int level) { 27 | var result = new StringBuffer(); 28 | Arrays.stream(subject.splitWithDelimiters("/", -1)) 29 | // subject is assumed to start with "/", so we skip the empty string 30 | .skip(1) 31 | .limit(level * 2L) 32 | .forEach(result::append); 33 | 34 | return result.toString(); 35 | } 36 | 37 | @Override 38 | public String sequenceIdFor(Event rawEvent) { 39 | return shortenSubject(rawEvent.subject(), this.keepLevels); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Pipeline 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | maven: 9 | name: Maven Central 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - uses: cachix/install-nix-action@v31 17 | with: 18 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | - name: Build 21 | env: 22 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 23 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 24 | run: nix develop --impure --command ./gradlew -Pversion=${{ github.ref_name }} clean publishAllPublicationsToStagingRepository 25 | 26 | - name: Bundle 27 | env: 28 | JRELEASER_MAVENCENTRAL_USERNAME: dry-run 29 | JRELEASER_MAVENCENTRAL_PASSWORD: dry-run 30 | run: nix develop --impure --command jreleaser-cli deploy --output-directory build -Djreleaser.project.version=${{ github.ref_name }} --dry-run 31 | 32 | - name: Archive Bundle 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: maven-bundle 36 | path: build/jreleaser/deploy/mavenCentral/sonatype/*.zip 37 | if-no-files-found: error 38 | 39 | - name: Deploy 40 | env: 41 | JRELEASER_MAVENCENTRAL_STAGE: UPLOAD 42 | JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.JRELEASER_MAVENCENTRAL_USERNAME }} 43 | JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.JRELEASER_MAVENCENTRAL_PASSWORD }} 44 | run: nix develop --impure --command jreleaser-cli deploy --output-directory build -Djreleaser.project.version=${{ github.ref_name }} 45 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1746689328, 24 | "narHash": "sha256-lTw19ufJ2lGdlQn7peekiaTla3CJX7wKA2kOv6MmqWg=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "d5313ff801cf0beb336eb00a1e2c66e4b398be61", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "repo": "nixpkgs", 33 | "type": "github" 34 | } 35 | }, 36 | "root": { 37 | "inputs": { 38 | "flake-utils": "flake-utils", 39 | "nixpkgs": "nixpkgs" 40 | } 41 | }, 42 | "systems": { 43 | "locked": { 44 | "lastModified": 1681028828, 45 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 46 | "owner": "nix-systems", 47 | "repo": "default", 48 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 49 | "type": "github" 50 | }, 51 | "original": { 52 | "owner": "nix-systems", 53 | "repo": "default", 54 | "type": "github" 55 | } 56 | } 57 | }, 58 | "root": "root", 59 | "version": 7 60 | } 61 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/transaction/SpringTransactionOperationsAdapter.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.transaction; 3 | 4 | import com.opencqrs.framework.eventhandler.EventHandling; 5 | import java.lang.reflect.Method; 6 | import org.springframework.transaction.PlatformTransactionManager; 7 | import org.springframework.transaction.interceptor.TransactionAttributeSource; 8 | import org.springframework.transaction.support.TransactionOperations; 9 | import org.springframework.transaction.support.TransactionTemplate; 10 | 11 | /** 12 | * {@link TransactionOperationsAdapter} implementation that uses a {@link TransactionOperations} delegate for 13 | * {@linkplain #execute(Runnable) transactional execution} of an {@link EventHandling} annotated method according to the 14 | * supplied {@link org.springframework.transaction.annotation.Transactional} configuration. 15 | */ 16 | public class SpringTransactionOperationsAdapter implements TransactionOperationsAdapter { 17 | 18 | private final TransactionOperations delegate; 19 | 20 | public SpringTransactionOperationsAdapter( 21 | PlatformTransactionManager platformTransactionManager, 22 | TransactionAttributeSource transactionAttributeSource, 23 | Method method, 24 | Class clazz) { 25 | this.delegate = new TransactionTemplate( 26 | platformTransactionManager, transactionAttributeSource.getTransactionAttribute(method, clazz)); 27 | } 28 | 29 | @Override 30 | public void execute(Runnable runnable) { 31 | delegate.executeWithoutResult(transactionStatus -> runnable.run()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/qa.yml: -------------------------------------------------------------------------------- 1 | name: QA Pipeline 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | jobs: 13 | 14 | build: 15 | name: Build 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v4 21 | 22 | - uses: cachix/install-nix-action@v31 23 | with: 24 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 25 | 26 | - name: Assemble 27 | run: nix develop --impure --command ./gradlew --no-daemon -Pversion=${{ github.sha }} clean assemble 28 | 29 | - name: Archive Jars 30 | uses: actions/upload-artifact@v4 31 | with: 32 | name: jars 33 | path: | 34 | **/build/libs/*.jar 35 | include-hidden-files: true 36 | if-no-files-found: error 37 | 38 | test: 39 | name: Run All Tests 40 | runs-on: ubuntu-latest 41 | 42 | steps: 43 | - name: Checkout code 44 | uses: actions/checkout@v4 45 | 46 | - uses: cachix/install-nix-action@v31 47 | with: 48 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 49 | 50 | - name: Lint 51 | run: nix develop --impure --command ./gradlew --no-daemon clean spotlessCheck 52 | 53 | - name: Run Tests 54 | run: nix develop --impure --command ./gradlew --no-daemon clean test 55 | 56 | - name: Archive build reports 57 | uses: actions/upload-artifact@v4 58 | with: 59 | name: buildreports 60 | path: | 61 | **/build/reports 62 | **/build/test-results 63 | include-hidden-files: true 64 | if-no-files-found: error 65 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/projection/statistics/BookStatisticsProjector.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.projection.statistics; 3 | 4 | import com.opencqrs.example.domain.book.api.BookLentEvent; 5 | import com.opencqrs.example.domain.book.api.BookPageDamagedEvent; 6 | import com.opencqrs.example.domain.book.api.BookPurchasedEvent; 7 | import java.util.*; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @RestController 13 | @RequestMapping("/statistics/books") 14 | public class BookStatisticsProjector { 15 | 16 | private final Map bookStats = new HashMap<>(); 17 | 18 | @GetMapping 19 | public Map fetchBookStatistics() { 20 | return bookStats; 21 | } 22 | 23 | @StatisticsHandling 24 | public void on(BookPurchasedEvent event) { 25 | bookStats.merge(event.isbn(), new BookStats(0L, event.numPages(), 0L), this::merge); 26 | } 27 | 28 | @StatisticsHandling 29 | public void on(BookLentEvent event) { 30 | bookStats.merge(event.isbn(), new BookStats(1L, 0L, 0L), this::merge); 31 | } 32 | 33 | @StatisticsHandling 34 | public void on(BookPageDamagedEvent event) { 35 | bookStats.merge(event.isbn(), new BookStats(0L, 0L, 1L), this::merge); 36 | } 37 | 38 | private BookStats merge(BookStats a, BookStats b) { 39 | return new BookStats(a.lent() + b.lent(), a.numPages() + b.numPages(), a.damagedPages() + b.damagedPages()); 40 | } 41 | 42 | record BookStats(Long lent, Long numPages, Long damagedPages) {} 43 | } 44 | -------------------------------------------------------------------------------- /example-application/build.gradle.kts: -------------------------------------------------------------------------------- 1 | description = "OpenCQRS Library Example Application" 2 | 3 | plugins { 4 | id("java") 5 | id("org.springframework.boot") version "3.5.3" 6 | id("com.google.cloud.tools.jib") version "3.4.5" 7 | } 8 | 9 | dependencies { 10 | implementation(project(":framework-spring-boot-starter")) 11 | implementation("org.springframework.boot:spring-boot-starter-web") 12 | implementation("org.springframework.boot:spring-boot-starter-validation") 13 | implementation("org.springframework.boot:spring-boot-starter-data-jpa") 14 | implementation("org.springframework.boot:spring-boot-starter-integration") 15 | implementation("org.springframework.integration:spring-integration-jdbc") 16 | implementation("org.springframework.boot:spring-boot-starter-actuator") 17 | runtimeOnly("com.h2database:h2") 18 | implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9") 19 | testImplementation("org.springframework.boot:spring-boot-starter-test") 20 | testImplementation("org.springframework.boot:spring-boot-testcontainers") 21 | testImplementation(project(":framework-test")) 22 | testImplementation("org.testcontainers:junit-jupiter") 23 | // https://github.com/gradle/gradle/issues/33950 24 | testRuntimeOnly("org.junit.platform:junit-platform-launcher") 25 | } 26 | 27 | jib { 28 | container { 29 | mainClass = "com.opencqrs.example.LibraryApplication" 30 | } 31 | from { 32 | image = when (System.getProperty("os.arch")) { 33 | "aarch64" -> "gcr.io/distroless/java21-debian12:latest-arm64" 34 | else -> "gcr.io/distroless/java21-debian12:latest-amd64" 35 | } 36 | } 37 | to { 38 | image = "opencqrs/example-application:latest" 39 | } 40 | } -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/CqrsFrameworkException.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework; 3 | 4 | import com.opencqrs.framework.client.ClientRequestErrorMapper; 5 | import java.util.function.Supplier; 6 | 7 | /** 8 | * Base class for exceptions handled within the framework. This class is {@code sealed} to distinguish between 9 | * {@linkplain TransientException transient} (potentially recoverable) and {@linkplain NonTransientException 10 | * non-transient} (non-recoverable) errors. 11 | * 12 | * @see ClientRequestErrorMapper#handleMappingExceptionsIfNecessary(Supplier) 13 | */ 14 | public sealed class CqrsFrameworkException extends RuntimeException { 15 | public CqrsFrameworkException(String message) { 16 | super(message); 17 | } 18 | 19 | public CqrsFrameworkException(String message, Throwable cause) { 20 | super(message, cause); 21 | } 22 | 23 | /** Exception class representing potentially recoverable errors, such as communication or concurrency errors. */ 24 | public static non-sealed class TransientException extends CqrsFrameworkException { 25 | public TransientException(String message) { 26 | super(message); 27 | } 28 | 29 | public TransientException(String message, Throwable cause) { 30 | super(message, cause); 31 | } 32 | } 33 | 34 | /** Exception class representing non-recoveralbe errors. */ 35 | public static non-sealed class NonTransientException extends CqrsFrameworkException { 36 | public NonTransientException(String message) { 37 | super(message); 38 | } 39 | 40 | public NonTransientException(String message, Throwable cause) { 41 | super(message, cause); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /esdb-client/src/main/java/com/opencqrs/esdb/client/Event.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client; 3 | 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | import java.time.Instant; 7 | import java.util.Map; 8 | import java.util.Set; 9 | import java.util.function.Consumer; 10 | 11 | /** 12 | * Event data structure retrieved from an event store, conforming to the Cloud Events Specification. 14 | * 15 | * @param source identifies the originating source of publication 16 | * @param subject an absolute path identifying the subject that the event is related to 17 | * @param type uniquely identifies the event type, specifically for being able to interpret the contained data structure 18 | * @param data a generic map structure containing the event payload 19 | * @param specVersion cloud events specification version 20 | * @param id a unique event identifier with respect to the originating event store 21 | * @param time the publication time-stamp 22 | * @param dataContentType the data content-type, always {@code application/json} 23 | * @param hash the hash of this event 24 | * @param predecessorHash the hash of the preceding event in the event store 25 | * @see EventCandidate 26 | * @see EsdbClient#read(String, Set) 27 | * @see EsdbClient#read(String, Set, Consumer) 28 | * @see EsdbClient#observe(String, Set, Consumer) 29 | */ 30 | public record Event( 31 | @NotBlank String source, 32 | @NotBlank String subject, 33 | @NotBlank String type, 34 | @NotNull Map data, 35 | @NotBlank String specVersion, 36 | @NotBlank String id, 37 | @NotNull Instant time, 38 | @NotBlank String dataContentType, 39 | String hash, 40 | @NotBlank String predecessorHash) 41 | implements Marshaller.ResponseElement {} 42 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/command/StateRebuilding.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | import java.lang.annotation.*; 6 | 7 | /** 8 | * Annotation to be used in favor of defining {@link StateRebuildingHandlerDefinition} 9 | * {@link org.springframework.context.annotation.Bean}s. 10 | * 11 | *

It can be placed on public methods returning a non-primitive {@link Object} representing the 12 | * {@link StateRebuildingHandlerDefinition#instanceClass()} and with at least one non-primitive parameter extending 13 | * {@link Object} representing an assignable {@link StateRebuildingHandlerDefinition#eventClass()} Additionally such 14 | * methods may have any of the following unique parameter types, in any order: 15 | * 16 | *

    17 | *
  • a type derived from {@link Object} representing the {@link StateRebuildingHandlerDefinition#instanceClass()} 18 | *
  • a {@link java.util.Map java.util.Map<String, ?>} for the event meta-data 19 | *
  • a {@link String} for the event subject 20 | *
  • an {@link Event} for the raw event 21 | *
  • any number of {@link org.springframework.beans.factory.annotation.Autowired} annotated parameters, resolving to 22 | * single beans within the {@link org.springframework.context.ApplicationContext} 23 | *
24 | * 25 | *

The method must be contained within an 26 | * {@link org.springframework.beans.factory.annotation.AnnotatedBeanDefinition}, for instance within 27 | * {@link org.springframework.stereotype.Component}s or {@link org.springframework.context.annotation.Configuration}s. 28 | * 29 | * @see StateRebuildingHandler.FromObjectAndMetaDataAndSubjectAndRawEvent 30 | */ 31 | @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) 32 | @Retention(RetentionPolicy.RUNTIME) 33 | @Documented 34 | @Inherited 35 | public @interface StateRebuilding {} 36 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/rest/BookController.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.rest; 3 | 4 | import com.opencqrs.example.domain.book.api.BorrowBookCommand; 5 | import com.opencqrs.example.domain.book.api.PurchaseBookCommand; 6 | import com.opencqrs.example.domain.book.api.ReturnBookCommand; 7 | import com.opencqrs.framework.command.CommandRouter; 8 | import jakarta.servlet.http.HttpServletRequest; 9 | import java.net.URI; 10 | import java.util.Map; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.validation.annotation.Validated; 14 | import org.springframework.web.bind.annotation.PostMapping; 15 | import org.springframework.web.bind.annotation.RequestBody; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | import org.springframework.web.bind.annotation.RestController; 18 | 19 | @RestController 20 | @RequestMapping("/api/book/commands") 21 | public class BookController { 22 | 23 | @Autowired 24 | private CommandRouter commandRouter; 25 | 26 | @PostMapping("/purchase") 27 | public ResponseEntity purchase(@RequestBody @Validated PurchaseBookCommand body, HttpServletRequest request) { 28 | String id = commandRouter.send(body, Map.of("request-uri", request.getRequestURI())); 29 | return ResponseEntity.created(URI.create("/api/book/" + id)).build(); 30 | } 31 | 32 | @PostMapping("/borrow") 33 | public void borrow(@RequestBody @Validated BorrowBookCommand body, HttpServletRequest request) { 34 | commandRouter.send(body, Map.of("request-uri", request.getRequestURI())); 35 | } 36 | 37 | @PostMapping("/return") 38 | public void returnBook(@RequestBody @Validated ReturnBookCommand body, HttpServletRequest request) { 39 | commandRouter.send(body, Map.of("request-uri", request.getRequestURI())); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/serialization/JacksonEventDataMarshaller.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.serialization; 3 | 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.opencqrs.framework.CqrsFrameworkException; 6 | import jakarta.validation.constraints.NotNull; 7 | import java.util.Map; 8 | 9 | /** {@link EventDataMarshaller} implementation that uses a configurable {@link ObjectMapper} for marshalling. */ 10 | public class JacksonEventDataMarshaller implements EventDataMarshaller { 11 | 12 | private final ObjectMapper objectMapper; 13 | 14 | public JacksonEventDataMarshaller(ObjectMapper objectMapper) { 15 | this.objectMapper = objectMapper; 16 | } 17 | 18 | @Override 19 | public Map serialize(EventData data) { 20 | try { 21 | Map payload = objectMapper.convertValue(data.payload(), Map.class); 22 | Map metaData = objectMapper.convertValue(data.metaData(), Map.class); 23 | return Map.of("payload", payload, "metadata", metaData); 24 | } catch (IllegalArgumentException e) { 25 | throw new CqrsFrameworkException.NonTransientException("failed to serialize: " + data, e.getCause()); 26 | } 27 | } 28 | 29 | @Override 30 | public EventData deserialize(Map json, Class clazz) { 31 | try { 32 | JacksonData deserialized = objectMapper.convertValue( 33 | json, objectMapper.getTypeFactory().constructParametricType(JacksonData.class, clazz)); 34 | return new EventData<>(deserialized.metadata(), deserialized.payload()); 35 | } catch (IllegalArgumentException e) { 36 | throw new CqrsFrameworkException.NonTransientException("failed to deserialize: " + json, e.getCause()); 37 | } 38 | } 39 | 40 | record JacksonData(@NotNull Map metadata, @NotNull E payload) {} 41 | } 42 | -------------------------------------------------------------------------------- /esdb-client/src/main/java/com/opencqrs/esdb/client/eventql/EventQueryRowHandler.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client.eventql; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | import java.util.Map; 6 | import java.util.function.Consumer; 7 | 8 | /** 9 | * Sealed base interface for handlers capable of processing 10 | * {@linkplain com.opencqrs.esdb.client.EsdbClient#query(EventQuery, EventQueryRowHandler, EventQueryErrorHandler) 11 | * query} result rows. 12 | */ 13 | public sealed interface EventQueryRowHandler { 14 | 15 | /** 16 | * {@link FunctionalInterface} to be implemented for consuming query result rows as {@link Event}. This can be used 17 | * for queries similar to {@code FROM e IN events ... PROJECT INTO e}. 18 | */ 19 | @FunctionalInterface 20 | non-sealed interface AsEvent extends EventQueryRowHandler, Consumer {} 21 | 22 | /** 23 | * {@link FunctionalInterface} to be implemented for consuming query result rows as JSON maps. This can be used for 24 | * queries similar to {@code FROM e IN events ... PROJECT INTO { id: e.id, ... }}. 25 | */ 26 | @FunctionalInterface 27 | non-sealed interface AsMap extends EventQueryRowHandler, Consumer> {} 28 | 29 | /** 30 | * Interface to be implemented for consuming query result rows as JSON objects. This can be used for queries similar 31 | * to {@code FROM e IN events ... PROJECT INTO { id: e.id, ... }}. 32 | */ 33 | non-sealed interface AsObject extends EventQueryRowHandler, Consumer { 34 | 35 | Class type(); 36 | } 37 | 38 | /** 39 | * {@link FunctionalInterface} to be implemented for consuming query result rows as scalar data types. This can be 40 | * used for queries similar to {@code FROM e IN events ... PROJECT INTO e.time }. 41 | */ 42 | @FunctionalInterface 43 | non-sealed interface AsScalar extends EventQueryRowHandler, Consumer {} 44 | } 45 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/persistence/EventPublisher.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.persistence; 3 | 4 | import com.opencqrs.esdb.client.Precondition; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | /** 9 | * Interface specifying operations for publishing Java event objects. Implementations typically either capture these 10 | * events in-memory for further (or deferred) processing or immediately convert and pass them to 11 | * {@link com.opencqrs.esdb.client.EsdbClient#write(List, List)}. 12 | */ 13 | public interface EventPublisher { 14 | 15 | /** 16 | * Publishes the given event onto the given subject. No meta-data, i.e. an empty map, is published with the event. 17 | * 18 | * @param subject the absolute subject path 19 | * @param event the event object 20 | * @param the generic event type 21 | */ 22 | default void publish(String subject, E event) { 23 | publish(subject, event, Map.of()); 24 | } 25 | 26 | /** 27 | * Publishes the given event and its meta-data onto the given subject. 28 | * 29 | * @param subject the absolute subject path 30 | * @param event the event object 31 | * @param metaData the event meta-data 32 | * @param the generic event type 33 | */ 34 | default void publish(String subject, E event, Map metaData) { 35 | publish(subject, event, metaData, List.of()); 36 | } 37 | 38 | /** 39 | * Publishes the given event and its meta-data onto the given subject with preconditions. 40 | * 41 | * @param subject the absolute subject path 42 | * @param event the event object 43 | * @param metaData the event meta-data 44 | * @param preconditions the preconditions that must not be violated 45 | * @param the generic event type 46 | */ 47 | void publish(String subject, E event, Map metaData, List preconditions); 48 | } 49 | -------------------------------------------------------------------------------- /mkdocs/docs/concepts/events/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Capturing Significant Changes in a System 3 | --- 4 | 5 | Events represent significant changes within a system. Unlike commands, which express an intent to perform an action, 6 | events record facts about what has already happened. They are immutable and serve as a reliable source of truth, 7 | enabling various components to react and maintain consistency in a distributed environment. 8 | 9 | By treating events as first-class citizens, systems become more __adaptable, scalable, and resilient__, making them a 10 | powerful tool for modern software design. 11 | 12 | ## Key Characteristics of Events 13 | 14 | - **Immutable** – Once an event is recorded, it cannot be changed. Any state changes must be represented as new events. 15 | - **Ordered** – Events are typically stored and processed in the order they occurred. 16 | - **Domain-Specific** – Events should clearly describe something meaningful in the business domain, e.g. `BookPurchasedEvent`, `ReaderRegisterEvent`, or `BookLentEvent`, ideally using past tense. 17 | 18 | ## Benefits of Using Events 19 | - **Recording State Changes** – Instead of directly modifying state, a system captures changes as events. 20 | - **Enabling Auditability** – A complete event history allows tracking of past actions, useful for debugging, analytics, and compliance. 21 | - **Building Derived Data** – Events can be used to construct various views of the system’s state, optimized for different use cases. 22 | - **Propagating Information** – Events notify other components about changes, ensuring they stay in sync. 23 | - **Decoupled Processing** – Events enable loosely coupled components, as they allow different parts of the system to react asynchronously. 24 | 25 | !!! note "Events and Event Sourcing" 26 | In some architectures, events are not just notifications but the primary source of truth. 27 | [Event Sourcing](../event_sourcing/index.md) uses stored events to reconstruct system state, ensuring 28 | full traceability and state replay. 29 | 30 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/serialization/EventDataMarshaller.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.serialization; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | import com.opencqrs.esdb.client.EventCandidate; 6 | import java.util.Map; 7 | 8 | /** 9 | * Interface implemented for transforming Java event objects and additional meta-data to JSON-like maps, and vice versa. 10 | * Used for transformations to or from {@link Event#data()} and to {@link EventCandidate#data()}. 11 | * 12 | *

To ensure interoperability, implementations of {@code this} should conform to the same JSON schema for the 13 | * {@code data} event content, i.e. using {@code metadata} for {@link EventData#metaData()} and {@code payload} for 14 | * {@link EventData#payload()}, e.g. as follows: 15 | * 16 | *

17 |  *     {
18 |  *         "data": {
19 |  *             "metadata": {
20 |  *                 "userId": "345897345"
21 |  *             },
22 |  *             "payload": {
23 |  *                 // serialized java object
24 |  *             }
25 |  *         },
26 |  *         // other cloud event attributes omitted for brevity
27 |  *     }
28 |  * 
29 | */ 30 | public interface EventDataMarshaller { 31 | 32 | /** 33 | * Converts the given {@link EventData} to a {@link Map} representation. 34 | * 35 | * @param data the event data 36 | * @return a JSON-like map representation 37 | * @param the generic payload type 38 | */ 39 | Map serialize(EventData data); 40 | 41 | /** 42 | * Converts a JSON-like {@link Map} representation to {@link EventData} using the given {@link Class} to determine 43 | * the payload type. 44 | * 45 | * @param json the JSON-like map representation 46 | * @param clazz the target type of the {@link EventData#payload()} 47 | * @return the event and meta-data 48 | * @param the generic payload type 49 | */ 50 | EventData deserialize(Map json, Class clazz); 51 | } 52 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/rest/ExceptionControllerAdvice.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.rest; 3 | 4 | import com.opencqrs.framework.CqrsFrameworkException; 5 | import com.opencqrs.framework.command.CommandSubjectAlreadyExistsException; 6 | import com.opencqrs.framework.command.CommandSubjectDoesNotExistException; 7 | import java.util.Map; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.web.bind.annotation.ControllerAdvice; 10 | import org.springframework.web.bind.annotation.ExceptionHandler; 11 | import org.springframework.web.bind.annotation.ResponseBody; 12 | import org.springframework.web.bind.annotation.ResponseStatus; 13 | 14 | @ControllerAdvice 15 | public class ExceptionControllerAdvice { 16 | 17 | private Map jsonError(Exception e) { 18 | return Map.of("message", e.getMessage()); 19 | } 20 | 21 | @ExceptionHandler(CqrsFrameworkException.TransientException.class) 22 | @ResponseStatus(value = HttpStatus.CONFLICT) 23 | @ResponseBody 24 | public Map transientErrors(CqrsFrameworkException.TransientException e) { 25 | return jsonError(e); 26 | } 27 | 28 | @ExceptionHandler(CqrsFrameworkException.NonTransientException.class) 29 | @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) 30 | @ResponseBody 31 | public Map nonTansientErrors(CqrsFrameworkException.NonTransientException e) { 32 | return jsonError(e); 33 | } 34 | 35 | @ExceptionHandler(CommandSubjectDoesNotExistException.class) 36 | @ResponseStatus(value = HttpStatus.NOT_FOUND) 37 | @ResponseBody 38 | public Map subjectNotFound(CommandSubjectDoesNotExistException e) { 39 | return jsonError(e); 40 | } 41 | 42 | @ExceptionHandler(CommandSubjectAlreadyExistsException.class) 43 | @ResponseStatus(value = HttpStatus.CONFLICT) 44 | @ResponseBody 45 | public Map subjectAlreadyExists(CommandSubjectAlreadyExistsException e) { 46 | return jsonError(e); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /mkdocs/docs/tutorials/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started with OpenCQRS 3 | description: Learning the core concepts and how to build OpenCQRS applications 4 | --- 5 | 6 | Welcome to the __Getting Started__ series for {{ custom.framework_name }}! This tutorial series will guide you through the 7 | core concepts and practical implementation of CQRS (Command Query Responsibility Segregation) using our framework. 8 | Whether you're new to CQRS or looking for a structured approach to building scalable and resilient applications, 9 | this series will provide you with a solid foundation. 10 | 11 | ## What You'll Learn 12 | 13 | Throughout these six tutorials, you will step through the key aspects of developing a CQRS-based system: 14 | 15 | 1. [Setting Up the Environment](01_setup/index.md) – Learn how to install and configure the necessary dependencies to get started. 16 | 2. [Command Handling](02_command_handling/index.md) – Understand how to define and process commands that modify the system's state. 17 | 3. [Developing the Domain](03_domain_logic/index.md) – Model your domain logic effectively to enforce business rules and maintain consistency. 18 | 4. [Handling Events](04_book_reminder/index.md) – Discover how to capture and react to domain events, ensuring proper event-driven behavior. 19 | 5. [Building Read Models from Events](05_catalog_projection/index.md) – Learn how to generate and maintain efficient read models for querying data. 20 | 6. [Testing the Domain](06_testing/index.md) – Explore strategies for writing robust tests to validate your domain logic and ensure system reliability. 21 | 22 | ## Who Should Follow This Series? 23 | 24 | - **Developers** looking to build applications with CQRS and Event Sourcing. 25 | - **Architects** interested in designing scalable, decoupled systems. 26 | - **Anyone curious about {{ custom.framework_name }}** and looking for a practical introduction with real-world examples. 27 | 28 | Each tutorial includes clear explanations, code examples, and practical exercises to help you gain hands-on experience. 29 | 30 | Ready to get started? Jump into the first tutorial and begin your journey with {{ custom.framework_name }}! 31 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/command/CommandEventCapturer.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | import com.opencqrs.framework.persistence.CapturedEvent; 5 | import com.opencqrs.framework.persistence.EventCapturer; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.concurrent.atomic.AtomicReference; 9 | 10 | /** 11 | * Default implementation of {@link CommandEventPublisher} used by {@link CommandRouter} to apply events to the 12 | * {@link StateRebuildingHandler}s relevant for the {@link Command} being executed. 13 | * 14 | * @param the instance as defined by the {@link CommandHandlerDefinition} being executed 15 | */ 16 | public class CommandEventCapturer extends EventCapturer implements CommandEventPublisher { 17 | 18 | private final List> stateRebuildingHandlerDefinitions; 19 | private final String subject; 20 | 21 | final AtomicReference previousInstance; 22 | 23 | public CommandEventCapturer( 24 | I initialInstance, 25 | String subject, 26 | List> stateRebuildingHandlerDefinitions) { 27 | this.stateRebuildingHandlerDefinitions = stateRebuildingHandlerDefinitions; 28 | this.previousInstance = new AtomicReference<>(initialInstance); 29 | this.subject = subject; 30 | } 31 | 32 | @Override 33 | public I publish(E event, Map metaData) { 34 | getEvents().add(new CapturedEvent(subject, event, metaData, List.of())); 35 | 36 | Util.applyUsingHandlers(stateRebuildingHandlerDefinitions, previousInstance, subject, event, metaData, null); 37 | 38 | return previousInstance.get(); 39 | } 40 | 41 | @Override 42 | public I publishRelative(String subjectSuffix, E event, Map metaData) { 43 | String s = subject + "/" + subjectSuffix; 44 | getEvents().add(new CapturedEvent(s, event, metaData, List.of())); 45 | 46 | Util.applyUsingHandlers(stateRebuildingHandlerDefinitions, previousInstance, s, event, metaData, null); 47 | 48 | return previousInstance.get(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /framework/src/test/java/com/opencqrs/framework/metadata/PropagationUtilTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.metadata; 3 | 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | 6 | import java.util.Map; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.params.ParameterizedTest; 9 | import org.junit.jupiter.params.provider.EnumSource; 10 | 11 | public class PropagationUtilTest { 12 | 13 | @ParameterizedTest 14 | @EnumSource(value = PropagationMode.class, mode = EnumSource.Mode.EXCLUDE, names = "NONE") 15 | public void nonConflictingMetaDataPropagated(PropagationMode mode) { 16 | Map source = Map.of("source01", 42L); 17 | Map destination = Map.of("destination01", true); 18 | 19 | assertThat(PropagationUtil.propagateMetaData(destination, source, mode)) 20 | .containsAllEntriesOf(source) 21 | .containsAllEntriesOf(destination); 22 | } 23 | 24 | @Test 25 | public void noMetaDataPropagated() { 26 | Map source = Map.of("source01", 42L); 27 | Map destination = Map.of("destination01", true); 28 | 29 | assertThat(PropagationUtil.propagateMetaData(destination, source, PropagationMode.NONE)) 30 | .containsAllEntriesOf(destination) 31 | .doesNotContainKeys(source.keySet().toArray()); 32 | } 33 | 34 | @Test 35 | public void metaDataPropagatedDuplicatesPreserved() { 36 | Map source = Map.of("source01", 42L, "destination01", false); 37 | Map destination = Map.of("destination01", true); 38 | 39 | assertThat(PropagationUtil.propagateMetaData(destination, source, PropagationMode.KEEP_IF_PRESENT)) 40 | .isEqualTo(Map.of("source01", 42L, "destination01", true)); 41 | } 42 | 43 | @Test 44 | public void metaDataPropagatedDuplicatesOverridden() { 45 | Map source = Map.of("source01", 42L, "destination01", false); 46 | Map destination = Map.of("destination01", true); 47 | 48 | assertThat(PropagationUtil.propagateMetaData(destination, source, PropagationMode.OVERRIDE_IF_PRESENT)) 49 | .isEqualTo(Map.of("source01", 42L, "destination01", false)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/command/CommandHandling.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | import java.lang.annotation.*; 5 | 6 | /** 7 | * Annotation to be used in favor of defining {@link CommandHandlerDefinition} 8 | * {@link org.springframework.context.annotation.Bean}s. 9 | * 10 | *

It can be placed on public methods returning {@code void} or any other {@link CommandHandler} result. Such methods 11 | * may have any of the following unique parameter types, in any order: 12 | * 13 | *

    14 | *
  • a mandatory type derived from {@link Command} representing the {@link CommandHandlerDefinition#commandClass()} 15 | *
  • an optional meta-data {@link java.util.Map} 16 | *
  • a type derived from {@link Object} representing the {@link CommandHandlerDefinition#instanceClass()} 17 | *
  • a {@link CommandEventPublisher} with the generic type matching the 18 | * {@link CommandHandlerDefinition#instanceClass()} 19 | *
  • any number of {@link org.springframework.beans.factory.annotation.Autowired} annotated parameters, resolving to 20 | * single beans within the {@link org.springframework.context.ApplicationContext} 21 | *
22 | * 23 | * To be able to derive the {@link CommandHandlerDefinition#instanceClass()}, any of the latter two parameters 24 | * is optional, but not both. 25 | * 26 | *

The method must be contained within an 27 | * {@link org.springframework.beans.factory.annotation.AnnotatedBeanDefinition}, for instance within 28 | * {@link org.springframework.stereotype.Component}s or {@link org.springframework.context.annotation.Configuration}s. 29 | * 30 | * @see StateRebuildingHandler.FromObjectAndMetaDataAndSubjectAndRawEvent 31 | */ 32 | @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) 33 | @Retention(RetentionPolicy.RUNTIME) 34 | @Documented 35 | @Inherited 36 | public @interface CommandHandling { 37 | 38 | /** 39 | * The {@link SourcingMode} for the {@link CommandHandlerDefinition}. 40 | * 41 | * @return the sourcing mode to be used, defaults to {@link SourcingMode#RECURSIVE} 42 | */ 43 | SourcingMode sourcingMode() default SourcingMode.RECURSIVE; 44 | } 45 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/command/Command.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | 6 | /** 7 | * Interface to be implemented by commands that can be handled by {@link CommandHandler}s. 8 | * 9 | * @see CommandRouter#send(Command) 10 | */ 11 | public interface Command { 12 | 13 | /** 14 | * Specifies the subject of this command, needed to source the necessary events for the command execution. 15 | * 16 | * @return the subject path 17 | */ 18 | String getSubject(); 19 | 20 | /** 21 | * Specifies the condition to check for the given {@link #getSubject()} before a {@link CommandHandler} will be 22 | * executed. 23 | * 24 | * @return the subject condition to check for 25 | */ 26 | default SubjectCondition getSubjectCondition() { 27 | return SubjectCondition.NONE; 28 | } 29 | 30 | /** The {@linkplain #getSubject() subject} condition checked before {@link CommandHandler} execution. */ 31 | enum SubjectCondition { 32 | 33 | /** No condition checks apply to the given {@linkplain #getSubject() subject}. */ 34 | NONE, 35 | 36 | /** 37 | * Assures that the given {@linkplain #getSubject() subject} does not exist, that is no {@link Event} was 38 | * sourced with that specific {@link Event#subject()}, in spite of any {@linkplain SourcingMode#RECURSIVE 39 | * recursive} subjects. Otherwise {@link CommandSubjectAlreadyExistsException} will be thrown. 40 | * 41 | *

The condition cannot be checked properly, if {@link SourcingMode#NONE} is used. 42 | */ 43 | PRISTINE, 44 | 45 | /** 46 | * Assures that the given {@linkplain #getSubject() subject} exists, that is at least one {@link Event} was 47 | * sourced with that specific {@link Event#subject()}, in spite of any {@linkplain SourcingMode#RECURSIVE 48 | * recursive} subjects. Otherwise {@link CommandSubjectDoesNotExistException} will be thrown. 49 | * 50 | *

The condition cannot be checked properly, if {@link SourcingMode#NONE} is used. 51 | */ 52 | EXISTS, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/eventhandler/partitioning/EventSequenceResolver.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.eventhandler.partitioning; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | import com.opencqrs.framework.eventhandler.EventHandlingProcessor; 6 | import java.util.Map; 7 | 8 | /** 9 | * Sealed base interface for inherited {@link FunctionalInterface} variants encapsulating the logic to derive a 10 | * sequence identifier for an event. 11 | * 12 | *

An event's sequence identifier is a {@link String} determining, if two consecutive events must be handled in 13 | * order. Two events with the same sequence identifier will be handled with respect to their order within the event 14 | * stream, while events with different sequence identifiers may be handled in parallel with no ordering constraints. 15 | * 16 | * @param the generic Java event type 17 | * @see EventHandlingProcessor#run() 18 | */ 19 | public sealed interface EventSequenceResolver { 20 | 21 | /** 22 | * {@link FunctionalInterface} to be implemented, if an event's sequence identifier can be derived from a raw 23 | * {@link Event}, that is without any upcasting or Java object deserialization. 24 | */ 25 | @FunctionalInterface 26 | non-sealed interface ForRawEvent extends EventSequenceResolver { 27 | 28 | /** 29 | * Determines the sequence identifier from a raw {@link Event}. 30 | * 31 | * @param rawEvent the raw event 32 | * @return the event's sequence identifier 33 | */ 34 | String sequenceIdFor(Event rawEvent); 35 | } 36 | 37 | /** 38 | * {@link FunctionalInterface} to be implemented, if an event's sequence identifier can only be derived from a fully 39 | * upcasted and deserialized Java event object. 40 | * 41 | * @param the generic Java event type 42 | */ 43 | @FunctionalInterface 44 | non-sealed interface ForObjectAndMetaDataAndRawEvent extends EventSequenceResolver { 45 | 46 | /** 47 | * Determines the sequence identifier from an upcasted and deserialized {@link Event}. 48 | * 49 | * @param event the Java event object 50 | * @param metaData the event meta-data 51 | * @return the event's sequence identifier 52 | */ 53 | String sequenceIdFor(E event, Map metaData); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/types/PreconfiguredAssignableClassEventTypeResolver.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.types; 3 | 4 | import static java.util.stream.Collectors.toMap; 5 | 6 | import com.opencqrs.framework.serialization.EventDataMarshaller; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Optional; 10 | 11 | /** 12 | * {@link EventTypeResolver} implementation that can be pre-configured using a specific mapping between event types and 13 | * Java classes. 14 | * 15 | *

This implementation takes into account {@linkplain Class#isAssignableFrom(Class) class assignability} and hence 16 | * may be used to configure event types for abstract or sealed super classes, as well. This reduces the effort of 17 | * maintaining explicit mappings for each subclass, as long as the {@link EventDataMarshaller} is capable of 18 | * {@linkplain EventDataMarshaller#deserialize(Map, Class) deserializing} class hierarchies. 19 | */ 20 | public class PreconfiguredAssignableClassEventTypeResolver implements EventTypeResolver { 21 | 22 | private final Map> typesToClasses; 23 | private final Map, String> classesToTypes; 24 | 25 | public PreconfiguredAssignableClassEventTypeResolver(Map> typesToClasses) { 26 | this.typesToClasses = typesToClasses; 27 | this.classesToTypes = typesToClasses.entrySet().stream().collect(toMap(Map.Entry::getValue, Map.Entry::getKey)); 28 | } 29 | 30 | @Override 31 | public String getEventType(Class clazz) { 32 | return classesToTypes.keySet().stream() 33 | .filter(c -> c.isAssignableFrom(clazz)) 34 | .reduce((a, b) -> { 35 | throw new EventTypeResolutionException( 36 | "ambiguous assignable classes found for event class (" + clazz + "): " + List.of(a, b)); 37 | }) 38 | .map(classesToTypes::get) 39 | .orElseThrow(() -> 40 | new EventTypeResolutionException("no assignable type configured for event class: " + clazz)); 41 | } 42 | 43 | @Override 44 | public Class getJavaClass(String eventType) { 45 | return Optional.ofNullable(typesToClasses.get(eventType)) 46 | .orElseThrow( 47 | () -> new EventTypeResolutionException("failed to resolve event class for type: " + eventType)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/command/Util.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | import com.opencqrs.framework.CqrsFrameworkException; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Optional; 9 | import java.util.concurrent.atomic.AtomicReference; 10 | 11 | class Util { 12 | 13 | static boolean applyUsingHandlers( 14 | List> stateRebuildingHandlerDefinitions, 15 | AtomicReference state, 16 | String subject, 17 | E event, 18 | Map metaData, 19 | Event rawEvent) { 20 | var wasApplied = new AtomicReference<>(false); 21 | stateRebuildingHandlerDefinitions.stream() 22 | .filter(srhd -> srhd.eventClass().isAssignableFrom(event.getClass())) 23 | .forEach(srhd -> { 24 | state.updateAndGet(i -> Optional.ofNullable( 25 | switch (srhd.handler()) { 26 | case StateRebuildingHandler.FromObject handler -> handler.on(i, event); 27 | case StateRebuildingHandler.FromObjectAndRawEvent handler -> 28 | handler.on(i, event, rawEvent); 29 | case StateRebuildingHandler.FromObjectAndMetaData handler -> 30 | handler.on(i, event, metaData); 31 | case StateRebuildingHandler.FromObjectAndMetaDataAndSubject handler -> 32 | handler.on(i, event, metaData, subject); 33 | case StateRebuildingHandler.FromObjectAndMetaDataAndSubjectAndRawEvent 34 | handler -> handler.on(i, event, metaData, subject, rawEvent); 35 | }) 36 | .orElseThrow(() -> new CqrsFrameworkException.NonTransientException( 37 | "state rebuilding handler returned 'null' instance for event: " 38 | + event.getClass().getName()))); 39 | wasApplied.set(true); 40 | }); 41 | 42 | return wasApplied.get(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/command/cache/LruInMemoryStateRebuildingCache.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command.cache; 3 | 4 | import com.opencqrs.esdb.client.IdUtil; 5 | import java.util.Collections; 6 | import java.util.LinkedHashMap; 7 | import java.util.Map; 8 | import java.util.function.Function; 9 | import java.util.logging.Logger; 10 | 11 | /** 12 | * {@link StateRebuildingCache} implementation backed by a {@code synchronized} {@link LinkedHashMap} with configurable 13 | * maximum capacity and LRU semantics. 14 | */ 15 | public class LruInMemoryStateRebuildingCache implements StateRebuildingCache { 16 | 17 | private static final Logger log = Logger.getLogger(LruInMemoryStateRebuildingCache.class.getName()); 18 | final Map cache; 19 | 20 | /** 21 | * Configures this with a maximum capacity. 22 | * 23 | * @param capacity the maximum number of {@link StateRebuildingCache.CacheValue}s to keep, before 24 | * {@linkplain LinkedHashMap#removeEldestEntry(Map.Entry) discarding exceed entries} 25 | */ 26 | public LruInMemoryStateRebuildingCache(int capacity) { 27 | cache = Collections.synchronizedMap(new LinkedHashMap<>(capacity, 0.75f, true) { 28 | @Override 29 | protected boolean removeEldestEntry(Map.Entry eldest) { 30 | if (size() > capacity) { 31 | log.fine(() -> "discarding eldest cache element: " + eldest.getKey()); 32 | return true; 33 | } 34 | return false; 35 | } 36 | }); 37 | } 38 | 39 | @Override 40 | public CacheValue fetchAndMerge(CacheKey key, Function, CacheValue> mergeFunction) { 41 | CacheValue updatedValue = mergeFunction.apply(cache.getOrDefault(key, new CacheValue<>(null, null, Map.of()))); 42 | 43 | return switch (updatedValue.eventId()) { 44 | case null -> updatedValue; 45 | default -> 46 | cache.compute(key, (cacheKey, cacheValue) -> switch (cacheValue) { 47 | case null -> updatedValue; 48 | default -> 49 | isHigherEventId(cacheValue.eventId(), updatedValue.eventId()) ? cacheValue : updatedValue; 50 | }); 51 | }; 52 | } 53 | 54 | private boolean isHigherEventId(String a, String b) { 55 | return IdUtil.fromEventId(a) > IdUtil.fromEventId(b); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /esdb-client/src/main/java/com/opencqrs/esdb/client/Precondition.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.esdb.client; 3 | 4 | import com.opencqrs.esdb.client.eventql.EventQuery; 5 | import jakarta.validation.constraints.NotBlank; 6 | import jakarta.validation.constraints.NotNull; 7 | import java.util.List; 8 | 9 | /** 10 | * Sealed interface for preconditions used for {@link EsdbClient#write(List, List) event publication} to ensure 11 | * consistency within the underlying event store. 12 | */ 13 | public sealed interface Precondition 14 | permits Precondition.SubjectIsOnEventId, 15 | Precondition.SubjectIsPristine, 16 | Precondition.SubjectIsPopulated, 17 | Precondition.EventQlQueryIsTrue { 18 | 19 | /** 20 | * A precondition stating the given subject must not yet exist within the event store. This precondition is not 21 | * violated by recursive subjects, that is subjects that are stored within a hierarchy underneath the given one. 22 | * 23 | * @param subject the path to the subject that needs to be pristine 24 | */ 25 | record SubjectIsPristine(@NotBlank String subject) implements Precondition {} 26 | 27 | /** 28 | * A precondition stating the given subject must have been updated by the given event id. The precondition is 29 | * violated if either the subject does not exist at all or an event with another id has already been published for 30 | * that subject. 31 | * 32 | * @param subject the path to the subject 33 | * @param eventId the expected event id 34 | */ 35 | record SubjectIsOnEventId(@NotBlank String subject, @NotBlank String eventId) implements Precondition {} 36 | 37 | /** 38 | * A precondition stating the given subject must already exist within the event store, that is at least one event 39 | * must have been published for that subject. This precondition is not violated by recursive subjects, that is 40 | * subjects that are stored within a hierarchy underneath the given one. 41 | * 42 | * @param subject the path to the subject that needs to be populated 43 | */ 44 | record SubjectIsPopulated(@NotBlank String subject) implements Precondition {} 45 | 46 | /** 47 | * A precondition stating the given {@link com.opencqrs.esdb.client.eventql.EventQuery} must evaluate to 48 | * {@code true}. This precondition allows for complex conditional logic when publishing events. 49 | * 50 | * @param query the EventQL query that must evaluate to {@code true} 51 | */ 52 | record EventQlQueryIsTrue(@NotNull EventQuery query) implements Precondition {} 53 | } 54 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/upcaster/EventUpcaster.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.upcaster; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | import com.opencqrs.framework.serialization.EventData; 6 | import com.opencqrs.framework.serialization.EventDataMarshaller; 7 | import jakarta.validation.constraints.NotBlank; 8 | import jakarta.validation.constraints.NotNull; 9 | import java.util.Map; 10 | import java.util.stream.Stream; 11 | 12 | /** 13 | * Interface to be implemented when {@link Event}s need to be migrated to a new representation, so-called upcasting. 14 | * This is required, as {@link Event}s are immutable and thus need to be migrated on-the-fly. 15 | * 16 | *

Event upcasting is limited to {@link Event#type()} and {@link Event#data()}, represented by {@link Result}. 17 | * 18 | *

This interface (and direct implementations) operate on the low-level {@link Event} and hence may have to 19 | * use an additional {@link EventDataMarshaller} if {@link Event#data()} needs to be upcasted. 20 | * {@link AbstractEventDataMarshallingEventUpcaster} may be inherited to gain access to {@link EventData} based 21 | * upcasting. 22 | * 23 | * @see EventUpcasters 24 | * @see AbstractEventDataMarshallingEventUpcaster 25 | */ 26 | public interface EventUpcaster { 27 | 28 | /** 29 | * Determines if {@code this} upcaster is relevant for upcasting the given {@link Event}. This method must be called 30 | * prior to {@link #upcast(Event)}. 31 | * 32 | * @param event the event that may need to be upcasted 33 | * @return {@code true} if {@code this} can upcast the event, {@code false} otherwise 34 | */ 35 | boolean canUpcast(Event event); 36 | 37 | /** 38 | * Upcasts the given event to a stream of {@link Result}s. This allows implementations to: 39 | * 40 | *

    41 | *
  • effectively drop an event by returning an empty stream 42 | *
  • upcast an event to one new event by returning a single element stream 43 | *
  • effectively split up an event by returning a multi element stream 44 | *
45 | * 46 | * @param event the event to be upcasted 47 | * @return a stream of {@link Result}s carrying the upcasted {@link Event#type()} and {@link Event#data()} 48 | */ 49 | Stream upcast(Event event); 50 | 51 | /** 52 | * Captures upcasted {@link Event#type()} and {@link Event#data()}. 53 | * 54 | * @param type the potentially modified {@link Event#type()} 55 | * @param data the potentially modified {@link Event#data()} 56 | */ 57 | record Result(@NotBlank String type, @NotNull Map data) {} 58 | } 59 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/domain/book/BookHandling.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.domain.book; 3 | 4 | import com.opencqrs.example.domain.book.api.*; 5 | import com.opencqrs.example.domain.reader.api.NoSuchReaderException; 6 | import com.opencqrs.example.projection.reader.ReaderRepository; 7 | import com.opencqrs.framework.command.*; 8 | import java.util.Set; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | 11 | @CommandHandlerConfiguration 12 | public class BookHandling { 13 | 14 | @CommandHandling(sourcingMode = SourcingMode.LOCAL) 15 | public String purchase(PurchaseBookCommand command, CommandEventPublisher publisher) { 16 | publisher.publish( 17 | new BookPurchasedEvent(command.isbn(), command.author(), command.title(), command.numPages())); 18 | 19 | return command.isbn(); 20 | } 21 | 22 | @StateRebuilding 23 | public Book on(BookPurchasedEvent event) { 24 | return new Book(event.isbn(), event.numPages(), Set.of(), new Book.Lending.Available()); 25 | } 26 | 27 | @CommandHandling 28 | public void borrow( 29 | Book book, 30 | BorrowBookCommand command, 31 | CommandEventPublisher publisher, 32 | @Autowired ReaderRepository readerRepository) { 33 | if (book.lending() instanceof Book.Lending.Lent) { 34 | throw new BookAlreadyLentException(); 35 | } 36 | 37 | if (!readerRepository.existsById(command.reader())) { 38 | throw new NoSuchReaderException(); 39 | } 40 | 41 | if (book.damagedPages().size() > 4) { 42 | throw new BookNeedsReplacementException(); 43 | } 44 | 45 | publisher.publish(new BookLentEvent(command.isbn(), command.reader())); 46 | } 47 | 48 | @StateRebuilding 49 | public Book on(Book book, BookLentEvent event) { 50 | return book.with(new Book.Lending.Lent(event.reader())); 51 | } 52 | 53 | @CommandHandling(sourcingMode = SourcingMode.LOCAL) 54 | public void returnBook(Book book, ReturnBookCommand command, CommandEventPublisher publisher) { 55 | switch (book.lending()) { 56 | case Book.Lending.Available ignored -> throw new BookNotLentException(); 57 | case Book.Lending.Lent lent -> publisher.publish(new BookReturnedEvent(command.isbn(), lent.reader())); 58 | } 59 | } 60 | 61 | @StateRebuilding 62 | public Book on(Book book, BookReturnedEvent event) { 63 | return book.with(new Book.Lending.Available()); 64 | } 65 | 66 | @StateRebuilding 67 | public Book on(Book book, BookPageDamagedEvent event) { 68 | return book.withDamagedPage(event.page()); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/eventhandler/LeaderElectionEventHandlingProcessorLifecycleController.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.eventhandler; 3 | 4 | import java.util.concurrent.CompletableFuture; 5 | import java.util.concurrent.ExecutionException; 6 | import java.util.concurrent.Future; 7 | import java.util.logging.Level; 8 | import java.util.logging.Logger; 9 | import org.springframework.integration.leader.AbstractCandidate; 10 | import org.springframework.integration.leader.Context; 11 | 12 | /** 13 | * {@link EventHandlingProcessorLifecycleController} implementation which in turn is managed by a 14 | * {@link org.springframework.integration.support.leader.LockRegistryLeaderInitiator} bean using {link 15 | * {@link #onGranted(Context)}} and {@link #onRevoked(Context)}. 16 | */ 17 | class LeaderElectionEventHandlingProcessorLifecycleController extends AbstractCandidate 18 | implements EventHandlingProcessorLifecycleController { 19 | 20 | private static final Logger log = 21 | Logger.getLogger(LeaderElectionEventHandlingProcessorLifecycleController.class.getName()); 22 | 23 | private final EventHandlingProcessor eventHandlingProcessor; 24 | private boolean running = false; 25 | 26 | LeaderElectionEventHandlingProcessorLifecycleController(EventHandlingProcessor eventHandlingProcessor) { 27 | super( 28 | null, 29 | "[group=" + eventHandlingProcessor.getGroupId() + ", partition=" + eventHandlingProcessor.getPartition() 30 | + "]"); 31 | this.eventHandlingProcessor = eventHandlingProcessor; 32 | } 33 | 34 | @Override 35 | public void onGranted(Context ctx) { 36 | log.info(() -> "leadership granted for " + eventHandlingProcessor.eventProcessorForLogs() + ": " + ctx); 37 | CompletableFuture.runAsync(() -> { 38 | try { 39 | Future started = eventHandlingProcessor.start(); 40 | running = true; 41 | started.get(); 42 | } catch (ExecutionException | InterruptedException e) { 43 | log.log(Level.INFO, eventHandlingProcessor.eventProcessorForLogs() + " prematurely terminated", e); 44 | } finally { 45 | log.info(() -> eventHandlingProcessor.eventProcessorForLogs() + " yielding leadership: " + ctx); 46 | running = false; 47 | ctx.yield(); 48 | } 49 | }); 50 | } 51 | 52 | @Override 53 | public void onRevoked(Context ctx) { 54 | log.warning(() -> "leadership revoked for " + eventHandlingProcessor.eventProcessorForLogs() + ": " + ctx); 55 | eventHandlingProcessor.stop(); 56 | running = false; 57 | ctx.yield(); 58 | } 59 | 60 | @Override 61 | public boolean isRunning() { 62 | return running; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /framework-spring-boot-autoconfigure/src/main/java/com/opencqrs/framework/reflection/AutowiredParameterResolver.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.reflection; 3 | 4 | import com.opencqrs.framework.CqrsFrameworkException; 5 | import java.lang.reflect.Method; 6 | import java.util.Map; 7 | import java.util.Set; 8 | import org.springframework.beans.BeansException; 9 | import org.springframework.beans.factory.annotation.ParameterResolutionDelegate; 10 | import org.springframework.context.ApplicationContext; 11 | import org.springframework.context.ApplicationContextAware; 12 | import org.springframework.util.ReflectionUtils; 13 | 14 | /** Base class for reflective {@link AutowiredParameter} resolution. */ 15 | public abstract class AutowiredParameterResolver implements ApplicationContextAware { 16 | 17 | private ApplicationContext applicationContext; 18 | protected final Method method; 19 | private final Set autowiredParameters; 20 | 21 | public AutowiredParameterResolver(Method method, Set autowiredParameters) { 22 | this.method = method; 23 | this.autowiredParameters = autowiredParameters; 24 | 25 | ReflectionUtils.makeAccessible(method); 26 | } 27 | 28 | @Override 29 | public void setApplicationContext(ApplicationContext applicationContext) { 30 | this.applicationContext = applicationContext; 31 | } 32 | 33 | /** 34 | * Resolves the configured {@link AutowiredParameter}s, merges them with the given parameter positions and returns 35 | * them in order. 36 | * 37 | * @param params any non-autowired positional parameter values to be included 38 | * @return an ordered array of the input and autowired params 39 | */ 40 | protected final Object[] resolveIncludingAutowiredParameters(Map params) { 41 | autowiredParameters.forEach(p -> { 42 | try { 43 | if (params.put( 44 | p.index(), 45 | ParameterResolutionDelegate.resolveDependency( 46 | p.parameter(), 47 | p.index(), 48 | p.containingClass(), 49 | applicationContext.getAutowireCapableBeanFactory())) 50 | != null) { 51 | throw new IllegalArgumentException( 52 | "conflicting parameter positions found for autowired parameter: " + p); 53 | } 54 | } catch (BeansException e) { 55 | throw new CqrsFrameworkException.NonTransientException( 56 | "could not resolve required autowired dependency for method: " + method, e); 57 | } 58 | }); 59 | 60 | return params.keySet().stream().sorted().map(params::get).toArray(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/upcaster/AbstractEventDataMarshallingEventUpcaster.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.upcaster; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | import com.opencqrs.framework.serialization.EventData; 6 | import com.opencqrs.framework.serialization.EventDataMarshaller; 7 | import jakarta.validation.constraints.NotBlank; 8 | import jakarta.validation.constraints.NotNull; 9 | import java.util.Map; 10 | import java.util.stream.Stream; 11 | 12 | /** 13 | * Template implementation of {@link EventUpcaster} that uses a delegate {@link EventDataMarshaller} to allow subclasses 14 | * to upcast {@link EventData#metaData()} and {@link EventData#payload()} as pre-extracted JSON-like {@link Map}s. 15 | * 16 | * @see #doUpcast(Event, Map, Map) 17 | */ 18 | public abstract class AbstractEventDataMarshallingEventUpcaster implements EventUpcaster { 19 | 20 | private final EventDataMarshaller eventDataMarshaller; 21 | 22 | /** 23 | * Constructor for implementations of {@code this}. 24 | * 25 | * @param eventDataMarshaller the marshaller used to extract {@link EventData} from {@link Event#data()} 26 | */ 27 | protected AbstractEventDataMarshallingEventUpcaster(EventDataMarshaller eventDataMarshaller) { 28 | this.eventDataMarshaller = eventDataMarshaller; 29 | } 30 | 31 | @Override 32 | public final Stream upcast(Event event) { 33 | EventData deserialized = eventDataMarshaller.deserialize(event.data(), Map.class); 34 | return doUpcast(event, deserialized.metaData(), (Map) deserialized.payload()) 35 | .map(o -> { 36 | Map serialized = eventDataMarshaller.serialize(new EventData<>(o.metaData, o.payload)); 37 | return new Result(o.type, serialized); 38 | }); 39 | } 40 | 41 | /** 42 | * Template method to be implemented by subclasses to upcast the given meta-data and payload. 43 | * 44 | * @param event the event from which meta-data and payload have been 45 | * {@linkplain EventDataMarshaller#deserialize(Map, Class) extracted} 46 | * @param metaData the meta-data 47 | * @param payload the payload as JSON-like map 48 | * @return a stream of {@link MetaDataAndPayloadResult} carrying the upcasted representations 49 | * @see EventUpcaster#upcast(Event) 50 | */ 51 | protected abstract Stream doUpcast( 52 | Event event, Map metaData, Map payload); 53 | 54 | /** 55 | * Captures upcasted {@link Event#type()}, meta-data, and payload. 56 | * 57 | * @param type the upcasted {@link Event#type()} 58 | * @param metaData the upcasted meta-data 59 | * @param payload the upcasted payload 60 | */ 61 | public record MetaDataAndPayloadResult( 62 | @NotBlank String type, @NotNull Map metaData, @NotNull Map payload) {} 63 | } 64 | -------------------------------------------------------------------------------- /framework/src/main/java/com/opencqrs/framework/persistence/ImmediateEventPublisher.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.persistence; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | import com.opencqrs.esdb.client.Precondition; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.function.Consumer; 9 | 10 | /** Interface specifying operations for immediate, atomic event publication. */ 11 | public interface ImmediateEventPublisher { 12 | 13 | /** 14 | * Publishes the given event onto the given subject. 15 | * 16 | * @param subject the absolute subject path 17 | * @param event the event object 18 | * @return the published {@link Event} 19 | * @param the generic event type 20 | */ 21 | default Event publish(String subject, E event) { 22 | return publish(subject, event, Map.of(), List.of()); 23 | } 24 | 25 | /** 26 | * Publishes the given event including its meta-data onto the given subject with preconditions. 27 | * 28 | * @param subject the absolute subject path 29 | * @param event the event object 30 | * @param metaData the event meta-data 31 | * @param preconditions the preconditions that must not be violated 32 | * @return the published {@link Event} 33 | * @param the generic event type 34 | */ 35 | default Event publish(String subject, E event, Map metaData, List preconditions) { 36 | return publish(eventPublisher -> eventPublisher.publish(subject, event, metaData, preconditions)) 37 | .getFirst(); 38 | } 39 | 40 | /** 41 | * Atomically publishes all events captured from the given {@link EventPublisher} consumer. 42 | * 43 | * @param handler callback capturing all events to be published atomically 44 | * @return the list of published {@link Event}s 45 | */ 46 | default List publish(Consumer handler) { 47 | return publish(handler, List.of()); 48 | } 49 | 50 | /** 51 | * Atomically publishes all events captured from the given {@link EventPublisher} consumer together with the 52 | * additional preconditions. 53 | * 54 | * @param handler callback capturing all events to be published atomically 55 | * @param additionalPreconditions additional preconditions that must not be violated 56 | * @return the list of published {@link Event}s 57 | */ 58 | List publish(Consumer handler, List additionalPreconditions); 59 | 60 | /** 61 | * Atomically publishes the given events together with the additional preconditions. 62 | * 63 | * @param events a list of captured events to be published atomically 64 | * @param additionalPreconditions additional preconditions that must not be violated 65 | * @return the list of published {@link Event}s 66 | */ 67 | List publish(List events, List additionalPreconditions); 68 | } 69 | -------------------------------------------------------------------------------- /framework-test/src/test/java/com/opencqrs/framework/command/CommandHandlingTestSliceTest.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 6 | 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import com.opencqrs.esdb.client.EsdbClient; 9 | import com.opencqrs.esdb.client.EsdbClientAutoConfiguration; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.params.ParameterizedTest; 12 | import org.junit.jupiter.params.provider.ValueSource; 13 | import org.springframework.beans.factory.NoSuchBeanDefinitionException; 14 | import org.springframework.beans.factory.ObjectProvider; 15 | import org.springframework.beans.factory.UnsatisfiedDependencyException; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.context.ApplicationContext; 18 | 19 | @CommandHandlingTest 20 | public class CommandHandlingTestSliceTest { 21 | 22 | @Autowired 23 | private ApplicationContext context; 24 | 25 | @Test 26 | public void fixtureAutoCreatedWithProperGenericTypes_beanNoDependency( 27 | @Autowired ObjectProvider> fixture) { 28 | assertThat(fixture.getIfAvailable()).isNotNull(); 29 | } 30 | 31 | @Test 32 | public void fixtureAutoCreatedWithProperGenericTypes_beanUnresolvableDependency( 33 | @Autowired ObjectProvider> fixture) { 34 | assertThatThrownBy(fixture::getIfAvailable) 35 | .hasCauseInstanceOf(UnsatisfiedDependencyException.class) 36 | .hasMessageContaining("chdUnresolvableDependency"); 37 | } 38 | 39 | @Test 40 | public void fixtureAutoCreatedWithProperGenericTypes_beanCommandHandling( 41 | @Autowired ObjectProvider> fixture) { 42 | assertThat(fixture.getIfAvailable()).isNotNull(); 43 | } 44 | 45 | @Test 46 | public void fixtureAutoCreatedWithProperGenericTypes_programmaticBeanRegistration( 47 | @Autowired ObjectProvider> fixture) { 48 | assertThat(fixture.getIfAvailable()).isNotNull(); 49 | } 50 | 51 | @Test 52 | public void fixtureNotCreated(@Autowired ObjectProvider> fixture) { 53 | assertThat(fixture.getIfAvailable()).isNull(); 54 | } 55 | 56 | @ParameterizedTest 57 | @ValueSource( 58 | classes = { 59 | CommandRouterAutoConfiguration.class, 60 | EsdbClientAutoConfiguration.class, 61 | CommandRouter.class, 62 | EsdbClient.class, 63 | ObjectMapper.class 64 | }) 65 | public void unnecessaryBeansIgnored(Class bean) { 66 | assertThatThrownBy(() -> context.getBean(bean)).isInstanceOf(NoSuchBeanDefinitionException.class); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /mkdocs/docs/index.md: -------------------------------------------------------------------------------- 1 | # {{ custom.framework_name }} Documentation 2 | 3 | Welcome to the official documentation for **{{ custom.framework_name }}** – an **open-source application framework** for building systems with **CQRS** and **event sourcing** on the **JVM**. 4 | 5 | {{ custom.framework_name }} is designed to work seamlessly with **{{ esdb_ref() }}**, making it straightforward to model, implement, and operate event-driven applications. Built with **Java** and **Kotlin** developers in mind, it provides ready-to-use building blocks for **commands, events, aggregates, projections, and testing** – all with a strong focus on **clarity, testability, and maintainability**. 6 | 7 | {{ custom.framework_name }} is developed and maintained by the German IT consultancy **[Digital Frontiers GmbH & Co. KG](https://www.digitalfrontiers.de/)** and released as **open source software** under the **Apache 2.0 license**. It is free to use and backed by professional expertise and consulting services when you need them. 8 | 9 | ## Learning the Concepts 10 | 11 | New to **CQRS** or **event sourcing**, or wondering how {{ custom.framework_name }} can help you build applications? Start here: 12 | 13 | - **[Getting Started](tutorials/README.md)** 14 | 15 | Step-by-step tutorials that guide you through building your first applications with {{ custom.framework_name }}. 16 | 17 | - **[Guides and How-Tos](howto/README.md)** 18 | 19 | Practical recipes for solving common problems and implementing specific features. 20 | 21 | - **[Reference](reference/README.md)** 22 | 23 | Detailed reference documentation for **core framework components**, **command and event handling extension points**, the **ESDB client SDK**, and **test support**. 24 | 25 | - **[Concepts](concepts/README.md)** 26 | 27 | Background explanations of the core ideas behind {{ custom.framework_name }} and its design principles. 28 | 29 | !!! tip "Need a refresher on CQRS or Event Sourcing?" 30 | If you are new to the underlying concepts, visit **[CQRS.com](https://www.cqrs.com)** for an in-depth introduction to **CQRS** and **event sourcing**. 31 | 32 | ## Platform and Integration 33 | 34 | {{ custom.framework_name }} runs on the **Java Virtual Machine** and is fully compatible with JVM-based languages such as **Java** and **Kotlin**. It integrates seamlessly into modern application stacks: 35 | 36 | - Available from **Maven Central** for both **Maven** and **Gradle** builds 37 | - Smooth integration with **Spring Boot** for rapid application development and production deployment 38 | - Native support for **{{ esdb_ref() }}** as the underlying event store, with a dedicated **Java client SDK** 39 | - Built-in support for **testing command logic** with fixtures and utilities 40 | - **Modular architecture** for extension and customization 41 | 42 | ## Need Support? 43 | 44 | If you or your team need help **designing, integrating, or scaling** applications with {{ custom.framework_name }}, the team behind the framework is here to assist. Just reach out at **[opencqrs@digitalfrontiers.de](mailto:opencqrs@digitalfrontiers.de)**. 45 | -------------------------------------------------------------------------------- /framework-test/src/test/java/com/opencqrs/framework/command/CommandHandlingConfiguration.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.framework.command; 3 | 4 | import com.opencqrs.framework.MyEvent; 5 | import com.opencqrs.framework.State; 6 | import org.springframework.beans.factory.config.ConstructorArgumentValues; 7 | import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; 8 | import org.springframework.beans.factory.support.RootBeanDefinition; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.core.ResolvableType; 12 | 13 | @CommandHandlerConfiguration 14 | public class CommandHandlingConfiguration { 15 | 16 | @Bean 17 | public StateRebuildingHandlerDefinition myStateRebuildingHandlerDefinition() { 18 | return new StateRebuildingHandlerDefinition<>( 19 | State.class, MyEvent.class, (StateRebuildingHandler.FromObject) 20 | (instance, event) -> instance); 21 | } 22 | 23 | @Bean 24 | public CommandHandlerDefinition chdNoDependency() { 25 | return new CommandHandlerDefinition<>( 26 | State.class, MyCommand1.class, (CommandHandler.ForCommand) 27 | (command, commandEventPublisher) -> null); 28 | } 29 | 30 | @Bean 31 | public CommandHandlerDefinition chdUnresolvableDependency(Runnable noSuchBean) { 32 | return new CommandHandlerDefinition<>( 33 | State.class, MyCommand2.class, (CommandHandler.ForCommand) 34 | (command, commandEventPublisher) -> null); 35 | } 36 | 37 | @CommandHandling 38 | public String handle(State instance, MyCommand3 command) { 39 | return "test"; 40 | } 41 | 42 | @Configuration 43 | public static class MyConfig { 44 | 45 | @Bean 46 | public static BeanDefinitionRegistryPostProcessor programmaticCommandHandlerDefinitionRegistration() { 47 | return registry -> { 48 | RootBeanDefinition chd = new RootBeanDefinition(); 49 | chd.setBeanClass(CommandHandlerDefinition.class); 50 | chd.setTargetType(ResolvableType.forClassWithGenerics( 51 | CommandHandlerDefinition.class, State.class, MyCommand4.class, Void.class)); 52 | 53 | ConstructorArgumentValues values = new ConstructorArgumentValues(); 54 | values.addGenericArgumentValue(State.class); 55 | values.addGenericArgumentValue(MyCommand4.class); 56 | values.addGenericArgumentValue( 57 | (CommandHandler.ForCommand) (command, commandEventPublisher) -> null); 58 | 59 | chd.setConstructorArgumentValues(values); 60 | registry.registerBeanDefinition("myProgrammaticCommandHandlerDefinition", chd); 61 | }; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /example-application/src/main/java/com/opencqrs/example/configuration/CqrsConfiguration.java: -------------------------------------------------------------------------------- 1 | /* Copyright (C) 2025 OpenCQRS and contributors */ 2 | package com.opencqrs.example.configuration; 3 | 4 | import com.opencqrs.esdb.client.Event; 5 | import com.opencqrs.example.domain.book.api.BookLentEvent; 6 | import com.opencqrs.example.domain.book.api.BookPageDamagedEvent; 7 | import com.opencqrs.example.domain.book.api.BookPurchasedEvent; 8 | import com.opencqrs.example.domain.book.api.BookReturnedEvent; 9 | import com.opencqrs.example.domain.reader.api.ReaderRegisteredEvent; 10 | import com.opencqrs.framework.eventhandler.EventHandling; 11 | import com.opencqrs.framework.eventhandler.progress.JdbcProgressTracker; 12 | import com.opencqrs.framework.persistence.EventSource; 13 | import com.opencqrs.framework.types.PreconfiguredAssignableClassEventTypeResolver; 14 | import java.util.Map; 15 | import javax.sql.DataSource; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | import org.springframework.context.annotation.Bean; 19 | import org.springframework.context.annotation.Configuration; 20 | import org.springframework.integration.jdbc.lock.DefaultLockRepository; 21 | import org.springframework.integration.jdbc.lock.JdbcLockRegistry; 22 | import org.springframework.integration.jdbc.lock.LockRepository; 23 | import org.springframework.transaction.PlatformTransactionManager; 24 | 25 | @Configuration 26 | public class CqrsConfiguration { 27 | 28 | private static final Logger log = LoggerFactory.getLogger(CqrsConfiguration.class); 29 | 30 | @Bean 31 | public DefaultLockRepository defaultLockRepository(DataSource dataSource) { 32 | var result = new DefaultLockRepository(dataSource); 33 | 34 | result.setPrefix("EVENTHANDLER_"); 35 | return result; 36 | } 37 | 38 | @Bean 39 | public JdbcLockRegistry jdbcLockRegistry(LockRepository lockRepository) { 40 | return new JdbcLockRegistry(lockRepository); 41 | } 42 | 43 | @Bean 44 | public JdbcProgressTracker jdbcProgressTracker( 45 | DataSource dataSource, PlatformTransactionManager transactionManager) { 46 | return new JdbcProgressTracker(dataSource, transactionManager); 47 | } 48 | 49 | @Bean 50 | public PreconfiguredAssignableClassEventTypeResolver eventTypeResolver() { 51 | return new PreconfiguredAssignableClassEventTypeResolver(Map.of( 52 | "com.opencqrs.example.library.reader.registered.v1", ReaderRegisteredEvent.class, 53 | "com.opencqrs.example.library.book.purchased.v1", BookPurchasedEvent.class, 54 | "com.opencqrs.example.library.book.lent.v1", BookLentEvent.class, 55 | "com.opencqrs.example.library.book.returned.v1", BookReturnedEvent.class, 56 | "com.opencqrs.example.library.book.page.damaged.v1", BookPageDamagedEvent.class)); 57 | } 58 | 59 | @Bean 60 | public EventSource eventSource() { 61 | return new EventSource("tag://service-spring-library"); 62 | } 63 | 64 | @EventHandling("logging") 65 | public void on(Event event) { 66 | log.info("event published: {}", event); 67 | } 68 | } 69 | --------------------------------------------------------------------------------