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