├── doc ├── cqrs-overview.png └── cqrs-overview-small.png ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── quarkus ├── query │ ├── doc │ │ ├── cdi-view.png │ │ └── cdi-view-small.png │ ├── .mvn │ │ └── wrapper │ │ │ ├── maven-wrapper.jar │ │ │ └── maven-wrapper.properties │ ├── src │ │ └── main │ │ │ ├── java │ │ │ └── org │ │ │ │ └── fuin │ │ │ │ └── cqrs4j │ │ │ │ └── example │ │ │ │ └── quarkus │ │ │ │ └── query │ │ │ │ ├── views │ │ │ │ ├── personlist │ │ │ │ │ ├── package-info.java │ │ │ │ │ ├── PersonCreatedEventHandler.java │ │ │ │ │ ├── PersonDeletedEventHandler.java │ │ │ │ │ ├── PersonListResource.java │ │ │ │ │ ├── PersonListEventDispatcher.java │ │ │ │ │ ├── PersonListEventChunkHandler.java │ │ │ │ │ ├── PersonListEntry.java │ │ │ │ │ └── PersonListProjector.java │ │ │ │ ├── package-info.java │ │ │ │ ├── statistic │ │ │ │ │ ├── Statistic.java │ │ │ │ │ ├── QryStatisticResource.java │ │ │ │ │ ├── EntityType.java │ │ │ │ │ ├── StatisticEntity.java │ │ │ │ │ └── StatisticView.java │ │ │ │ └── common │ │ │ │ │ ├── QryProjectionPositionRepository.java │ │ │ │ │ └── QryProjectionPosition.java │ │ │ │ └── app │ │ │ │ ├── QryCheckForViewUpdatesEvent.java │ │ │ │ ├── QryApp.java │ │ │ │ └── QryScheduler.java │ │ │ └── resources │ │ │ ├── application.properties │ │ │ └── META-INF │ │ │ └── resources │ │ │ └── index.html │ └── README.md ├── command │ ├── doc │ │ ├── cdi-command.png │ │ └── cdi-command-small.png │ ├── .mvn │ │ └── wrapper │ │ │ ├── maven-wrapper.jar │ │ │ └── maven-wrapper.properties │ ├── src │ │ ├── main │ │ │ ├── java │ │ │ │ └── org │ │ │ │ │ └── fuin │ │ │ │ │ └── cqrs4j │ │ │ │ │ └── example │ │ │ │ │ └── quarkus │ │ │ │ │ └── command │ │ │ │ │ ├── domain │ │ │ │ │ ├── package-info.java │ │ │ │ │ ├── PersonRepository.java │ │ │ │ │ ├── PersonRepositoryFactory.java │ │ │ │ │ ├── EventStorePersonRepository.java │ │ │ │ │ ├── DuplicatePersonNameException.java │ │ │ │ │ └── Person.java │ │ │ │ │ ├── app │ │ │ │ │ └── CmdApp.java │ │ │ │ │ └── api │ │ │ │ │ ├── AggregateDeletedExceptionMapper.java │ │ │ │ │ ├── AggregateNotFoundExceptionMapper.java │ │ │ │ │ ├── AggregateAlreadyExistsExceptionMapper.java │ │ │ │ │ ├── AggregateVersionConflictExceptionMapper.java │ │ │ │ │ ├── AggregateVersionNotFoundExceptionMapper.java │ │ │ │ │ ├── CommandExecutionFailedExceptionMapper.java │ │ │ │ │ ├── ConstraintViolationExceptionMapper.java │ │ │ │ │ └── PersonResource.java │ │ │ └── resources │ │ │ │ ├── META-INF │ │ │ │ └── resources │ │ │ │ │ └── index.html │ │ │ │ ├── application.properties │ │ │ │ └── reflection-config.json │ │ └── test │ │ │ └── java │ │ │ └── org │ │ │ └── fuin │ │ │ └── cqrs4j │ │ │ └── example │ │ │ └── quarkus │ │ │ └── command │ │ │ └── domain │ │ │ └── PersonTest.java │ └── README.md ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── shared │ ├── src │ │ ├── main │ │ │ ├── java │ │ │ │ └── org │ │ │ │ │ └── fuin │ │ │ │ │ └── cqrs4j │ │ │ │ │ └── example │ │ │ │ │ └── quarkus │ │ │ │ │ └── shared │ │ │ │ │ ├── package-info.java │ │ │ │ │ ├── JsonbFactory.java │ │ │ │ │ ├── HttpClientFactory.java │ │ │ │ │ ├── SerDeserializerRegistryFactory.java │ │ │ │ │ ├── ProjectionAdminEventStoreFactory.java │ │ │ │ │ ├── EventStoreFactory.java │ │ │ │ │ ├── CreatePersonCommand.java │ │ │ │ │ ├── PersonDeletedEvent.java │ │ │ │ │ ├── PersonCreatedEvent.java │ │ │ │ │ ├── DeletePersonCommand.java │ │ │ │ │ └── PersonId.java │ │ │ └── resources │ │ │ │ └── META-INF │ │ │ │ └── beans.xml │ │ └── test │ │ │ └── java │ │ │ └── org │ │ │ └── fuin │ │ │ └── cqrs4j │ │ │ └── example │ │ │ └── quarkus │ │ │ └── shared │ │ │ ├── PersonIdTest.java │ │ │ ├── PersonCreatedEventTest.java │ │ │ ├── CreatePersonCommandTest.java │ │ │ └── PersonDeletedEventTest.java │ └── README.md ├── README.md └── pom.xml ├── .gitignore ├── spring-boot ├── query │ ├── doc │ │ ├── spring-view.png │ │ └── spring-view-small.png │ ├── .mvn │ │ └── wrapper │ │ │ ├── maven-wrapper.jar │ │ │ └── maven-wrapper.properties │ ├── src │ │ └── main │ │ │ ├── java │ │ │ └── org │ │ │ │ └── fuin │ │ │ │ └── cqrs4j │ │ │ │ └── example │ │ │ │ └── spring │ │ │ │ └── query │ │ │ │ ├── views │ │ │ │ ├── statistics │ │ │ │ │ ├── package-info.java │ │ │ │ │ ├── Statistic.java │ │ │ │ │ ├── EntityType.java │ │ │ │ │ ├── QryStatisticController.java │ │ │ │ │ ├── StatisticView.java │ │ │ │ │ └── StatisticEntity.java │ │ │ │ ├── personlist │ │ │ │ │ ├── package-info.java │ │ │ │ │ ├── Person.java │ │ │ │ │ ├── PersonCreatedEventHandler.java │ │ │ │ │ ├── PersonDeletedEventHandler.java │ │ │ │ │ ├── PersonListView.java │ │ │ │ │ ├── PersonListEntry.java │ │ │ │ │ └── PersonListController.java │ │ │ │ └── package-info.java │ │ │ │ └── app │ │ │ │ ├── QryBeanConfig.java │ │ │ │ └── QryApplication.java │ │ │ └── resources │ │ │ ├── application.properties │ │ │ └── public │ │ │ └── index.html │ └── README.md ├── command │ ├── src │ │ ├── main │ │ │ ├── resources │ │ │ │ ├── application.properties │ │ │ │ └── public │ │ │ │ │ └── index.html │ │ │ └── java │ │ │ │ └── org │ │ │ │ └── fuin │ │ │ │ └── cqrs4j │ │ │ │ └── example │ │ │ │ └── spring │ │ │ │ └── command │ │ │ │ ├── domain │ │ │ │ ├── package-info.java │ │ │ │ ├── PersonRepository.java │ │ │ │ ├── EventStorePersonRepository.java │ │ │ │ ├── DuplicatePersonNameException.java │ │ │ │ ├── CreatePersonCommand.java │ │ │ │ ├── DeletePersonCommand.java │ │ │ │ └── Person.java │ │ │ │ └── app │ │ │ │ └── CmdApplication.java │ │ └── test │ │ │ └── java │ │ │ └── org │ │ │ └── fuin │ │ │ └── cqrs4j │ │ │ └── example │ │ │ └── spring │ │ │ └── command │ │ │ └── domain │ │ │ ├── PersonTest.java │ │ │ └── CreatePersonCommandTest.java │ ├── doc │ │ ├── spring-command.png │ │ └── spring-command-small.png │ ├── .mvn │ │ └── wrapper │ │ │ ├── maven-wrapper.jar │ │ │ └── maven-wrapper.properties │ └── README.md ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── shared │ ├── .mvn │ │ └── wrapper │ │ │ ├── maven-wrapper.jar │ │ │ └── maven-wrapper.properties │ ├── src │ │ ├── main │ │ │ └── java │ │ │ │ └── org │ │ │ │ └── fuin │ │ │ │ └── cqrs4j │ │ │ │ └── example │ │ │ │ └── spring │ │ │ │ └── shared │ │ │ │ ├── package-info.java │ │ │ │ ├── SharedJacksonModule.java │ │ │ │ ├── PersonId.java │ │ │ │ ├── PersonDeletedEvent.java │ │ │ │ ├── PersonCreatedEvent.java │ │ │ │ └── Config.java │ │ └── test │ │ │ └── java │ │ │ └── org │ │ │ └── fuin │ │ │ └── cqrs4j │ │ │ └── example │ │ │ └── spring │ │ │ └── shared │ │ │ ├── ArchitectureTest.java │ │ │ ├── PersonIdTest.java │ │ │ ├── PersonCreatedEventTest.java │ │ │ └── PersonDeletedEventTest.java │ └── README.md └── README.md ├── demo ├── create-persons.sh ├── delete-harry-osborn.sh ├── create-harry-osborn-command.json ├── create-peter-parker-command.json ├── create-mary-jane-watson-command.json └── delete-harry-osborn-command.json ├── quarkus.md ├── spring-boot.md ├── .github └── workflows │ └── maven.yml └── docker-compose.yml /doc/cqrs-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuinorg/ddd-cqrs-4-java-example/HEAD/doc/cqrs-overview.png -------------------------------------------------------------------------------- /doc/cqrs-overview-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuinorg/ddd-cqrs-4-java-example/HEAD/doc/cqrs-overview-small.png -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuinorg/ddd-cqrs-4-java-example/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /quarkus/query/doc/cdi-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuinorg/ddd-cqrs-4-java-example/HEAD/quarkus/query/doc/cdi-view.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.gitignore 3 | !.gitkeep 4 | !.mvn 5 | !.github 6 | target 7 | *.log 8 | *.iml 9 | pom.xml.versionsBackup 10 | -------------------------------------------------------------------------------- /quarkus/command/doc/cdi-command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuinorg/ddd-cqrs-4-java-example/HEAD/quarkus/command/doc/cdi-command.png -------------------------------------------------------------------------------- /quarkus/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuinorg/ddd-cqrs-4-java-example/HEAD/quarkus/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /quarkus/query/doc/cdi-view-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuinorg/ddd-cqrs-4-java-example/HEAD/quarkus/query/doc/cdi-view-small.png -------------------------------------------------------------------------------- /spring-boot/query/doc/spring-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuinorg/ddd-cqrs-4-java-example/HEAD/spring-boot/query/doc/spring-view.png -------------------------------------------------------------------------------- /spring-boot/command/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | server.port = 8081 3 | #spring.mvc.converters.preferred-json-mapper=jsonb 4 | -------------------------------------------------------------------------------- /quarkus/command/doc/cdi-command-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuinorg/ddd-cqrs-4-java-example/HEAD/quarkus/command/doc/cdi-command-small.png -------------------------------------------------------------------------------- /spring-boot/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuinorg/ddd-cqrs-4-java-example/HEAD/spring-boot/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /spring-boot/command/doc/spring-command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuinorg/ddd-cqrs-4-java-example/HEAD/spring-boot/command/doc/spring-command.png -------------------------------------------------------------------------------- /spring-boot/query/doc/spring-view-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuinorg/ddd-cqrs-4-java-example/HEAD/spring-boot/query/doc/spring-view-small.png -------------------------------------------------------------------------------- /quarkus/query/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuinorg/ddd-cqrs-4-java-example/HEAD/quarkus/query/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /quarkus/command/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuinorg/ddd-cqrs-4-java-example/HEAD/quarkus/command/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /spring-boot/command/doc/spring-command-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuinorg/ddd-cqrs-4-java-example/HEAD/spring-boot/command/doc/spring-command-small.png -------------------------------------------------------------------------------- /spring-boot/query/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuinorg/ddd-cqrs-4-java-example/HEAD/spring-boot/query/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /spring-boot/command/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuinorg/ddd-cqrs-4-java-example/HEAD/spring-boot/command/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /spring-boot/shared/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuinorg/ddd-cqrs-4-java-example/HEAD/spring-boot/shared/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /demo/create-persons.sh: -------------------------------------------------------------------------------- 1 | for file in ./create-*-command.json 2 | do 3 | curl -i \ 4 | -H "Content-Type:application/json" \ 5 | -d "@$file" \ 6 | "http://localhost:8081/persons/create" 7 | done 8 | -------------------------------------------------------------------------------- /demo/delete-harry-osborn.sh: -------------------------------------------------------------------------------- 1 | curl -i \ 2 | -X DELETE \ 3 | -H "Content-Type:application/json" \ 4 | -d "@delete-harry-osborn-command.json" \ 5 | "http://localhost:8081/persons/954177c4-aeb7-4d1e-b6d7-3e02fe9432cb" 6 | -------------------------------------------------------------------------------- /spring-boot/shared/src/main/java/org/fuin/cqrs4j/example/spring/shared/package-info.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.shared; 2 | 3 | /** 4 | * Spring Boot specific code to be shared between all modules. 5 | */ 6 | -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/views/personlist/package-info.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.views.personlist; 2 | 3 | /** 4 | * Classes building the 'person list' view. 5 | */ 6 | -------------------------------------------------------------------------------- /quarkus/shared/src/main/java/org/fuin/cqrs4j/example/quarkus/shared/package-info.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.shared; 2 | 3 | /** 4 | * Quarkus specific code to be shared between command / query service. 5 | */ 6 | -------------------------------------------------------------------------------- /spring-boot/query/src/main/java/org/fuin/cqrs4j/example/spring/query/views/statistics/package-info.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.query.views.statistics; 2 | 3 | /** 4 | * Classes building the 'statistic' view. 5 | */ 6 | -------------------------------------------------------------------------------- /spring-boot/query/src/main/java/org/fuin/cqrs4j/example/spring/query/views/personlist/package-info.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.query.views.personlist; 2 | 3 | /** 4 | * Classes building the 'person list' view. 5 | */ 6 | -------------------------------------------------------------------------------- /quarkus/command/src/main/java/org/fuin/cqrs4j/example/quarkus/command/domain/package-info.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.command.domain; 2 | 3 | /** 4 | * Domain specific code like aggregate root classes and their repositories. 5 | */ 6 | -------------------------------------------------------------------------------- /spring-boot/command/src/main/java/org/fuin/cqrs4j/example/spring/command/domain/package-info.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.command.domain; 2 | 3 | /** 4 | * Domain specific code like aggregate root classes and their repositories. 5 | */ 6 | -------------------------------------------------------------------------------- /demo/create-harry-osborn-command.json: -------------------------------------------------------------------------------- 1 | { 2 | "event-id": "5601097d-6e2e-4df1-a7b2-ecc4f443c068", 3 | "event-timestamp": "2024-01-07T10:00:00.000+01:00[Europe/Berlin]", 4 | "entity-id-path": "PERSON 954177c4-aeb7-4d1e-b6d7-3e02fe9432cb", 5 | "name": "Harry Osborn" 6 | } -------------------------------------------------------------------------------- /demo/create-peter-parker-command.json: -------------------------------------------------------------------------------- 1 | { 2 | "event-id": "109a77b2-1de2-46fc-aee1-97fa7740a552", 3 | "event-timestamp": "2019-11-17T10:27:13.183+01:00[Europe/Berlin]", 4 | "entity-id-path": "PERSON 84565d62-115e-4502-b7c9-38ad69c64b05", 5 | "name": "Peter Parker" 6 | } -------------------------------------------------------------------------------- /demo/create-mary-jane-watson-command.json: -------------------------------------------------------------------------------- 1 | { 2 | "event-id": "4bf5bb56-4fe8-47a3-8358-25144e15497d", 3 | "event-timestamp": "2024-01-07T09:00:00.000+01:00[Europe/Berlin]", 4 | "entity-id-path": "PERSON 568df38c-fdc3-4f60-81aa-d3cce9ebfd7b", 5 | "name": "Mary Jane Watson" 6 | } -------------------------------------------------------------------------------- /spring-boot/shared/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/app/QryCheckForViewUpdatesEvent.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.app; 2 | 3 | /** 4 | * Notifies the projectors to check for view updates. 5 | */ 6 | public class QryCheckForViewUpdatesEvent { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /demo/delete-harry-osborn-command.json: -------------------------------------------------------------------------------- 1 | { 2 | "event-id": "109a77b2-1de2-46fc-aee1-97fa7740a552", 3 | "event-timestamp": "2019-11-17T10:27:13.183+01:00[Europe/Berlin]", 4 | "entity-id-path": "PERSON 954177c4-aeb7-4d1e-b6d7-3e02fe9432cb", 5 | "aggregate-version": 0, 6 | "name": "Harry Osborn" 7 | } -------------------------------------------------------------------------------- /quarkus/shared/src/main/resources/META-INF/beans.xml: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/views/package-info.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.views; 2 | 3 | /** 4 | * Contains the views used in this query application. A view never uses code of another view, 5 | * means all views are completely independent of each other. As an exception, the 'commons' package 6 | * has some small classes that are not view specific. 7 | */ 8 | -------------------------------------------------------------------------------- /spring-boot/query/src/main/java/org/fuin/cqrs4j/example/spring/query/views/package-info.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.query.views; 2 | 3 | /** 4 | * Contains the views used in this query application. A view never uses code of another view, 5 | * means all views are completely independent of each other. As an exception, the 'commons' package 6 | * has some small classes that are not view specific. 7 | */ 8 | -------------------------------------------------------------------------------- /quarkus/command/src/main/java/org/fuin/cqrs4j/example/quarkus/command/domain/PersonRepository.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.command.domain; 2 | 3 | import org.fuin.cqrs4j.example.shared.PersonId; 4 | import org.fuin.ddd4j.core.Repository; 5 | 6 | /** 7 | * Event sourced repository for storing a {@link Person} aggregate. 8 | */ 9 | public interface PersonRepository extends Repository { 10 | 11 | } -------------------------------------------------------------------------------- /spring-boot/command/src/main/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CQRS Spring Boot Example Command Module 6 | 7 | 8 |

CQRS Spring Boot Example Command Module

9 |

This page is served by Spring Boot. The source is in src/main/resources/public/index.html.

10 | 11 | 12 | -------------------------------------------------------------------------------- /quarkus/command/src/main/resources/META-INF/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CQRS Quarkus Example Command Module 6 | 7 | 8 |

CQRS Quarkus Example Command Module

9 |

This page is served by Quarkus. The source is in src/main/resources/META-INF/resources/index.html.

10 | 11 | 12 | -------------------------------------------------------------------------------- /spring-boot/command/src/main/java/org/fuin/cqrs4j/example/spring/command/domain/PersonRepository.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.command.domain; 2 | 3 | import org.fuin.cqrs4j.example.spring.shared.PersonId; 4 | import org.fuin.ddd4j.core.Repository; 5 | 6 | /** 7 | * Event sourced repository for storing a {@link Person} aggregate. 8 | */ 9 | public interface PersonRepository extends Repository { 10 | 11 | } -------------------------------------------------------------------------------- /quarkus/command/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Configuration file 2 | # key = value 3 | 4 | quarkus.http.port=8081 5 | 6 | quarkus.native.additional-build-args=-H:ReflectionConfigurationFiles=reflection-config.json,--initialize-at-run-time=org.apache.http.impl.auth.NTLMEngineImpl 7 | quarkus.native.native-image-xmx=8192m 8 | 9 | quarkus.index-dependency.ddd4j.group-id=org.fuin 10 | quarkus.index-dependency.ddd4j.artifact-id=ddd-4-java 11 | -------------------------------------------------------------------------------- /quarkus/query/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Configuration file 2 | 3 | quarkus.datasource.db-kind=mariadb 4 | quarkus.datasource.username=mary 5 | quarkus.datasource.password=abc 6 | quarkus.datasource.jdbc.url=jdbc:mariadb://localhost:3306/querydb 7 | 8 | quarkus.hibernate-orm.database.generation=drop-and-create 9 | 10 | quarkus.log.level=INFO 11 | 12 | quarkus.native.additional-build-args=--initialize-at-run-time=org.apache.http.impl.auth.NTLMEngineImpl 13 | quarkus.native.native-image-xmx=8192m -------------------------------------------------------------------------------- /quarkus/shared/README.md: -------------------------------------------------------------------------------- 1 | # cqrs4j-quarkus-example-shared 2 | Code shared between Quarkus command and query microservice. 3 | 4 | > :warning: Normally you should **not** share code this way between your command and query microservices. A shared-nothing approach is a good practice for microservices (See for example [Don't Share Libraries among Microservices](https://phauer.com/2016/dont-share-libraries-among-microservices/)). You can simply copy the content of this module directly into your command and query projects. 5 | -------------------------------------------------------------------------------- /spring-boot/shared/README.md: -------------------------------------------------------------------------------- 1 | # cqrs4j-spring-example-shared 2 | Code shared between Spring Boot command and query microservice. 3 | 4 | > :warning: Normally you should **not** share code this way between your command and query microservices. A shared-nothing approach is a good practice for microservices (See for example [Don't Share Libraries among Microservices](https://phauer.com/2016/dont-share-libraries-among-microservices/)). You can simply copy the content of this module directly into your command and query projects. 5 | -------------------------------------------------------------------------------- /spring-boot/query/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | spring.datasource.url=jdbc:mariadb://localhost:3306/querydb 3 | spring.datasource.driverClassName=org.mariadb.jdbc.Driver 4 | spring.datasource.username=mary 5 | spring.datasource.password=abc 6 | spring.jpa.database-platform=org.hibernate.dialect.MariaDBDialect 7 | spring.jpa.hibernate.ddl-auto=create-drop 8 | spring.jpa.show-sql=false 9 | spring.h2.console.enabled=false 10 | 11 | server.port = 8080 12 | spring.mvc.converters.preferred-json-mapper=jsonb 13 | spring.jpa.open-in-view=false 14 | -------------------------------------------------------------------------------- /quarkus/README.md: -------------------------------------------------------------------------------- 1 | # cqrs4j-quarkus-example 2 | Example applications that uses [Quarkus](https://quarkus.io/), [ddd-4-java](https://github.com/fuinorg/ddd-4-java) and [cqrs-4-java](https://github.com/fuinorg/cqrs-4-java) libraries. Events are stored in an [EventStore](https://eventstore.org/) and the query data is retrieved from a [MariaDB](https://mariadb.org/) database. 3 | 4 | ## Prerequisites 5 | Make sure you installed everything as described [here](../../../). 6 | 7 | ## Start command / query implementation 8 | Start the command and query microservice. 9 | - [Command](command) 10 | - [Query](query) 11 | -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/app/QryApp.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.app; 2 | 3 | import io.quarkus.runtime.Quarkus; 4 | import io.quarkus.runtime.annotations.QuarkusMain; 5 | 6 | /** 7 | * Represents the (custom) entry point, most likely used to the Quarkus application in the IDE. 8 | */ 9 | @QuarkusMain 10 | public class QryApp { 11 | 12 | /** 13 | * Main method to start the app. 14 | * 15 | * @param args Arguments from the command line. 16 | */ 17 | public static void main(String[] args) { 18 | Quarkus.run(args); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /quarkus/command/src/main/java/org/fuin/cqrs4j/example/quarkus/command/app/CmdApp.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.command.app; 2 | 3 | import io.quarkus.runtime.Quarkus; 4 | import io.quarkus.runtime.annotations.QuarkusMain; 5 | 6 | /** 7 | * Represents the (custom) entry point, most likely used to the Quarkus application in the IDE. 8 | */ 9 | @QuarkusMain 10 | public class CmdApp { 11 | 12 | /** 13 | * Main method to start the app. 14 | * 15 | * @param args Arguments from the command line. 16 | */ 17 | public static void main(String[] args) { 18 | Quarkus.run(args); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/app/QryScheduler.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.app; 2 | 3 | import io.quarkus.scheduler.Scheduled; 4 | import jakarta.enterprise.context.ApplicationScoped; 5 | import jakarta.enterprise.event.Event; 6 | import jakarta.inject.Inject; 7 | 8 | @ApplicationScoped 9 | public class QryScheduler { 10 | 11 | @Inject 12 | Event checkForViewUpdates; 13 | 14 | @Scheduled(every = "1s") 15 | public void fireCheckForUpdatesEvent() { 16 | checkForViewUpdates.fireAsync(new QryCheckForViewUpdatesEvent()); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /quarkus/command/src/main/resources/reflection-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name" : "org.fuin.cqrs4j.SimpleResult", 4 | "allDeclaredConstructors" : true, 5 | "allPublicConstructors" : true, 6 | "allDeclaredMethods" : true, 7 | "allPublicMethods" : true, 8 | "allDeclaredFields" : true, 9 | "allPublicFields" : true 10 | }, 11 | { 12 | "name" : "org.fuin.cqrs4j.example.aggregates.Person", 13 | "allDeclaredConstructors" : true, 14 | "allPublicConstructors" : true, 15 | "allDeclaredMethods" : true, 16 | "allPublicMethods" : true, 17 | "allDeclaredFields" : true, 18 | "allPublicFields" : true 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /spring-boot/query/src/main/java/org/fuin/cqrs4j/example/spring/query/views/personlist/Person.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.query.views.personlist; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 4 | 5 | /** 6 | * DTO class sent back to the client. We could also serialize the entity instead. 7 | * In case you want to provide some Java library for the client, this class can be 8 | * moved to the API module without making JPA entity details public. 9 | * 10 | * @param id Unique identifier of the person. 11 | * @param name Name of the person. 12 | */ 13 | @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) 14 | public record Person(String id, String name) { 15 | } 16 | -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/views/statistic/Statistic.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.views.statistic; 2 | 3 | import jakarta.json.bind.annotation.JsonbCreator; 4 | 5 | /** 6 | * DTO class sent back to the client. We could also serialize the entity instead. 7 | * In case you want to provide some Java library for the client, this class can be 8 | * moved to the API module without making JPA entity details public. 9 | * 10 | * @param type Unique type name. 11 | * @param count Number of instances the type currently has. 12 | */ 13 | public record Statistic(String type, int count) { 14 | 15 | @JsonbCreator 16 | public Statistic { 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /spring-boot/query/src/main/java/org/fuin/cqrs4j/example/spring/query/app/QryBeanConfig.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.query.app; 2 | 3 | import org.fuin.cqrs4j.example.spring.query.views.personlist.PersonListView; 4 | import org.fuin.cqrs4j.example.spring.query.views.statistics.StatisticView; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | public class QryBeanConfig { 10 | 11 | @Bean 12 | public PersonListView personListView() { 13 | return new PersonListView(); 14 | } 15 | 16 | @Bean 17 | public StatisticView statisticView() { 18 | return new StatisticView(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /spring-boot/query/src/main/java/org/fuin/cqrs4j/example/spring/query/views/statistics/Statistic.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.query.views.statistics; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 4 | 5 | /** 6 | * DTO class sent back to the client. We could also serialize the entity instead. 7 | * In case you want to provide some Java library for the client, this class can be 8 | * moved to the API module without making JPA entity details public. 9 | * 10 | * @param type Unique type name. 11 | * @param count Number of instances the type currently has. 12 | */ 13 | @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) 14 | public record Statistic(String type, int count) { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /quarkus.md: -------------------------------------------------------------------------------- 1 | #### Quarkus Microservices 2 | 3 | ##### Quarkus Query Service (Console window 2) 4 | 1. Start the Quarkus query service: 5 | ``` 6 | cd ddd-cqrs-4-java-example/quarkus/query 7 | ./mvnw quarkus:dev 8 | ``` 9 | 2. Opening [http://localhost:8080/](http://localhost:8080/) should show the query welcome page 10 | 11 | For more details see [quarkus/query](quarkus/query). 12 | 13 | ##### Quarkus Command Service (Console window 3) 14 | 1. Start the Quarkus command service: 15 | ``` 16 | cd ddd-cqrs-4-java-example/quarkus/command 17 | ./mvnw quarkus:dev 18 | ``` 19 | 2. Opening [http://localhost:8081/](http://localhost:8081/) should show the command welcome page 20 | 21 | For more details see [quarkus/command](quarkus/command). 22 | -------------------------------------------------------------------------------- /spring-boot/README.md: -------------------------------------------------------------------------------- 1 | # cqrs4j-spring-example 2 | Example applications that uses [Spring Boot](https://spring.io/projects/spring-boot/), [ddd-4-java](https://github.com/fuinorg/ddd-4-java) and [cqrs-4-java](https://github.com/fuinorg/cqrs-4-java) libraries. 3 | Events are stored in an [EventStore](https://eventstore.org/) and the query data is retrieved from a [MariaDB](https://mariadb.org/) database. 4 | 5 | ## Prerequisites 6 | Make sure you installed everything as described [here](../README.md). 7 | 8 | ## Start command / query implementation 9 | Start the command and query microservice s. 10 | - [Command](command) - Command microservice 11 | - [Query](query) - Query microservice 12 | - [Shared](shared) - Code shared between command and query modules (commands, events, value objects and utilities) 13 | -------------------------------------------------------------------------------- /spring-boot.md: -------------------------------------------------------------------------------- 1 | #### Spring Boot Microservices 2 | 3 | ##### Spring Boot Query Service (Console window 2) 4 | 1. Start the Spring Boot query service: 5 | ``` 6 | cd ddd-cqrs-4-java-example/spring-boot/query 7 | ./mvnw spring-boot:run 8 | ``` 9 | 2. Opening [http://localhost:8080/](http://localhost:8080/) should show the query welcome page 10 | 11 | For more details see [spring-boot/query](spring-boot/query). 12 | 13 | ##### Spring Boot Command Service (Console window 3) 14 | 1. Start the Spring Boot command service: 15 | ``` 16 | cd ddd-cqrs-4-java-example/spring-boot/command 17 | ./mvnw spring-boot:run 18 | ``` 19 | 2. Opening [http://localhost:8081/](http://localhost:8081/) should show the command welcome page 20 | 21 | For more details see [spring-boot/command](spring-boot/command). 22 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Java Maven Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | paths-ignore: 8 | - '**/README.md' 9 | pull_request: 10 | branches: 11 | - '**' 12 | paths-ignore: 13 | - '**/README.md' 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Checkout source 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Set up JDK 27 | uses: actions/setup-java@v4 28 | with: 29 | java-version: '17' 30 | distribution: 'zulu' 31 | cache: maven 32 | 33 | - name: Cache Maven packages 34 | uses: actions/cache@v4 35 | with: 36 | path: ~/.m2 37 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 38 | restore-keys: ${{ runner.os }}-m2 39 | 40 | - name: Build with Maven 41 | run: ./mvnw clean verify -U -B --file pom.xml 42 | 43 | -------------------------------------------------------------------------------- /quarkus/shared/src/main/java/org/fuin/cqrs4j/example/quarkus/shared/JsonbFactory.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.shared; 2 | 3 | import jakarta.enterprise.context.ApplicationScoped; 4 | import jakarta.enterprise.inject.Produces; 5 | import jakarta.json.bind.Jsonb; 6 | import jakarta.json.bind.JsonbBuilder; 7 | import jakarta.json.bind.JsonbConfig; 8 | import org.eclipse.yasson.FieldAccessStrategy; 9 | import org.fuin.cqrs4j.example.shared.SharedUtils; 10 | 11 | /** 12 | * CDI factory that creates a JSON-B instance. 13 | */ 14 | @ApplicationScoped 15 | public class JsonbFactory { 16 | 17 | /** 18 | * Creates a JSON-B instance. 19 | * 20 | * @return Fully configured instance. 21 | */ 22 | @Produces 23 | public Jsonb createJsonb() { 24 | final JsonbConfig config = new JsonbConfig().withAdapters(SharedUtils.getJsonbAdapters()) 25 | .withPropertyVisibilityStrategy(new FieldAccessStrategy()); 26 | return JsonbBuilder.create(config); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /spring-boot/query/src/main/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CQRS Spring Boot Example Query Module 6 | 7 | 8 |

CQRS Spring Boot Example Query Module

9 |

This page is served by Spring Boot. The source is in src/main/resources/public/index.html.

10 | Try 11 | 17 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /quarkus/command/src/main/java/org/fuin/cqrs4j/example/quarkus/command/domain/PersonRepositoryFactory.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.command.domain; 2 | 3 | import jakarta.enterprise.context.ApplicationScoped; 4 | import jakarta.enterprise.context.Dependent; 5 | import jakarta.enterprise.inject.Produces; 6 | import org.fuin.cqrs4j.example.aggregates.EventStorePersonRepository; 7 | import org.fuin.cqrs4j.example.aggregates.PersonRepository; 8 | import org.fuin.esc.esgrpc.IESGrpcEventStore; 9 | 10 | /** 11 | * CDI factory that creates an event store connection and repositories. 12 | */ 13 | @ApplicationScoped 14 | public class PersonRepositoryFactory { 15 | 16 | /** 17 | * Creates a repository. 18 | * 19 | * @param eventStore 20 | * Event store implementation. 21 | * 22 | * @return Request scoped project repository. 23 | */ 24 | @Produces 25 | @Dependent 26 | public PersonRepository create(final IESGrpcEventStore eventStore) { 27 | return new EventStorePersonRepository(eventStore); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /quarkus/query/src/main/resources/META-INF/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CQRS Quarkus Example Query Module 6 | 7 | 8 |

CQRS Quarkus Example Query Module

9 |

This page is served by Quarkus. The source is in src/main/resources/META-INF/resources/index.html.

10 | Try 11 | 17 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 19 | -------------------------------------------------------------------------------- /quarkus/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 19 | -------------------------------------------------------------------------------- /spring-boot/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 19 | -------------------------------------------------------------------------------- /quarkus/command/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 19 | -------------------------------------------------------------------------------- /quarkus/query/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 19 | -------------------------------------------------------------------------------- /spring-boot/command/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 19 | -------------------------------------------------------------------------------- /spring-boot/query/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 19 | -------------------------------------------------------------------------------- /quarkus/shared/src/main/java/org/fuin/cqrs4j/example/quarkus/shared/HttpClientFactory.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.shared; 2 | 3 | import jakarta.enterprise.context.ApplicationScoped; 4 | import jakarta.enterprise.inject.Produces; 5 | 6 | import java.net.Authenticator; 7 | import java.net.PasswordAuthentication; 8 | import java.net.http.HttpClient; 9 | import java.time.Duration; 10 | import java.time.temporal.ChronoUnit; 11 | 12 | /** 13 | * CDI factory that creates a {@link HttpClient} instance. 14 | */ 15 | @ApplicationScoped 16 | public class HttpClientFactory { 17 | 18 | @Produces 19 | public HttpClient getHttpClient(final Config config) { 20 | return HttpClient.newBuilder() 21 | .authenticator(new Authenticator() { 22 | @Override 23 | protected PasswordAuthentication getPasswordAuthentication() { 24 | return new PasswordAuthentication(config.getEventStoreUser(), config.getEventStorePassword().toCharArray()); 25 | } 26 | }) 27 | .connectTimeout(Duration.of(10, ChronoUnit.SECONDS)) 28 | .build(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /quarkus/shared/src/main/java/org/fuin/cqrs4j/example/quarkus/shared/SerDeserializerRegistryFactory.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.shared; 2 | 3 | import jakarta.enterprise.context.ApplicationScoped; 4 | import jakarta.enterprise.inject.Produces; 5 | import org.fuin.cqrs4j.example.shared.SharedUtils; 6 | import org.fuin.esc.jsonb.JsonbDeSerializer; 7 | import org.fuin.esc.api.SerDeserializerRegistry; 8 | import org.fuin.esc.api.SerializedDataTypeRegistry; 9 | 10 | /** 11 | * CDI bean that creates a {@link SerDeserializerRegistry}. 12 | */ 13 | @ApplicationScoped 14 | public class SerDeserializerRegistryFactory { 15 | 16 | @Produces 17 | @ApplicationScoped 18 | public SerDeserializerRegistry createRegistry() { 19 | 20 | // Knows about all types for usage with JSON-B 21 | final SerializedDataTypeRegistry typeRegistry = SharedUtils.createTypeRegistry(); 22 | 23 | // Does the actual marshalling/unmarshalling 24 | final JsonbDeSerializer jsonbDeSer = SharedUtils.createJsonbDeSerializer(); 25 | 26 | // Registry connects the type with the appropriate serializer and de-serializer 27 | return SharedUtils.createSerDeserializerRegistry(typeRegistry, jsonbDeSer); 28 | 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /spring-boot/query/src/main/java/org/fuin/cqrs4j/example/spring/query/views/personlist/PersonCreatedEventHandler.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.query.views.personlist; 2 | 3 | import jakarta.persistence.EntityManager; 4 | import org.fuin.cqrs4j.core.JpaEventHandler; 5 | import org.fuin.cqrs4j.example.spring.shared.PersonCreatedEvent; 6 | import org.fuin.cqrs4j.example.spring.shared.PersonId; 7 | import org.fuin.ddd4j.core.EventType; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | /** 12 | * Handles the {@link PersonCreatedEvent}. 13 | */ 14 | public class PersonCreatedEventHandler implements JpaEventHandler { 15 | 16 | private static final Logger LOG = LoggerFactory.getLogger(PersonCreatedEventHandler.class); 17 | 18 | @Override 19 | public EventType getEventType() { 20 | return PersonCreatedEvent.TYPE; 21 | } 22 | 23 | @Override 24 | public void handle(final EntityManager em, final PersonCreatedEvent event) { 25 | LOG.info("Handle {}", event); 26 | final PersonId personId = event.getEntityId(); 27 | if (em.find(PersonListEntry.class, personId.asString()) == null) { 28 | em.persist(new PersonListEntry(personId, event.getName())); 29 | } 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /quarkus/command/src/main/java/org/fuin/cqrs4j/example/quarkus/command/api/AggregateDeletedExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.command.api; 2 | 3 | import jakarta.ws.rs.core.Context; 4 | import jakarta.ws.rs.core.HttpHeaders; 5 | import jakarta.ws.rs.core.Response; 6 | import jakarta.ws.rs.core.Response.Status; 7 | import jakarta.ws.rs.ext.ExceptionMapper; 8 | import jakarta.ws.rs.ext.Provider; 9 | import org.fuin.cqrs4j.jsonb.SimpleResult; 10 | import org.fuin.ddd4j.core.AggregateDeletedException; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | /** 15 | * Maps the exceptions into a HTTP status. 16 | */ 17 | @Provider 18 | public class AggregateDeletedExceptionMapper implements ExceptionMapper { 19 | 20 | private static final Logger LOG = LoggerFactory.getLogger(AggregateDeletedExceptionMapper.class); 21 | 22 | @Context 23 | private HttpHeaders headers; 24 | 25 | @Override 26 | public Response toResponse(final AggregateDeletedException ex) { 27 | 28 | LOG.info("{} {}", ex.getShortId(), ex.getMessage()); 29 | 30 | return Response.status(Status.GONE).entity(SimpleResult.error(ex.getShortId(), ex.getMessage())).type(headers.getMediaType()) 31 | .build(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /quarkus/command/src/main/java/org/fuin/cqrs4j/example/quarkus/command/api/AggregateNotFoundExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.command.api; 2 | 3 | import jakarta.ws.rs.core.Context; 4 | import jakarta.ws.rs.core.HttpHeaders; 5 | import jakarta.ws.rs.core.Response; 6 | import jakarta.ws.rs.core.Response.Status; 7 | import jakarta.ws.rs.ext.ExceptionMapper; 8 | import jakarta.ws.rs.ext.Provider; 9 | import org.fuin.cqrs4j.jsonb.SimpleResult; 10 | import org.fuin.ddd4j.core.AggregateNotFoundException; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | /** 15 | * Maps the exceptions into a HTTP status. 16 | */ 17 | @Provider 18 | public class AggregateNotFoundExceptionMapper implements ExceptionMapper { 19 | 20 | private static final Logger LOG = LoggerFactory.getLogger(AggregateNotFoundExceptionMapper.class); 21 | 22 | @Context 23 | private HttpHeaders headers; 24 | 25 | @Override 26 | public Response toResponse(final AggregateNotFoundException ex) { 27 | 28 | LOG.info("{} {}", ex.getShortId(), ex.getMessage()); 29 | 30 | return Response.status(Status.NOT_FOUND).entity(SimpleResult.error(ex.getShortId(), ex.getMessage())).type(headers.getMediaType()) 31 | .build(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /spring-boot/query/src/main/java/org/fuin/cqrs4j/example/spring/query/views/personlist/PersonDeletedEventHandler.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.query.views.personlist; 2 | 3 | import jakarta.persistence.EntityManager; 4 | import org.fuin.cqrs4j.core.JpaEventHandler; 5 | import org.fuin.cqrs4j.example.spring.shared.PersonDeletedEvent; 6 | import org.fuin.cqrs4j.example.spring.shared.PersonId; 7 | import org.fuin.ddd4j.core.EventType; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | /** 12 | * Handles the {@link PersonDeletedEvent}. 13 | */ 14 | public class PersonDeletedEventHandler implements JpaEventHandler { 15 | 16 | private static final Logger LOG = LoggerFactory.getLogger(PersonDeletedEventHandler.class); 17 | 18 | @Override 19 | public EventType getEventType() { 20 | return PersonDeletedEvent.TYPE; 21 | } 22 | 23 | @Override 24 | public void handle(final EntityManager em, final PersonDeletedEvent event) { 25 | LOG.info("Handle {}: {}", event.getClass().getSimpleName(), event); 26 | final PersonId personId = event.getEntityId(); 27 | final PersonListEntry entity = em.find(PersonListEntry.class, personId.asString()); 28 | if (entity != null) { 29 | em.remove(entity); 30 | } 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /quarkus/command/src/main/java/org/fuin/cqrs4j/example/quarkus/command/api/AggregateAlreadyExistsExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.command.api; 2 | 3 | import jakarta.ws.rs.core.Context; 4 | import jakarta.ws.rs.core.HttpHeaders; 5 | import jakarta.ws.rs.core.Response; 6 | import jakarta.ws.rs.core.Response.Status; 7 | import jakarta.ws.rs.ext.ExceptionMapper; 8 | import jakarta.ws.rs.ext.Provider; 9 | import org.fuin.cqrs4j.jsonb.SimpleResult; 10 | import org.fuin.ddd4j.core.AggregateAlreadyExistsException; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | /** 15 | * Maps the exceptions into a HTTP status. 16 | */ 17 | @Provider 18 | public class AggregateAlreadyExistsExceptionMapper implements ExceptionMapper { 19 | 20 | private static final Logger LOG = LoggerFactory.getLogger(AggregateAlreadyExistsExceptionMapper.class); 21 | 22 | @Context 23 | private HttpHeaders headers; 24 | 25 | @Override 26 | public Response toResponse(final AggregateAlreadyExistsException ex) { 27 | 28 | LOG.info("{} {}", ex.getShortId(), ex.getMessage()); 29 | 30 | return Response.status(Status.CONFLICT).entity(SimpleResult.error(ex.getShortId(), ex.getMessage())).type(headers.getMediaType()) 31 | .build(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.4' 2 | 3 | services: 4 | 5 | eventstore: 6 | image: eventstore/eventstore:24.10 7 | container_name: "cqrs4j-quarkus-example-eventstore" 8 | environment: 9 | EVENTSTORE_MEM_DB: "true" 10 | EVENTSTORE_RUN_PROJECTIONS: "All" 11 | EVENTSTORE_INSECURE: "true" 12 | EVENTSTORE_LOG: "/tmp/log-eventstore" 13 | EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP: "true" 14 | ports: 15 | - "2113:2113" 16 | networks: 17 | - cqrs4j-quarkus-example-net 18 | 19 | # jkmq-postgres: 20 | # image: postgres:11.0 21 | # container_name: "cqrs4j-quarkus-example-postgres" 22 | # ports: 23 | # - "5432:5432" 24 | # environment: 25 | # - POSTGRES_DB=querydb 26 | # - POSTGRES_USER=postgres 27 | # - POSTGRES_PASSWORD=abc 28 | # networks: 29 | # - cqrs4j-quarkus-example-net 30 | 31 | mariadb: 32 | image: mariadb:10.6 33 | container_name: "cqrs4j-quarkus-example-mariadb" 34 | environment: 35 | MYSQL_ROOT_PASSWORD: xyz 36 | MYSQL_DATABASE: querydb 37 | MYSQL_USER: mary 38 | MYSQL_PASSWORD: abc 39 | MYSQL_INITDB_SKIP_TZINFO: 1 40 | ports: 41 | - "3306:3306" 42 | networks: 43 | - cqrs4j-quarkus-example-net 44 | 45 | networks: 46 | cqrs4j-quarkus-example-net: 47 | name: cqrs4j-quarkus-example-net 48 | -------------------------------------------------------------------------------- /quarkus/command/src/main/java/org/fuin/cqrs4j/example/quarkus/command/api/AggregateVersionConflictExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.command.api; 2 | 3 | import jakarta.ws.rs.core.Context; 4 | import jakarta.ws.rs.core.HttpHeaders; 5 | import jakarta.ws.rs.core.Response; 6 | import jakarta.ws.rs.core.Response.Status; 7 | import jakarta.ws.rs.ext.ExceptionMapper; 8 | import jakarta.ws.rs.ext.Provider; 9 | import org.fuin.cqrs4j.jsonb.SimpleResult; 10 | import org.fuin.ddd4j.core.AggregateVersionConflictException; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | /** 15 | * Maps the exceptions into a HTTP status. 16 | */ 17 | @Provider 18 | public class AggregateVersionConflictExceptionMapper implements ExceptionMapper { 19 | 20 | private static final Logger LOG = LoggerFactory.getLogger(AggregateVersionConflictExceptionMapper.class); 21 | 22 | @Context 23 | private HttpHeaders headers; 24 | 25 | @Override 26 | public Response toResponse(final AggregateVersionConflictException ex) { 27 | 28 | LOG.info("{} {}", ex.getShortId(), ex.getMessage()); 29 | 30 | return Response.status(Status.CONFLICT).entity(SimpleResult.error(ex.getShortId(), ex.getMessage())).type(headers.getMediaType()) 31 | .build(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /quarkus/command/src/main/java/org/fuin/cqrs4j/example/quarkus/command/api/AggregateVersionNotFoundExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.command.api; 2 | 3 | import jakarta.ws.rs.core.Context; 4 | import jakarta.ws.rs.core.HttpHeaders; 5 | import jakarta.ws.rs.core.Response; 6 | import jakarta.ws.rs.core.Response.Status; 7 | import jakarta.ws.rs.ext.ExceptionMapper; 8 | import jakarta.ws.rs.ext.Provider; 9 | import org.fuin.cqrs4j.jsonb.SimpleResult; 10 | import org.fuin.ddd4j.core.AggregateVersionNotFoundException; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | /** 15 | * Maps the exceptions into a HTTP status. 16 | */ 17 | @Provider 18 | public class AggregateVersionNotFoundExceptionMapper implements ExceptionMapper { 19 | 20 | private static final Logger LOG = LoggerFactory.getLogger(AggregateVersionNotFoundExceptionMapper.class); 21 | 22 | @Context 23 | private HttpHeaders headers; 24 | 25 | @Override 26 | public Response toResponse(final AggregateVersionNotFoundException ex) { 27 | 28 | LOG.error("{} {}", ex.getShortId(), ex.getMessage()); 29 | 30 | return Response.status(Status.NOT_FOUND).entity(SimpleResult.error(ex.getShortId(), ex.getMessage())).type(headers.getMediaType()) 31 | .build(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/views/personlist/PersonCreatedEventHandler.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.views.personlist; 2 | 3 | import jakarta.enterprise.context.ApplicationScoped; 4 | import jakarta.inject.Inject; 5 | import jakarta.persistence.EntityManager; 6 | import org.fuin.cqrs4j.core.EventHandler; 7 | import org.fuin.cqrs4j.example.shared.PersonCreatedEvent; 8 | import org.fuin.cqrs4j.example.shared.PersonId; 9 | import org.fuin.ddd4j.core.EventType; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | /** 14 | * Handles the {@link PersonCreatedEvent}. 15 | */ 16 | @ApplicationScoped 17 | public class PersonCreatedEventHandler implements EventHandler { 18 | 19 | private static final Logger LOG = LoggerFactory.getLogger(PersonCreatedEventHandler.class); 20 | 21 | @Inject 22 | EntityManager em; 23 | 24 | @Override 25 | public EventType getEventType() { 26 | return PersonCreatedEvent.TYPE; 27 | } 28 | 29 | @Override 30 | public void handle(final PersonCreatedEvent event) { 31 | LOG.info("Handle {}: {}", event.getClass().getSimpleName(), event); 32 | final PersonId personId = event.getEntityId(); 33 | if (em.find(PersonListEntry.class, personId.asString()) == null) { 34 | em.persist(new PersonListEntry(personId, event.getName())); 35 | } 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/views/personlist/PersonDeletedEventHandler.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.views.personlist; 2 | 3 | import jakarta.enterprise.context.ApplicationScoped; 4 | import jakarta.inject.Inject; 5 | import jakarta.persistence.EntityManager; 6 | import org.fuin.cqrs4j.core.EventHandler; 7 | import org.fuin.cqrs4j.example.shared.PersonDeletedEvent; 8 | import org.fuin.cqrs4j.example.shared.PersonId; 9 | import org.fuin.ddd4j.core.EventType; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | /** 14 | * Handles the {@link PersonDeletedEvent}. 15 | */ 16 | @ApplicationScoped 17 | public class PersonDeletedEventHandler implements EventHandler { 18 | 19 | private static final Logger LOG = LoggerFactory.getLogger(PersonDeletedEventHandler.class); 20 | 21 | @Inject 22 | EntityManager em; 23 | 24 | @Override 25 | public EventType getEventType() { 26 | return PersonDeletedEvent.TYPE; 27 | } 28 | 29 | @Override 30 | public void handle(final PersonDeletedEvent event) { 31 | LOG.info("Handle {}: {}", event.getClass().getSimpleName(), event); 32 | final PersonId personId = event.getEntityId(); 33 | final PersonListEntry entity = em.find(PersonListEntry.class, personId.asString()); 34 | if (entity != null) { 35 | em.remove(entity); 36 | } 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /quarkus/command/src/main/java/org/fuin/cqrs4j/example/quarkus/command/domain/EventStorePersonRepository.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.command.domain; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | import org.fuin.cqrs4j.example.shared.PersonId; 5 | import org.fuin.ddd4j.core.EntityType; 6 | import org.fuin.ddd4j.esc.EventStoreRepository; 7 | import org.fuin.esc.api.EventStore; 8 | import org.fuin.objects4j.common.NotThreadSafe; 9 | 10 | /** 11 | * Event sourced repository for storing a {@link Person} aggregate. 12 | */ 13 | @NotThreadSafe 14 | public class EventStorePersonRepository extends EventStoreRepository implements PersonRepository { 15 | 16 | /** 17 | * Constructor all mandatory data. 18 | * 19 | * @param eventStore 20 | * Event store. 21 | */ 22 | public EventStorePersonRepository(final EventStore eventStore) { 23 | super(eventStore); 24 | } 25 | 26 | @Override 27 | @NotNull 28 | public Class getAggregateClass() { 29 | return Person.class; 30 | } 31 | 32 | @Override 33 | @NotNull 34 | public EntityType getAggregateType() { 35 | return PersonId.TYPE; 36 | } 37 | 38 | @Override 39 | @NotNull 40 | public Person create() { 41 | return new Person(); 42 | } 43 | 44 | @Override 45 | @NotNull 46 | public String getIdParamName() { 47 | return "personId"; 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /spring-boot/command/src/main/java/org/fuin/cqrs4j/example/spring/command/domain/EventStorePersonRepository.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.command.domain; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | import org.fuin.cqrs4j.example.spring.shared.PersonId; 5 | import org.fuin.ddd4j.core.EntityType; 6 | import org.fuin.ddd4j.esc.EventStoreRepository; 7 | import org.fuin.esc.api.EventStore; 8 | import org.fuin.objects4j.common.NotThreadSafe; 9 | 10 | /** 11 | * Event sourced repository for storing a {@link Person} aggregate. 12 | */ 13 | @NotThreadSafe 14 | public class EventStorePersonRepository extends EventStoreRepository implements PersonRepository { 15 | 16 | /** 17 | * Constructor all mandatory data. 18 | * 19 | * @param eventStore 20 | * Event store. 21 | */ 22 | public EventStorePersonRepository(final EventStore eventStore) { 23 | super(eventStore); 24 | } 25 | 26 | @Override 27 | @NotNull 28 | public Class getAggregateClass() { 29 | return Person.class; 30 | } 31 | 32 | @Override 33 | @NotNull 34 | public EntityType getAggregateType() { 35 | return PersonId.TYPE; 36 | } 37 | 38 | @Override 39 | @NotNull 40 | public Person create() { 41 | return new Person(); 42 | } 43 | 44 | @Override 45 | @NotNull 46 | public String getIdParamName() { 47 | return "personId"; 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /quarkus/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.fuin.cqrs4j.example 9 | cqrs4j-example-root 10 | 0.6.0-SNAPSHOT 11 | ../pom.xml 12 | 13 | 14 | org.fuin.cqrs4j.example.quarkus 15 | cqrs4j-quarkus-example-root 16 | pom 17 | 18 | 19 | 3.21.1 20 | 24.10 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | io.quarkus.platform 29 | quarkus-bom 30 | ${quarkus.version} 31 | pom 32 | import 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | shared 41 | query 42 | command 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/views/statistic/QryStatisticResource.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.views.statistic; 2 | 3 | import jakarta.inject.Inject; 4 | import jakarta.persistence.EntityManager; 5 | import jakarta.ws.rs.GET; 6 | import jakarta.ws.rs.Path; 7 | import jakarta.ws.rs.PathParam; 8 | import jakarta.ws.rs.Produces; 9 | import jakarta.ws.rs.core.MediaType; 10 | import jakarta.ws.rs.core.Response; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * REST resource providing the statistics. 16 | */ 17 | @Path("/statistics") 18 | public class QryStatisticResource { 19 | 20 | @Inject 21 | EntityManager em; 22 | 23 | @GET 24 | @Produces(MediaType.APPLICATION_JSON) 25 | public Response getAll() { 26 | final List statistics = em.createNamedQuery(StatisticEntity.FIND_ALL, Statistic.class).getResultList(); 27 | return Response.ok(statistics).build(); 28 | } 29 | 30 | @GET 31 | @Path("{name}") 32 | @Produces(MediaType.APPLICATION_JSON) 33 | public Response getByName(@PathParam("name") String name) { 34 | if (!EntityType.isValid(name)) { 35 | return Response.status(Response.Status.BAD_REQUEST).entity("Invalid entity type name").build(); 36 | } 37 | final StatisticEntity entity = em.find(StatisticEntity.class, name); 38 | if (entity == null) { 39 | return Response.status(Response.Status.NOT_FOUND).build(); 40 | } 41 | return Response.ok(entity.toDto()).build(); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/views/personlist/PersonListResource.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.views.personlist; 2 | 3 | import jakarta.inject.Inject; 4 | import jakarta.persistence.EntityManager; 5 | import jakarta.ws.rs.GET; 6 | import jakarta.ws.rs.Path; 7 | import jakarta.ws.rs.PathParam; 8 | import jakarta.ws.rs.Produces; 9 | import jakarta.ws.rs.core.MediaType; 10 | import jakarta.ws.rs.core.Response; 11 | import org.fuin.objects4j.core.UUIDStrValidator; 12 | 13 | import java.util.List; 14 | 15 | /** 16 | * REST resource providing the persons. 17 | */ 18 | @Path("/persons") 19 | public class PersonListResource { 20 | 21 | @Inject 22 | EntityManager em; 23 | 24 | @GET 25 | @Produces(MediaType.APPLICATION_JSON) 26 | public Response getAll() { 27 | final List persons = em.createNamedQuery(PersonListEntry.FIND_ALL, PersonListEntry.class).getResultList(); 28 | return Response.ok(persons).build(); 29 | } 30 | 31 | @GET 32 | @Path("{id}") 33 | @Produces(MediaType.APPLICATION_JSON) 34 | public Response getById(@PathParam("id") String id) { 35 | if (!UUIDStrValidator.isValid(id)) { 36 | return Response.status(Response.Status.BAD_REQUEST).entity("Invalid Person UUID").build(); 37 | } 38 | final PersonListEntry person = em.find(PersonListEntry.class, id); 39 | if (person == null) { 40 | return Response.status(Response.Status.NOT_FOUND).build(); 41 | } 42 | return Response.ok(person).build(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /spring-boot/command/README.md: -------------------------------------------------------------------------------- 1 | # cqrs4j-spring-example-command 2 | Command microservice that uses [Spring Boot](https://spring.io/projects/spring-boot/), [ddd-4-java](https://github.com/fuinorg/ddd-4-java) and [cqrs-4-java](https://github.com/fuinorg/cqrs-4-java) libraries. 3 | Events are stored in an [EventStore](https://eventstore.org/). 4 | 5 | ## Domain classes 6 | * Aggregates (DDD) 7 | * [Person](src/main/java/org/fuin/cqrs4j/example/spring/command/domain/Person.java) 8 | * Commands (CQRS) 9 | * [CreatePersonCommand](src/main/java/org/fuin/cqrs4j/example/spring/command/domain/CreatePersonCommand.java) 10 | * [DeletePersonCommand](src/main/java/org/fuin/cqrs4j/example/spring/command/domain/DeletePersonCommand.java) 11 | * Repository (DDD) 12 | * [PersonRepository](src/main/java/org/fuin/cqrs4j/example/spring/command/domain/PersonRepository.java) 13 | * [EventStorePersonRepository](src/main/java/org/fuin/cqrs4j/example/spring/command/domain/EventStorePersonRepository.java) (Eventstore implementation) 14 | 15 | ## Prerequisites 16 | Make sure you installed everything as described [here](../../README.md). 17 | 18 | ## Run the command microservice in development mode 19 | 1. Open a console (Ubuntu shortcut = ctrl alt t) 20 | 2. Start the command microservice: 21 | ``` 22 | cd ddd-cqrs-4-java-example/spring-boot/command 23 | ./mvnw spring-boot:run 24 | ``` 25 | 3. Opening http://localhost:8081/ should show the command welcome page 26 | 27 | ## Overview 28 | ![Overview](https://raw.github.com/fuinorg/ddd-cqrs-4-java-example/master/spring-boot/command/doc/spring-command.png) 29 | 30 | 31 | -------------------------------------------------------------------------------- /spring-boot/command/src/main/java/org/fuin/cqrs4j/example/spring/command/app/CmdApplication.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.command.app; 2 | 3 | import org.fuin.cqrs4j.example.spring.command.domain.EventStorePersonRepository; 4 | import org.fuin.cqrs4j.example.spring.command.domain.PersonRepository; 5 | import org.fuin.cqrs4j.springboot.base.EventstoreConfig; 6 | import org.fuin.esc.esgrpc.IESGrpcEventStore; 7 | import org.springframework.boot.SpringApplication; 8 | import org.springframework.boot.autoconfigure.SpringBootApplication; 9 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.web.context.annotation.RequestScope; 12 | 13 | @SpringBootApplication(scanBasePackages = { 14 | "org.fuin.cqrs4j.example.spring.shared", 15 | "org.fuin.cqrs4j.example.spring.command.app", 16 | "org.fuin.cqrs4j.example.spring.command.controller" 17 | }) 18 | @EnableConfigurationProperties(EventstoreConfig.class) 19 | public class CmdApplication { 20 | 21 | /** 22 | * Creates an event sourced repository that can store a person. 23 | * 24 | * @param eventStore 25 | * Event store to use. 26 | * 27 | * @return Repository only valid for the current request. 28 | */ 29 | @Bean 30 | @RequestScope 31 | public PersonRepository create(final IESGrpcEventStore eventStore) { 32 | return new EventStorePersonRepository(eventStore); 33 | } 34 | 35 | public static void main(String[] args) { 36 | SpringApplication.run(CmdApplication.class, args); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /quarkus/command/README.md: -------------------------------------------------------------------------------- 1 | # cqrs4j-quarkus-example-command 2 | Command microservice that uses [Quarkus](https://quarkus.io/), [ddd-4-java](https://github.com/fuinorg/ddd-4-java) and [cqrs-4-java](https://github.com/fuinorg/cqrs-4-java) libraries. Events are stored in an [EventStore](https://eventstore.org/). 3 | 4 | ## Prerequisites 5 | Make sure you installed everything as described [here](../../../../). 6 | 7 | ## Run the command microservice in development mode 8 | 1. Open a console (Ubuntu shortcut = ctrl alt t) 9 | 2. Start the command microservice: 10 | ``` 11 | cd ddd-cqrs-4-java-example/quarkus/command 12 | ./mvnw quarkus:dev 13 | ``` 14 | 3. Opening [http://localhost:8081/](http://localhost:8081/) should show the command welcome page 15 | 16 | ## Overview 17 | ![Overview](https://raw.github.com/fuinorg/ddd-cqrs-4-java-example/master/quarkus/command/doc/cdi-command.png) 18 | 19 | 20 | # TODO ... (Does currently not work) 21 | 22 | ## *OPTIONAL* Build and run the command microservice in native mode 23 | 1. Make sure you have enough memory (~6-8 GB) on your PC or VM 24 | 2. Open a console (Ubuntu shortcut = ctrl alt t) 25 | 3. Build the native executable 26 | ``` 27 | cd command 28 | ./mvnw verify -Pnative 29 | ``` 30 | 4. Run the microservice 31 | ``` 32 | ./target/cqrs4j-quarkus-example-command-0.6.0-SNAPSHOT-runner \ 33 | -Djava.library.path=$GRAALVM_HOME/jre/lib/amd64 \ 34 | -Djavax.net.ssl.trustStore=$GRAALVM_HOME/jre/lib/security/cacerts 35 | ``` 36 | 37 | **Issues** 38 | - [Quarkus native command microservice fails with Yasson NullPointerException](https://github.com/fuinorg/ddd-cqrs-4-java-example/issues/2) 39 | -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/views/statistic/EntityType.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.views.statistic; 2 | 3 | import jakarta.validation.constraints.Max; 4 | import jakarta.validation.constraints.NotEmpty; 5 | 6 | import java.util.Objects; 7 | 8 | /** 9 | * Defines the name of a type of entity. 10 | * 11 | * @param name Unique name. Will be converted to lowercase internally - Minimum 1 character, maximum 30 characters. 12 | */ 13 | public record EntityType(String name) { 14 | 15 | /** 16 | * Maximum allowed length of the name. 17 | */ 18 | public static final int MAX_LENGTH = 30; 19 | 20 | public EntityType(@NotEmpty @Max(MAX_LENGTH) String name) { 21 | this.name = Objects.requireNonNull(name, "name==null").toLowerCase(); 22 | if (name.isEmpty()) { 23 | throw new IllegalArgumentException("Name cannot be empty"); 24 | } 25 | if (name.length() > MAX_LENGTH) { 26 | throw new IllegalArgumentException("Name has a length of " + name.length() 27 | + ", but max allowed is " + MAX_LENGTH + " characters: '" + name + "'"); 28 | } 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return name; 34 | } 35 | 36 | /** 37 | * Determines if the name is valid. 38 | * 39 | * @param name Name to be verified. 40 | * @return {@literal true} if the given name can be converted into an instance of this class. 41 | */ 42 | public static boolean isValid(String name) { 43 | return name != null && !name.isEmpty() && name.length() <= MAX_LENGTH; 44 | } 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /spring-boot/query/src/main/java/org/fuin/cqrs4j/example/spring/query/views/statistics/EntityType.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.query.views.statistics; 2 | 3 | import jakarta.validation.constraints.Max; 4 | import jakarta.validation.constraints.NotEmpty; 5 | 6 | import java.util.Objects; 7 | 8 | /** 9 | * Defines the name of a type of entity. 10 | * 11 | * @param name Unique name. Will be converted to lowercase internally - Minimum 1 character, maximum 30 characters. 12 | */ 13 | public record EntityType(String name) { 14 | 15 | /** 16 | * Maximum allowed length of the name. 17 | */ 18 | public static final int MAX_LENGTH = 30; 19 | 20 | public EntityType(@NotEmpty @Max(MAX_LENGTH) String name) { 21 | this.name = Objects.requireNonNull(name, "name==null").toLowerCase(); 22 | if (name.isEmpty()) { 23 | throw new IllegalArgumentException("Name cannot be empty"); 24 | } 25 | if (name.length() > MAX_LENGTH) { 26 | throw new IllegalArgumentException("Name has a length of " + name.length() 27 | + ", but max allowed is " + MAX_LENGTH + " characters: '" + name + "'"); 28 | } 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return name; 34 | } 35 | 36 | /** 37 | * Determines if the name is valid. 38 | * 39 | * @param name Name to be verified. 40 | * @return {@literal true} if the given name can be converted into an instance of this class. 41 | */ 42 | public static boolean isValid(String name) { 43 | return name != null && !name.isEmpty() && name.length() <= MAX_LENGTH; 44 | } 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /quarkus/command/src/main/java/org/fuin/cqrs4j/example/quarkus/command/api/CommandExecutionFailedExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.command.api; 2 | 3 | import jakarta.ws.rs.core.Context; 4 | import jakarta.ws.rs.core.HttpHeaders; 5 | import jakarta.ws.rs.core.Response; 6 | import jakarta.ws.rs.core.Response.Status; 7 | import jakarta.ws.rs.ext.ExceptionMapper; 8 | import jakarta.ws.rs.ext.Provider; 9 | import org.fuin.cqrs4j.core.CommandExecutionFailedException; 10 | import org.fuin.cqrs4j.jsonb.SimpleResult; 11 | import org.fuin.objects4j.common.ExceptionShortIdentifable; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | /** 16 | * Maps the exceptions into a HTTP status. 17 | */ 18 | @Provider 19 | public class CommandExecutionFailedExceptionMapper implements ExceptionMapper { 20 | 21 | private static final Logger LOG = LoggerFactory.getLogger(CommandExecutionFailedExceptionMapper.class); 22 | 23 | @Context 24 | private HttpHeaders headers; 25 | 26 | @Override 27 | public Response toResponse(final CommandExecutionFailedException ex) { 28 | 29 | final String shortId; 30 | if (ex.getCause() instanceof ExceptionShortIdentifable) { 31 | final ExceptionShortIdentifable esi = (ExceptionShortIdentifable) ex.getCause(); 32 | shortId = esi.getShortId(); 33 | } else { 34 | shortId = ex.getCause().getClass().getName(); 35 | } 36 | 37 | LOG.info("{} {}", shortId, ex.getCause().getMessage()); 38 | 39 | return Response.status(Status.BAD_REQUEST).entity(SimpleResult.error(shortId, ex.getMessage())).type(headers.getMediaType()) 40 | .build(); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /spring-boot/query/src/main/java/org/fuin/cqrs4j/example/spring/query/views/personlist/PersonListView.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.query.views.personlist; 2 | 3 | 4 | import jakarta.persistence.EntityManager; 5 | import org.fuin.cqrs4j.core.JpaView; 6 | import org.fuin.cqrs4j.esc.JpaEventDispatcher; 7 | import org.fuin.cqrs4j.esc.SimpleJpaEventDispatcher; 8 | import org.fuin.cqrs4j.example.spring.shared.PersonCreatedEvent; 9 | import org.fuin.cqrs4j.example.spring.shared.PersonDeletedEvent; 10 | import org.fuin.ddd4j.core.Event; 11 | import org.fuin.ddd4j.core.EventType; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import java.util.List; 16 | import java.util.Set; 17 | 18 | /** 19 | * View with persons. 20 | */ 21 | public class PersonListView implements JpaView { 22 | 23 | private static final Logger LOG = LoggerFactory.getLogger(PersonListView.class); 24 | 25 | private final JpaEventDispatcher eventDispatcher; 26 | 27 | public PersonListView() { 28 | eventDispatcher = new SimpleJpaEventDispatcher( 29 | new PersonCreatedEventHandler(), new PersonDeletedEventHandler() 30 | ); 31 | } 32 | 33 | @Override 34 | public String getName() { 35 | return "spring-qry-personlist"; 36 | } 37 | 38 | @Override 39 | public String getCron() { 40 | // Every second 41 | return "* * * * * *"; 42 | } 43 | 44 | @Override 45 | public Set getEventTypes() { 46 | return Set.of(PersonCreatedEvent.TYPE, PersonDeletedEvent.TYPE); 47 | } 48 | 49 | @Override 50 | public void handleEvents(final EntityManager em, final List events) { 51 | eventDispatcher.dispatchEvents(em, events); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /quarkus/command/src/main/java/org/fuin/cqrs4j/example/quarkus/command/domain/DuplicatePersonNameException.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.command.domain; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | import org.fuin.cqrs4j.example.shared.PersonId; 5 | import org.fuin.cqrs4j.example.shared.PersonName; 6 | import org.fuin.objects4j.common.ExceptionShortIdentifable; 7 | 8 | /** 9 | * A name that should be unique does already exist. 10 | */ 11 | public final class DuplicatePersonNameException extends Exception implements ExceptionShortIdentifable { 12 | 13 | private static final long serialVersionUID = 1000L; 14 | 15 | private static final String SHORT_ID = "DUPLICATE_PERSON_NAME"; 16 | 17 | private final PersonId personId; 18 | 19 | private final PersonName name; 20 | 21 | /** 22 | * Constructor with mandatory data. 23 | * 24 | * @param personId 25 | * Identifier of the resource that caused the problem. 26 | * @param name 27 | * Name of the resource that caused the problem. 28 | */ 29 | public DuplicatePersonNameException(@NotNull final PersonId personId, @NotNull final PersonName name) { 30 | super("The name '" + name + "' already exists: " + personId.asString()); 31 | this.personId = personId; 32 | this.name = name; 33 | } 34 | 35 | /** 36 | * Returns the identifier of the entity that has the name. 37 | * 38 | * @return Identifier. 39 | */ 40 | public final PersonId getPersonId() { 41 | return personId; 42 | } 43 | 44 | /** 45 | * Returns the name that already exists. 46 | * 47 | * @return Name. 48 | */ 49 | public final PersonName getName() { 50 | return name; 51 | } 52 | 53 | @Override 54 | public final String getShortId() { 55 | return SHORT_ID; 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /spring-boot/command/src/main/java/org/fuin/cqrs4j/example/spring/command/domain/DuplicatePersonNameException.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.command.domain; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | import org.fuin.cqrs4j.example.spring.shared.PersonId; 5 | import org.fuin.cqrs4j.example.spring.shared.PersonName; 6 | import org.fuin.objects4j.common.ExceptionShortIdentifable; 7 | 8 | /** 9 | * A name that should be unique does already exist. 10 | */ 11 | public final class DuplicatePersonNameException extends Exception implements ExceptionShortIdentifable { 12 | 13 | private static final long serialVersionUID = 1000L; 14 | 15 | private static final String SHORT_ID = "DUPLICATE_PERSON_NAME"; 16 | 17 | private final PersonId personId; 18 | 19 | private final PersonName name; 20 | 21 | /** 22 | * Constructor with mandatory data. 23 | * 24 | * @param personId 25 | * Identifier of the resource that caused the problem. 26 | * @param name 27 | * Name of the resource that caused the problem. 28 | */ 29 | public DuplicatePersonNameException(@NotNull final PersonId personId, @NotNull final PersonName name) { 30 | super("The name '" + name + "' already exists: " + personId.asString()); 31 | this.personId = personId; 32 | this.name = name; 33 | } 34 | 35 | /** 36 | * Returns the identifier of the entity that has the name. 37 | * 38 | * @return Identifier. 39 | */ 40 | public final PersonId getPersonId() { 41 | return personId; 42 | } 43 | 44 | /** 45 | * Returns the name that already exists. 46 | * 47 | * @return Name. 48 | */ 49 | public final PersonName getName() { 50 | return name; 51 | } 52 | 53 | @Override 54 | public final String getShortId() { 55 | return SHORT_ID; 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/views/personlist/PersonListEventDispatcher.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.views.personlist; 2 | 3 | import jakarta.enterprise.context.ApplicationScoped; 4 | import jakarta.validation.constraints.NotNull; 5 | import org.fuin.cqrs4j.esc.EventDispatcher; 6 | import org.fuin.cqrs4j.esc.SimpleEventDispatcher; 7 | import org.fuin.ddd4j.core.Event; 8 | import org.fuin.ddd4j.core.EventType; 9 | import org.fuin.esc.api.CommonEvent; 10 | 11 | import java.util.List; 12 | import java.util.Set; 13 | 14 | /** 15 | * Dispatches events for the person list view. 16 | */ 17 | @ApplicationScoped 18 | public class PersonListEventDispatcher implements EventDispatcher { 19 | 20 | private final SimpleEventDispatcher delegate; 21 | 22 | /** 23 | * Constructor with all events to be dispatched. 24 | * 25 | * @param createdHandler 26 | * PersonCreatedEventHandler. 27 | * @param deletedHandler 28 | * PersonDeletedEventHandler. 29 | */ 30 | public PersonListEventDispatcher(final PersonCreatedEventHandler createdHandler, 31 | final PersonDeletedEventHandler deletedHandler) { 32 | super(); 33 | this.delegate = new SimpleEventDispatcher(createdHandler, deletedHandler); 34 | } 35 | 36 | @Override 37 | @NotNull 38 | public Set getAllTypes() { 39 | return delegate.getAllTypes(); 40 | } 41 | 42 | @Override 43 | public void dispatchCommonEvents(@NotNull final List commonEvents) { 44 | delegate.dispatchCommonEvents(commonEvents); 45 | } 46 | 47 | @Override 48 | public void dispatchEvents(@NotNull final List events) { 49 | delegate.dispatchEvents(events); 50 | } 51 | 52 | @Override 53 | public void dispatchEvent(@NotNull final Event event) { 54 | delegate.dispatchEvent(event); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /spring-boot/query/src/main/java/org/fuin/cqrs4j/example/spring/query/views/statistics/QryStatisticController.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.query.views.statistics; 2 | 3 | import jakarta.persistence.EntityManager; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.transaction.annotation.Transactional; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PathVariable; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import java.util.List; 16 | 17 | /** 18 | * REST controller providing the statistics. 19 | */ 20 | @RestController 21 | @RequestMapping("/statistics") 22 | @Transactional(readOnly = true) 23 | public class QryStatisticController { 24 | 25 | private static final Logger LOG = LoggerFactory.getLogger(QryStatisticController.class); 26 | 27 | @Autowired 28 | EntityManager em; 29 | 30 | @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) 31 | public List getAll() { 32 | final List statistics = em.createNamedQuery(StatisticEntity.FIND_ALL, Statistic.class).getResultList(); 33 | LOG.info("getAll() = {}", statistics.size()); 34 | return statistics; 35 | } 36 | 37 | @GetMapping(path = "/{name}", produces = MediaType.APPLICATION_JSON_VALUE) 38 | public ResponseEntity getByName(@PathVariable(value = "name") String name) { 39 | if (!EntityType.isValid(name)) { 40 | return ResponseEntity.badRequest().body("Invalid entity type name"); 41 | } 42 | final StatisticEntity entity = em.find(StatisticEntity.class, name); 43 | if (entity == null) { 44 | return ResponseEntity.notFound().build(); 45 | } 46 | return ResponseEntity.ok().body(entity.toDto()); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /quarkus/shared/src/main/java/org/fuin/cqrs4j/example/quarkus/shared/ProjectionAdminEventStoreFactory.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.shared; 2 | 3 | import jakarta.enterprise.context.ApplicationScoped; 4 | import jakarta.enterprise.inject.Disposes; 5 | import jakarta.enterprise.inject.Produces; 6 | import org.fuin.esc.api.ProjectionAdminEventStore; 7 | 8 | import java.net.MalformedURLException; 9 | import java.net.URL; 10 | import java.net.http.HttpClient; 11 | import io.kurrent.dbclient.KurrentDBClientSettings; 12 | import io.kurrent.dbclient.KurrentDBProjectionManagementClient; 13 | import jakarta.enterprise.context.ApplicationScoped; 14 | import jakarta.enterprise.inject.Disposes; 15 | import jakarta.enterprise.inject.Produces; 16 | import org.fuin.esc.api.ProjectionAdminEventStore; 17 | import org.fuin.esc.esgrpc.GrpcProjectionAdminEventStore; 18 | import org.fuin.esc.esgrpc.GrpcProjectionAdminEventStore; 19 | 20 | /** 21 | * CDI factory that creates a {@link ProjectionAdminEventStore} instance. 22 | */ 23 | @ApplicationScoped 24 | public class ProjectionAdminEventStoreFactory { 25 | 26 | @Produces 27 | @ApplicationScoped 28 | public ProjectionAdminEventStore getProjectionAdminEventStore(final Config config) { 29 | 30 | final KurrentDBClientSettings settings = KurrentDBClientSettings.builder() 31 | .addHost(config.getEventStoreHost(), config.getEventStoreHttpPort()) 32 | .defaultCredentials(config.getEventStoreUser(), config.getEventStorePassword()) 33 | .tls(false) 34 | .buildConnectionSettings(); 35 | final KurrentDBProjectionManagementClient client = KurrentDBProjectionManagementClient.create(settings); 36 | return new GrpcProjectionAdminEventStore(client).open(); 37 | 38 | } 39 | 40 | /** 41 | * Closes the projection admin event store when the context is disposed. 42 | * 43 | * @param es Event store to close. 44 | */ 45 | public void closeProjectionAdminEventStore(@Disposes final ProjectionAdminEventStore es) { 46 | es.close(); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /spring-boot/shared/src/test/java/org/fuin/cqrs4j/example/spring/shared/ArchitectureTest.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.shared; 2 | 3 | import com.tngtech.archunit.core.domain.JavaClasses; 4 | import com.tngtech.archunit.core.domain.JavaModifier; 5 | import com.tngtech.archunit.core.importer.ClassFileImporter; 6 | import com.tngtech.archunit.core.importer.ImportOption; 7 | import com.tngtech.archunit.junit.AnalyzeClasses; 8 | import org.fuin.ddd4j.core.AggregateRootUuid; 9 | import org.fuin.ddd4j.core.DomainEvent; 10 | import org.fuin.ddd4j.core.EntityId; 11 | import org.fuin.ddd4j.core.HasEntityTypeConstant; 12 | import org.fuin.esc.api.HasSerializedDataTypeConstant; 13 | import org.fuin.objects4j.common.HasPublicStaticValueOfMethod; 14 | import org.junit.jupiter.api.Test; 15 | 16 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 17 | 18 | /** 19 | * Tests architectural aspects. 20 | */ 21 | @AnalyzeClasses(packagesOf = ArchitectureTest.class, importOptions = ImportOption.DoNotIncludeTests.class) 22 | class ArchitectureTest { 23 | 24 | @Test 25 | public void testDomainEventsAnnotations() { 26 | 27 | final JavaClasses importedClasses = new ClassFileImporter().importPackages(ArchitectureTest.class.getPackageName()); 28 | 29 | classes() 30 | .that().implement(DomainEvent.class) 31 | .and().doNotHaveModifier(JavaModifier.ABSTRACT) 32 | .should().beAnnotatedWith(HasSerializedDataTypeConstant.class) 33 | .check(importedClasses); 34 | } 35 | @Test 36 | void testEntityIdAnnotations() { 37 | 38 | final JavaClasses importedClasses = new ClassFileImporter().importPackages(ArchitectureTest.class.getPackageName()); 39 | 40 | classes() 41 | .that().areAssignableTo(AggregateRootUuid.class) 42 | .and().implement(EntityId.class) 43 | .should().beAnnotatedWith(HasPublicStaticValueOfMethod.class) 44 | .andShould().beAnnotatedWith(HasEntityTypeConstant.class) 45 | .allowEmptyShould(true).check(importedClasses); 46 | 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /quarkus/command/src/main/java/org/fuin/cqrs4j/example/quarkus/command/api/ConstraintViolationExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.command.api; 2 | 3 | import jakarta.annotation.Nullable; 4 | import jakarta.validation.ConstraintViolation; 5 | import jakarta.validation.ConstraintViolationException; 6 | import jakarta.ws.rs.core.Context; 7 | import jakarta.ws.rs.core.HttpHeaders; 8 | import jakarta.ws.rs.core.Response; 9 | import jakarta.ws.rs.core.Response.Status; 10 | import jakarta.ws.rs.ext.ExceptionMapper; 11 | import jakarta.ws.rs.ext.Provider; 12 | import org.fuin.cqrs4j.jsonb.SimpleResult; 13 | import org.fuin.objects4j.common.Contract; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | import java.util.Set; 20 | 21 | /** 22 | * Maps the exceptions into a HTTP status. 23 | */ 24 | @Provider 25 | public class ConstraintViolationExceptionMapper implements ExceptionMapper { 26 | 27 | private static final String CONSTRAINT_VIOLATION = "CONSTRAINT_VIOLATION"; 28 | 29 | private static final Logger LOG = LoggerFactory.getLogger(ConstraintViolationExceptionMapper.class); 30 | 31 | @Context 32 | private HttpHeaders headers; 33 | 34 | @Override 35 | public Response toResponse(final ConstraintViolationException ex) { 36 | 37 | LOG.info("{} {}", CONSTRAINT_VIOLATION, "ConstraintViolationException"); 38 | 39 | return Response.status(Status.BAD_REQUEST).entity(SimpleResult.error(CONSTRAINT_VIOLATION, asString(ex.getConstraintViolations()))) 40 | .build(); 41 | 42 | } 43 | 44 | private static String asString(@Nullable final Set> constraintViolations) { 45 | if (constraintViolations == null || constraintViolations.isEmpty()) { 46 | return ""; 47 | } 48 | final List list = new ArrayList<>(); 49 | for (final ConstraintViolation constraintViolation : constraintViolations) { 50 | list.add(Contract.asString(constraintViolation)); 51 | } 52 | return list.toString(); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/views/common/QryProjectionPositionRepository.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.views.common; 2 | 3 | import jakarta.enterprise.context.ApplicationScoped; 4 | import jakarta.inject.Inject; 5 | import jakarta.persistence.EntityManager; 6 | import jakarta.validation.constraints.NotNull; 7 | import org.fuin.cqrs4j.esc.ProjectionService; 8 | import org.fuin.esc.api.StreamId; 9 | import org.fuin.objects4j.common.Contract; 10 | 11 | /** 12 | * Repository that contains the position of the stream. 13 | */ 14 | @ApplicationScoped 15 | public class QryProjectionPositionRepository implements ProjectionService { 16 | 17 | private static final String ARG_STREAM_ID = "streamId"; 18 | @Inject 19 | EntityManager em; 20 | 21 | @Override 22 | public void resetProjectionPosition(@NotNull final StreamId streamId) { 23 | Contract.requireArgNotNull(ARG_STREAM_ID, streamId); 24 | final QryProjectionPosition pos = em.find(QryProjectionPosition.class, streamId.asString()); 25 | if (pos != null) { 26 | pos.setNextPosition(0L); 27 | } 28 | } 29 | 30 | @Override 31 | public Long readProjectionPosition(@NotNull StreamId streamId) { 32 | Contract.requireArgNotNull(ARG_STREAM_ID, streamId); 33 | final QryProjectionPosition pos = em.find(QryProjectionPosition.class, streamId.asString()); 34 | if (pos == null) { 35 | return 0L; 36 | } 37 | return pos.getNextPos(); 38 | } 39 | 40 | @Override 41 | public void updateProjectionPosition(@NotNull StreamId streamId, @NotNull Long nextEventNumber) { 42 | Contract.requireArgNotNull(ARG_STREAM_ID, streamId); 43 | Contract.requireArgNotNull("nextEventNumber", nextEventNumber); 44 | final QryProjectionPosition pos = em.find(QryProjectionPosition.class, streamId.asString()); 45 | if (pos == null) { 46 | em.persist(new QryProjectionPosition(streamId, nextEventNumber)); 47 | } else { 48 | pos.setNextPosition(nextEventNumber); 49 | em.merge(pos); 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /spring-boot/query/README.md: -------------------------------------------------------------------------------- 1 | # cqrs4j-spring-example-query 2 | Query microservice that uses [Spring Boot](https://spring.io/projects/spring-boot/), [ddd-4-java](https://github.com/fuinorg/ddd-4-java) and [cqrs-4-java](https://github.com/fuinorg/cqrs-4-java) libraries. 3 | Events are stored in an [EventStore](https://eventstore.org/) and the query data is retrieved from a [MariaDB](https://mariadb.org/) database. 4 | 5 | ## CQRS Views 6 | * [personlist](src/main/java/org/fuin/cqrs4j/example/spring/query/views/personlist) 7 | * [PersonListView](src/main/java/org/fuin/cqrs4j/example/spring/query/views/personlist/PersonListView.java) - Defines a view with a list of persons. It maps incoming events to the database entities. 8 | * [PersonListEntry](src/main/java/org/fuin/cqrs4j/example/spring/query/views/personlist/PersonListEntry.java) - JPA Entity to store a person in the database 9 | * [PersonListController](src/main/java/org/fuin/cqrs4j/example/spring/query/views/personlist/PersonListController.java) - Spring Rest Controller to return the persons 10 | * [statistics](src/main/java/org/fuin/cqrs4j/example/spring/query/views/statistics) 11 | 12 | ## Prerequisites 13 | Make sure you installed everything as described [here](../../../../). 14 | 15 | ## Run the query microservice in development mode 16 | 1. Open a console (Ubuntu shortcut = ctrl alt t) 17 | 2. Start the query microservice: 18 | ``` 19 | cd ddd-cqrs-4-java-example/spring-boot/query 20 | ./mvnw spring-boot:run 21 | ``` 22 | 3. Opening [http://localhost:8080/](http://localhost:8080/) should show the query welcome page 23 | 24 | ## Overview 25 | ![Overview](https://raw.github.com/fuinorg/ddd-cqrs-4-java-example/master/spring-boot/query/doc/spring-view.png) 26 | 27 | ## Running test in IDE 28 | In case you want to run the integration test inside your IDE (Eclipse or other), you need to start the Eventstore and MariaDB before. 29 | 30 | 1. Start the Eventstore and MariaDB Docker container using the [docker-compose.yml](../../docker-compose.yml) script: `docker-compose up` 31 | 2. Run the test: [PersonControllerIT.java](src/test/java/org/fuin/cqrs4j/example/spring/query/api/PersonControllerIT.java) 32 | 3. Stop the containers in the console using CTRL+C and then remove the containers using again Docker Compose: `docker-compose rm` 33 | -------------------------------------------------------------------------------- /quarkus/query/README.md: -------------------------------------------------------------------------------- 1 | # cqrs4j-quarkus-example-query 2 | Query microservice that uses [Quarkus](https://quarkus.io/), [ddd-4-java](https://github.com/fuinorg/ddd-4-java) and [cqrs-4-java](https://github.com/fuinorg/cqrs-4-java) libraries. Events are stored in an [EventStore](https://eventstore.org/) and the query data is retrieved from a [MariaDB](https://mariadb.org/) database. 3 | 4 | ## Prerequisites 5 | Make sure you installed everything as described [here](../../../../). 6 | 7 | ## Run the query microservice in development mode 8 | 1. Open a console (Ubuntu shortcut = ctrl alt t) 9 | 2. Start the query microservice: 10 | ``` 11 | cd ddd-cqrs-4-java-example/quarkus/query 12 | ./mvnw quarkus:dev 13 | ``` 14 | 3. Opening [http://localhost:8080/](http://localhost:8080/) should show the query welcome page 15 | 16 | ## Overview 17 | ![Overview](https://raw.github.com/fuinorg/ddd-cqrs-4-java-example/master/quarkus/query/doc/cdi-view.png) 18 | 19 | ## Running test in IDE 20 | In case you want to run the integration test inside your IDE (Eclipse or other), you need to start the Eventstore and MariaDB before. 21 | 22 | 1. Start the Eventstore and MariaDB Docker container using the [docker-compose.yml](../../docker-compose.yml) script: `docker-compose up` 23 | 2. Run the test: [QryPersonResourceIT.java](src/test/java/org/fuin/cqrs4j/example/quarkus/query/api/QryPersonResourceIT.java) 24 | 3. Stop the containers in the console using CTRL+C and then remove the containers using again Docker Compose: `docker-compose rm` 25 | 26 | # TODO ... (Does currently not work) 27 | 28 | ## *OPTIONAL* Build and run the query microservice in native mode 29 | 1. Make sure you have enough memory (~6-8 GB) on your PC or VM 30 | 2. Open a console (Ubuntu shortcut = ctrl alt t) 31 | 3. Build the native executable 32 | ``` 33 | cd query 34 | ./mvnw verify -Pnative 35 | ``` 36 | 4. Run the microservice 37 | ``` 38 | ./target/cqrs4j-quarkus-example-query-0.6.0-SNAPSHOT-runner \ 39 | -Djava.library.path=$GRAALVM_HOME/jre/lib/amd64 \ 40 | -Djavax.net.ssl.trustStore=$GRAALVM_HOME/jre/lib/security/cacerts 41 | ``` 42 | 43 | **Issues** 44 | - [Quarkus native query microservice does not execute updates](https://github.com/fuinorg/ddd-cqrs-4-java-example/issues/1) 45 | - [Building native query microservice fails with PostgreSQL (MariaDB works fine)](https://github.com/fuinorg/ddd-cqrs-4-java-example/issues/3) 46 | -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/views/personlist/PersonListEventChunkHandler.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.views.personlist; 2 | 3 | import jakarta.enterprise.context.ApplicationScoped; 4 | import jakarta.inject.Inject; 5 | import jakarta.transaction.Transactional; 6 | import org.fuin.cqrs4j.esc.ProjectionService; 7 | import org.fuin.cqrs4j.example.shared.SharedUtils; 8 | import org.fuin.ddd4j.core.EventType; 9 | import org.fuin.esc.api.ProjectionStreamId; 10 | import org.fuin.esc.api.StreamEventsSlice; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.util.Set; 15 | 16 | @ApplicationScoped 17 | @Transactional 18 | public class PersonListEventChunkHandler { 19 | 20 | private static final Logger LOG = LoggerFactory.getLogger(PersonListEventChunkHandler.class); 21 | 22 | @Inject 23 | PersonListEventDispatcher dispatcher; 24 | 25 | @Inject 26 | ProjectionService projectionService; 27 | 28 | private ProjectionStreamId streamId; 29 | 30 | /** 31 | * Returns the name of the event store projection that is used by this handler. 32 | * 33 | * @return Unique projection stream name. 34 | */ 35 | public ProjectionStreamId getProjectionStreamId() { 36 | if (streamId == null) { 37 | final Set eventTypes = dispatcher.getAllTypes(); 38 | final String name = "quarkus-qry-person-" + SharedUtils.calculateChecksum(eventTypes); 39 | streamId = new ProjectionStreamId(name); 40 | } 41 | return streamId; 42 | } 43 | 44 | /** 45 | * Returns the next event position to read. 46 | * 47 | * @return Number of the next event to read. 48 | */ 49 | public Long readNextEventNumber() { 50 | return projectionService.readProjectionPosition(getProjectionStreamId()); 51 | } 52 | 53 | /** 54 | * Handles the current slice as a single transaction. 55 | * 56 | * @param currentSlice 57 | * Slice with events to dispatch. 58 | */ 59 | @Transactional 60 | public void handleChunk(final StreamEventsSlice currentSlice) { 61 | LOG.debug("Handle chunk: {}", currentSlice); 62 | dispatcher.dispatchCommonEvents(currentSlice.getEvents()); 63 | projectionService.updateProjectionPosition(getProjectionStreamId(), currentSlice.getNextEventNumber()); 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /quarkus/shared/src/main/java/org/fuin/cqrs4j/example/quarkus/shared/EventStoreFactory.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.shared; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | 5 | import org.fuin.esc.api.EnhancedMimeType; 6 | import org.fuin.esc.api.SerDeserializerRegistry; 7 | import org.fuin.esc.esgrpc.ESGrpcEventStore; 8 | import org.fuin.esc.esgrpc.IESGrpcEventStore; 9 | import org.fuin.esc.jsonb.BaseTypeFactory; 10 | 11 | import io.kurrent.dbclient.KurrentDBClient; 12 | import io.kurrent.dbclient.KurrentDBClientSettings; 13 | 14 | import jakarta.enterprise.context.ApplicationScoped; 15 | import jakarta.enterprise.inject.Disposes; 16 | import jakarta.enterprise.inject.Produces; 17 | 18 | /** 19 | * CDI factory that creates an event store connection. 20 | */ 21 | @ApplicationScoped 22 | public class EventStoreFactory { 23 | 24 | /** 25 | * Creates an GRPC based event store.
26 | *
27 | * CAUTION: The returned event store instance is NOT thread safe. 28 | * 29 | * @param config 30 | * Configuration to use. 31 | * @param registry 32 | * Serialization registry. 33 | * 34 | * @return Application scope event store. 35 | */ 36 | @Produces 37 | @ApplicationScoped 38 | public IESGrpcEventStore createEventStore(final Config config, final SerDeserializerRegistry registry) { 39 | 40 | final KurrentDBClientSettings setts = KurrentDBClientSettings.builder() 41 | .addHost(config.getEventStoreHost(), config.getEventStoreHttpPort()) 42 | .defaultCredentials(config.getEventStoreUser(), config.getEventStorePassword()) 43 | .tls(false) 44 | .buildConnectionSettings(); 45 | 46 | final KurrentDBClient client = KurrentDBClient.create(setts); 47 | final IESGrpcEventStore eventstore = new ESGrpcEventStore.Builder() 48 | .eventStore(client) 49 | .serDesRegistry(registry) 50 | .baseTypeFactory(new BaseTypeFactory()) 51 | .targetContentType(EnhancedMimeType.create("application", "json", StandardCharsets.UTF_8)) 52 | .build(); 53 | 54 | eventstore.open(); 55 | return eventstore; 56 | 57 | } 58 | 59 | /** 60 | * Closes the GRPC based event store when the context is disposed. 61 | * 62 | * @param es 63 | * Event store to close. 64 | */ 65 | public void closeEventStore(@Disposes final IESGrpcEventStore es) { 66 | es.close(); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/views/statistic/StatisticEntity.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.views.statistic; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.Id; 6 | import jakarta.persistence.NamedQuery; 7 | import jakarta.persistence.Table; 8 | import jakarta.validation.constraints.NotNull; 9 | 10 | import java.util.Objects; 11 | 12 | /** 13 | * Represents a statistic record that will be stored in the database. 14 | */ 15 | @Entity 16 | @Table(name = "QUARKUS_STATISTIC") 17 | @NamedQuery(name = StatisticEntity.FIND_ALL, 18 | query = "SELECT new org.fuin.cqrs4j.example.quarkus.query.views.statistic.Statistic(s.type, s.count) FROM StatisticEntity s") 19 | public class StatisticEntity { 20 | 21 | public static final String FIND_ALL = "StatisticEntity.findAll"; 22 | 23 | @Id 24 | @Column(name = "TYPE", nullable = false, length = EntityType.MAX_LENGTH, updatable = false) 25 | @NotNull 26 | private String type; 27 | 28 | @Column(name = "COUNT", updatable = true) 29 | private int count; 30 | 31 | /** 32 | * JPA default constructor. 33 | */ 34 | protected StatisticEntity() { 35 | } 36 | 37 | /** 38 | * Constrcutor with a given type that sets the number of instances to one. 39 | * 40 | * @param type Unique type ID. 41 | */ 42 | public StatisticEntity(@NotNull EntityType type) { 43 | this.type = Objects.requireNonNull(type, "type==null").name(); 44 | this.count = 1; 45 | } 46 | 47 | public Statistic toDto() { 48 | return new Statistic(type, count); 49 | } 50 | 51 | /** 52 | * Increases the number of entries for the type by one. 53 | */ 54 | public void inc() { 55 | this.count++; 56 | } 57 | 58 | /** 59 | * Decreases the number of entries for the type by one. 60 | */ 61 | public void dec() { 62 | this.count--; 63 | } 64 | 65 | @Override 66 | public boolean equals(Object o) { 67 | if (this == o) return true; 68 | if (o == null || getClass() != o.getClass()) return false; 69 | StatisticEntity that = (StatisticEntity) o; 70 | return Objects.equals(type, that.type); 71 | } 72 | 73 | @Override 74 | public int hashCode() { 75 | return Objects.hash(type); 76 | } 77 | 78 | @Override 79 | public String toString() { 80 | return "StatisticEntity{" + 81 | "type='" + type + '\'' + 82 | ", count=" + count + 83 | '}'; 84 | } 85 | } -------------------------------------------------------------------------------- /spring-boot/query/src/main/java/org/fuin/cqrs4j/example/spring/query/views/statistics/StatisticView.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.query.views.statistics; 2 | 3 | import jakarta.persistence.EntityManager; 4 | import org.fuin.cqrs4j.core.JpaView; 5 | import org.fuin.cqrs4j.example.spring.shared.PersonCreatedEvent; 6 | import org.fuin.cqrs4j.example.spring.shared.PersonDeletedEvent; 7 | import org.fuin.ddd4j.core.Event; 8 | import org.fuin.ddd4j.core.EventType; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import java.util.List; 13 | import java.util.Set; 14 | 15 | /** 16 | * Handles the events required to maintain the statistic database view. 17 | */ 18 | public class StatisticView implements JpaView { 19 | 20 | private static final Logger LOG = LoggerFactory.getLogger(StatisticView.class); 21 | 22 | private static final EntityType PERSON = new EntityType("person"); 23 | 24 | @Override 25 | public String getName() { 26 | return "spring-qry-statistic"; 27 | } 28 | 29 | @Override 30 | public String getCron() { 31 | // Every second 32 | return "* * * * * *"; 33 | } 34 | 35 | @Override 36 | public Set getEventTypes() { 37 | return Set.of(PersonCreatedEvent.TYPE, PersonDeletedEvent.TYPE); 38 | } 39 | 40 | @Override 41 | public void handleEvents(final EntityManager em, final List events) { 42 | for (final Event event : events) { 43 | if (event instanceof PersonCreatedEvent ev) { 44 | handlePersonCreatedEvent(em, ev); 45 | } else if (event instanceof PersonDeletedEvent ev) { 46 | handlePersonDeletedEvent(em, ev); 47 | } else { 48 | throw new IllegalStateException("Cannot handle event: " + event); 49 | } 50 | } 51 | } 52 | 53 | private void handlePersonCreatedEvent(final EntityManager em, final PersonCreatedEvent event) { 54 | LOG.info("Handle {}: {}", event.getClass().getSimpleName(), event); 55 | final StatisticEntity entity = em.find(StatisticEntity.class, PERSON.name()); 56 | if (entity == null) { 57 | em.persist(new StatisticEntity(PERSON)); 58 | } else { 59 | entity.inc(); 60 | } 61 | } 62 | 63 | private void handlePersonDeletedEvent(final EntityManager em, final PersonDeletedEvent event) { 64 | LOG.info("Handle {}: {}", event.getClass().getSimpleName(), event); 65 | final StatisticEntity entity = em.find(StatisticEntity.class, PERSON.name()); 66 | if (entity != null) { 67 | entity.dec(); 68 | } 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/views/common/QryProjectionPosition.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.views.common; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.Id; 6 | import jakarta.persistence.Table; 7 | import jakarta.validation.constraints.NotNull; 8 | import org.fuin.esc.api.SimpleStreamId; 9 | import org.fuin.esc.api.StreamId; 10 | import org.fuin.objects4j.common.Contract; 11 | 12 | /** 13 | * Stores the next position to read from the projection in the event store. 14 | */ 15 | @Entity 16 | @Table(name = "QUARKUS_QRY_PROJECTION_POS") 17 | public class QryProjectionPosition { 18 | 19 | @Id 20 | @Column(name = "STREAM_ID", nullable = false, length = 250, updatable = false) 21 | @NotNull 22 | private String streamId; 23 | 24 | @Column(name = "NEXT_POS", nullable = false, updatable = true) 25 | @NotNull 26 | private Long nextPos; 27 | 28 | /** 29 | * JPA constructor. 30 | */ 31 | protected QryProjectionPosition() { 32 | super(); 33 | } 34 | 35 | /** 36 | * Constructor with mandatory data. 37 | * 38 | * @param streamId 39 | * Unique stream identifier. 40 | * @param nextPos 41 | * Next position from the stream to read. 42 | */ 43 | public QryProjectionPosition(@NotNull final StreamId streamId, @NotNull final Long nextPos) { 44 | super(); 45 | Contract.requireArgNotNull("streamId", streamId); 46 | Contract.requireArgNotNull("nextPos", nextPos); 47 | this.streamId = streamId.asString(); 48 | this.nextPos = nextPos; 49 | } 50 | 51 | /** 52 | * Returns the unique stream identifier. 53 | * 54 | * @return Stream ID. 55 | */ 56 | @NotNull 57 | public StreamId getStreamId() { 58 | return new SimpleStreamId(streamId); 59 | } 60 | 61 | /** 62 | * Returns the next position read from the stream. 63 | * 64 | * @return Position to read next time. 65 | */ 66 | @NotNull 67 | public Long getNextPos() { 68 | return nextPos; 69 | } 70 | 71 | /** 72 | * Sets the next position read from the stream. 73 | * 74 | * @param nextPos 75 | * New position to set. 76 | */ 77 | public void setNextPosition(@NotNull final Long nextPos) { 78 | Contract.requireArgNotNull("nextPos", nextPos); 79 | this.nextPos = nextPos; 80 | } 81 | 82 | @Override 83 | public String toString() { 84 | return "QryProjectionPosition [streamId=" + streamId + ", nextPos=" + nextPos + "]"; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /spring-boot/shared/src/main/java/org/fuin/cqrs4j/example/spring/shared/SharedJacksonModule.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.shared; 2 | 3 | import com.fasterxml.jackson.core.Version; 4 | import com.fasterxml.jackson.databind.Module; 5 | import com.fasterxml.jackson.databind.module.SimpleDeserializers; 6 | import com.fasterxml.jackson.databind.module.SimpleSerializers; 7 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 8 | import org.fuin.ddd4j.core.EntityIdFactory; 9 | import org.fuin.ddd4j.jackson.EntityIdJacksonDeserializer; 10 | import org.fuin.ddd4j.jackson.EntityIdJacksonSerializer; 11 | import org.fuin.objects4j.jackson.ValueObjectStringJacksonDeserializer; 12 | import org.fuin.objects4j.jackson.ValueObjectStringJacksonSerializer; 13 | import org.fuin.utils4j.TestOmitted; 14 | 15 | import java.util.List; 16 | import java.util.Objects; 17 | 18 | /** 19 | * Module that registers the adapters for the package. 20 | */ 21 | @TestOmitted("Tested with other tests") 22 | public class SharedJacksonModule extends Module { 23 | 24 | private final EntityIdFactory entityIdFactory; 25 | 26 | /** 27 | * Constructor with entity ID factory. 28 | * 29 | * @param entityIdFactory Factory to use. 30 | */ 31 | public SharedJacksonModule(EntityIdFactory entityIdFactory) { 32 | this.entityIdFactory = Objects.requireNonNull(entityIdFactory, "entityIdFactory==null"); 33 | } 34 | 35 | @Override 36 | public String getModuleName() { 37 | return "Cqrs4JModule"; 38 | } 39 | 40 | @Override 41 | public Iterable getDependencies() { 42 | return List.of(new JavaTimeModule()); 43 | } 44 | 45 | @Override 46 | public void setupModule(SetupContext context) { 47 | 48 | final SimpleSerializers serializers = new SimpleSerializers(); 49 | serializers.addSerializer(new EntityIdJacksonSerializer<>(PersonId.class)); 50 | serializers.addSerializer(new ValueObjectStringJacksonSerializer<>(PersonName.class)); 51 | context.addSerializers(serializers); 52 | 53 | final SimpleDeserializers deserializers = new SimpleDeserializers(); 54 | deserializers.addDeserializer(PersonId.class, new EntityIdJacksonDeserializer<>(PersonId.class, entityIdFactory)); 55 | deserializers.addDeserializer(PersonName.class, new ValueObjectStringJacksonDeserializer<>(PersonName.class, PersonName::new)); 56 | context.addDeserializers(deserializers); 57 | 58 | } 59 | 60 | @Override 61 | public Version version() { 62 | return new Version(0, 5, 0, "SNAPSHOT", 63 | "org.fuin.cqrs4j.example.spring", "cqrs4j-spring-example-shared"); 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/views/statistic/StatisticView.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.views.statistic; 2 | 3 | import jakarta.enterprise.context.ApplicationScoped; 4 | import jakarta.inject.Inject; 5 | import jakarta.persistence.EntityManager; 6 | import org.fuin.cqrs4j.example.shared.PersonCreatedEvent; 7 | import org.fuin.cqrs4j.example.shared.PersonDeletedEvent; 8 | import org.fuin.cqrs4j.example.shared.View; 9 | import org.fuin.ddd4j.core.Event; 10 | import org.fuin.ddd4j.core.EventType; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.util.List; 15 | import java.util.Set; 16 | 17 | /** 18 | * Handles the events required to maintain the statistic database view. 19 | */ 20 | @ApplicationScoped 21 | public class StatisticView implements View { 22 | 23 | private static final Logger LOG = LoggerFactory.getLogger(StatisticView.class); 24 | 25 | private static final EntityType PERSON = new EntityType("person"); 26 | 27 | @Inject 28 | EntityManager em; 29 | 30 | @Override 31 | public String getName() { 32 | return "quarkus-qry-statistic"; 33 | } 34 | 35 | @Override 36 | public String getCron() { 37 | // Every second 38 | return "* * * * * ? *"; 39 | } 40 | 41 | @Override 42 | public Set getEventTypes() { 43 | return Set.of(PersonCreatedEvent.TYPE, PersonDeletedEvent.TYPE); 44 | } 45 | 46 | @Override 47 | public void handleEvents(final List events) { 48 | for (final Event event : events) { 49 | if (event instanceof PersonCreatedEvent ev) { 50 | handlePersonCreatedEvent(ev); 51 | } else if (event instanceof PersonDeletedEvent ev) { 52 | handlePersonDeletedEvent(ev); 53 | } else { 54 | throw new RuntimeException("Cannot handle event: " + event); 55 | } 56 | } 57 | } 58 | 59 | private void handlePersonCreatedEvent(final PersonCreatedEvent event) { 60 | LOG.info("Handle {}: {}", event.getClass().getSimpleName(), event); 61 | final StatisticEntity entity = em.find(StatisticEntity.class, PERSON.name()); 62 | if (entity == null) { 63 | em.persist(new StatisticEntity(PERSON)); 64 | } else { 65 | entity.inc(); 66 | } 67 | } 68 | 69 | private void handlePersonDeletedEvent(final PersonDeletedEvent event) { 70 | LOG.info("Handle {}: {}", event.getClass().getSimpleName(), event); 71 | final StatisticEntity entity = em.find(StatisticEntity.class, PERSON.name()); 72 | if (entity != null) { 73 | entity.dec(); 74 | } 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /spring-boot/query/src/main/java/org/fuin/cqrs4j/example/spring/query/views/statistics/StatisticEntity.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.query.views.statistics; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.Id; 6 | import jakarta.persistence.NamedQuery; 7 | import jakarta.persistence.Table; 8 | import jakarta.validation.constraints.NotNull; 9 | 10 | import java.util.Objects; 11 | 12 | /** 13 | * Represents a statistic record that will be stored in the database. 14 | */ 15 | @Entity 16 | @Table(name = "SPRING_STATISTIC") 17 | @NamedQuery(name = StatisticEntity.FIND_ALL, 18 | query = "SELECT new org.fuin.cqrs4j.example.spring.query.views.statistics.Statistic(s.type, s.count) FROM StatisticEntity s") 19 | public class StatisticEntity { 20 | 21 | public static final String FIND_ALL = "StatisticEntity.findAll"; 22 | 23 | @Id 24 | @Column(name = "TYPE", nullable = false, length = EntityType.MAX_LENGTH, updatable = false) 25 | @NotNull 26 | private String type; 27 | 28 | @Column(name = "COUNT", updatable = true) 29 | private int count; 30 | 31 | /** 32 | * JPA default constructor. 33 | */ 34 | protected StatisticEntity() { 35 | } 36 | 37 | /** 38 | * Constrcutor with a given type that sets the number of instances to one. 39 | * 40 | * @param type Unique type ID. 41 | */ 42 | public StatisticEntity(@NotNull EntityType type) { 43 | this.type = Objects.requireNonNull(type, "type==null").name(); 44 | this.count = 1; 45 | } 46 | 47 | /** 48 | * Returns the statistic as "DTO" instance. 49 | * 50 | * @return Statistic record. 51 | */ 52 | public Statistic toDto() { 53 | return new Statistic(type, count); 54 | } 55 | 56 | /** 57 | * Increases the number of entries for the type by one. 58 | */ 59 | public void inc() { 60 | this.count++; 61 | } 62 | 63 | /** 64 | * Decreases the number of entries for the type by one. 65 | */ 66 | public void dec() { 67 | this.count--; 68 | } 69 | 70 | @Override 71 | public boolean equals(Object o) { 72 | if (this == o) return true; 73 | if (o == null || getClass() != o.getClass()) return false; 74 | StatisticEntity that = (StatisticEntity) o; 75 | return Objects.equals(type, that.type); 76 | } 77 | 78 | @Override 79 | public int hashCode() { 80 | return Objects.hash(type); 81 | } 82 | 83 | @Override 84 | public String toString() { 85 | return "StatisticEntity{" + 86 | "type='" + type + '\'' + 87 | ", count=" + count + 88 | '}'; 89 | } 90 | } -------------------------------------------------------------------------------- /quarkus/shared/src/test/java/org/fuin/cqrs4j/example/quarkus/shared/PersonIdTest.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.shared; 2 | 3 | import nl.jqno.equalsverifier.EqualsVerifier; 4 | import nl.jqno.equalsverifier.Warning; 5 | import org.fuin.cqrs4j.example.shared.PersonId; 6 | import org.fuin.ddd4j.core.EntityType; 7 | import org.fuin.ddd4j.core.StringBasedEntityType; 8 | import org.fuin.objects4j.common.ConstraintViolationException; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import java.util.UUID; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.junit.jupiter.api.Assertions.fail; 15 | 16 | /** 17 | * Test for {@link org.fuin.cqrs4j.example.shared.PersonId}. 18 | */ 19 | public final class PersonIdTest { 20 | 21 | private static final String PERSON_UUID = "84565d62-115e-4502-b7c9-38ad69c64b05"; 22 | 23 | @Test 24 | public void testEquals() { 25 | EqualsVerifier.forClass(org.fuin.cqrs4j.example.shared.PersonId.class).suppress(Warning.NONFINAL_FIELDS).withNonnullFields("entityType", "uuid") 26 | .withPrefabValues(EntityType.class, new StringBasedEntityType("A"), new StringBasedEntityType("B")).verify(); 27 | } 28 | 29 | @Test 30 | public void testValueOf() { 31 | final org.fuin.cqrs4j.example.shared.PersonId personId = org.fuin.cqrs4j.example.shared.PersonId.valueOf(PERSON_UUID); 32 | 33 | assertThat(personId.asString()).isEqualTo(PERSON_UUID); 34 | 35 | } 36 | 37 | @Test 38 | public void testValueOfIllegalArgumentCharacter() { 39 | try { 40 | org.fuin.cqrs4j.example.shared.PersonId.valueOf("abc"); 41 | fail(); 42 | } catch (final ConstraintViolationException ex) { 43 | assertThat(ex.getMessage()).isEqualTo("The argument 'value' is not valid: 'abc'"); 44 | } 45 | } 46 | 47 | @Test 48 | public final void testConverterUnmarshal() throws Exception { 49 | 50 | // PREPARE 51 | final String personIdValue = PERSON_UUID; 52 | 53 | // TEST 54 | final org.fuin.cqrs4j.example.shared.PersonId personId = new org.fuin.cqrs4j.example.shared.PersonId.Converter().adaptFromJson(UUID.fromString(PERSON_UUID)); 55 | 56 | // VERIFY 57 | assertThat(personId.asString()).isEqualTo(personIdValue); 58 | } 59 | 60 | @Test 61 | public void testConverterMarshal() throws Exception { 62 | 63 | final org.fuin.cqrs4j.example.shared.PersonId personId = org.fuin.cqrs4j.example.shared.PersonId.valueOf(PERSON_UUID); 64 | 65 | // TEST 66 | final UUID uuid = new PersonId.Converter().adaptToJson(personId); 67 | 68 | // VERIFY 69 | assertThat(uuid).isEqualTo(UUID.fromString(PERSON_UUID)); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /spring-boot/shared/src/main/java/org/fuin/cqrs4j/example/spring/shared/PersonId.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.shared; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | import org.fuin.ddd4j.core.AggregateRootUuid; 5 | import org.fuin.ddd4j.core.EntityType; 6 | import org.fuin.ddd4j.core.HasEntityTypeConstant; 7 | import org.fuin.ddd4j.core.StringBasedEntityType; 8 | import org.fuin.objects4j.common.HasPublicStaticValueOfMethod; 9 | import org.fuin.objects4j.common.Immutable; 10 | import org.fuin.objects4j.ui.Label; 11 | import org.fuin.objects4j.ui.ShortLabel; 12 | import org.fuin.objects4j.ui.Tooltip; 13 | 14 | import java.util.UUID; 15 | 16 | /** 17 | * Identifies uniquely a person. 18 | */ 19 | @ShortLabel(bundle = "ddd-cqrs-4-java-example", key = "PersonId.slabel", value = "PID") 20 | @Label(bundle = "ddd-cqrs-4-java-example", key = "PersonId.label", value = "Person's ID") 21 | @Tooltip(bundle = "ddd-cqrs-4-java-example", key = "PersonId.tooltip", value = "Unique identifier of a person") 22 | @Immutable 23 | @HasPublicStaticValueOfMethod 24 | @HasEntityTypeConstant 25 | public final class PersonId extends AggregateRootUuid { 26 | 27 | private static final long serialVersionUID = 1000L; 28 | 29 | /** 30 | * Unique name of the aggregate this identifier refers to. 31 | */ 32 | public static final EntityType TYPE = new StringBasedEntityType("PERSON"); 33 | 34 | /** 35 | * Default constructor. 36 | */ 37 | protected PersonId() { 38 | super(PersonId.TYPE); 39 | } 40 | 41 | /** 42 | * Constructor with all data. 43 | * 44 | * @param value Persistent value. 45 | */ 46 | public PersonId(@NotNull final UUID value) { 47 | super(PersonId.TYPE, value); 48 | } 49 | 50 | /** 51 | * Verifies if the given string can be converted into a Person ID. 52 | * 53 | * @param value String with valid UUID string. A null value is also valid. 54 | * @return {@literal true} if the string is a valid UUID. 55 | */ 56 | public static boolean isValid(final String value) { 57 | if (value == null) { 58 | return true; 59 | } 60 | return AggregateRootUuid.isValid(value); 61 | } 62 | 63 | /** 64 | * Parses a given string and returns a new instance of PersonId. 65 | * 66 | * @param value String with valid UUID to convert. A null value returns null. 67 | * @return Converted value. 68 | */ 69 | public static PersonId valueOf(final String value) { 70 | if (value == null) { 71 | return null; 72 | } 73 | AggregateRootUuid.requireArgValid("value", value); 74 | return new PersonId(UUID.fromString(value)); 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /spring-boot/query/src/main/java/org/fuin/cqrs4j/example/spring/query/app/QryApplication.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.query.app; 2 | 3 | import org.fuin.cqrs4j.springboot.base.EventstoreConfig; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.autoconfigure.domain.EntityScan; 7 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 10 | import org.springframework.scheduling.TaskScheduler; 11 | import org.springframework.scheduling.annotation.EnableAsync; 12 | import org.springframework.scheduling.annotation.EnableScheduling; 13 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 14 | import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; 15 | import org.springframework.scheduling.config.ScheduledTaskRegistrar; 16 | 17 | import java.util.concurrent.Executor; 18 | 19 | @SpringBootApplication(scanBasePackages = { 20 | "org.fuin.cqrs4j.springboot.view", 21 | "org.fuin.cqrs4j.example.spring.shared", 22 | "org.fuin.cqrs4j.example.spring.query.app", 23 | "org.fuin.cqrs4j.example.spring.query.views" 24 | }) 25 | @EnableConfigurationProperties(EventstoreConfig.class) 26 | @EnableJpaRepositories("org.fuin.cqrs4j.") 27 | @EntityScan({ 28 | "org.fuin.cqrs4j.springboot.view", 29 | "org.fuin.cqrs4j.example.spring.query.views" 30 | }) 31 | @EnableScheduling 32 | @EnableAsync 33 | public class QryApplication { 34 | 35 | @Bean("projectorExecutor") 36 | public Executor taskExecutor() { 37 | final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 38 | executor.setCorePoolSize(1); 39 | executor.setMaxPoolSize(5); 40 | executor.setQueueCapacity(500); 41 | executor.setThreadNamePrefix("qry-app-"); 42 | executor.initialize(); 43 | return executor; 44 | } 45 | 46 | @Bean 47 | public ScheduledTaskRegistrar scheduledTaskRegistrar(TaskScheduler taskScheduler) { 48 | final ScheduledTaskRegistrar scheduledTaskRegistrar = new ScheduledTaskRegistrar(); 49 | scheduledTaskRegistrar.setScheduler(taskScheduler); 50 | return scheduledTaskRegistrar; 51 | } 52 | 53 | @Bean 54 | public TaskScheduler threadPoolTaskScheduler() { 55 | final ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); 56 | scheduler.setPoolSize(20); 57 | return scheduler; 58 | } 59 | 60 | public static void main(String[] args) { 61 | SpringApplication.run(QryApplication.class, args); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /spring-boot/query/src/main/java/org/fuin/cqrs4j/example/spring/query/views/personlist/PersonListEntry.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.query.views.personlist; 2 | 3 | import jakarta.persistence.*; 4 | import jakarta.validation.constraints.NotNull; 5 | import org.fuin.cqrs4j.example.spring.shared.PersonId; 6 | import org.fuin.cqrs4j.example.spring.shared.PersonName; 7 | import org.fuin.objects4j.common.Contract; 8 | 9 | /** 10 | * Represents a person that will be stored in the database. 11 | */ 12 | @Entity 13 | @NamedQuery(name = PersonListEntry.FIND_ALL, query = "SELECT p FROM PersonListEntry p") 14 | @Table(name = "SPRING_PERSON_LIST") 15 | public class PersonListEntry { 16 | 17 | public static final String FIND_ALL = "PersonListEntry.findAll"; 18 | 19 | @Id 20 | @Column(name = "ID", nullable = false, length = 36, updatable = false) 21 | @NotNull 22 | private String id; 23 | 24 | @Column(name = "NAME", nullable = false, length = PersonName.MAX_LENGTH, updatable = true) 25 | @NotNull 26 | private String name; 27 | 28 | /** 29 | * Deserialization constructor. 30 | */ 31 | protected PersonListEntry() { 32 | super(); 33 | } 34 | 35 | /** 36 | * Constructor with all data. 37 | * 38 | * @param id 39 | * Unique aggregate identifier. 40 | * @param name 41 | * Name of the created person 42 | */ 43 | public PersonListEntry(@NotNull final PersonId id, @NotNull final PersonName name) { 44 | super(); 45 | Contract.requireArgNotNull("id", id); 46 | Contract.requireArgNotNull("name", name); 47 | this.id = id.asString(); 48 | this.name = name.asString(); 49 | } 50 | 51 | /** 52 | * Returns the person as "DTO" instance. 53 | * 54 | * @return Person record. 55 | */ 56 | public Person toDto() { 57 | return new Person(id, name); 58 | } 59 | 60 | /** 61 | * Returns the unique person identifier. 62 | * 63 | * @return Aggregate ID. 64 | */ 65 | @NotNull 66 | public PersonId getId() { 67 | return PersonId.valueOf(id); 68 | } 69 | 70 | /** 71 | * Returns the name of the person to create. 72 | * 73 | * @return the Person name 74 | */ 75 | @NotNull 76 | public PersonName getName() { 77 | return new PersonName(name); 78 | } 79 | 80 | /** 81 | * Sets the name of the person. 82 | * 83 | * @param name 84 | * Name to set. 85 | */ 86 | public void setName(@NotNull final PersonName name) { 87 | Contract.requireArgNotNull("name", name); 88 | this.name = name.asString(); 89 | } 90 | 91 | @Override 92 | public String toString() { 93 | return "PersonListEntry [id=" + id + ", name=" + name + "]"; 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /spring-boot/query/src/main/java/org/fuin/cqrs4j/example/spring/query/views/personlist/PersonListController.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.query.views.personlist; 2 | 3 | import jakarta.persistence.EntityManager; 4 | import org.fuin.cqrs4j.example.spring.shared.PersonId; 5 | import org.fuin.ddd4j.core.AggregateNotFoundException; 6 | import org.fuin.objects4j.core.UUIDStr; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.transaction.annotation.Transactional; 13 | import org.springframework.web.bind.annotation.GetMapping; 14 | import org.springframework.web.bind.annotation.PathVariable; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | import java.util.List; 19 | import java.util.UUID; 20 | import java.util.stream.Collectors; 21 | 22 | /** 23 | * REST controller providing the persons. 24 | */ 25 | @RestController 26 | @RequestMapping("/persons") 27 | @Transactional(readOnly = true) 28 | public class PersonListController { 29 | 30 | private static final Logger LOG = LoggerFactory.getLogger(PersonListController.class); 31 | 32 | @Autowired 33 | private EntityManager em; 34 | 35 | /** 36 | * Get all persons list. 37 | * 38 | * @return the list 39 | */ 40 | @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) 41 | public List getAllPersons() { 42 | final List persons = em.createNamedQuery(PersonListEntry.FIND_ALL, PersonListEntry.class).getResultList(); 43 | LOG.info("getAllPersons() = {}", persons.size()); 44 | return persons.stream().map(PersonListEntry::toDto).toList(); 45 | } 46 | 47 | /** 48 | * Reads a person by it's universally unique aggregate UUID. 49 | * 50 | * @param personId 51 | * Person UUID. 52 | * 53 | * @return Person from database. 54 | * 55 | * @throws AggregateNotFoundException 56 | * A person with the given identifier is unknown. 57 | */ 58 | @GetMapping(path = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) 59 | public ResponseEntity getPersonById(@PathVariable(value = "id") @UUIDStr String personId) 60 | throws AggregateNotFoundException { 61 | 62 | final PersonListEntry person = em.find(PersonListEntry.class, personId); 63 | if (person == null) { 64 | throw new AggregateNotFoundException(PersonId.TYPE, new PersonId(UUID.fromString(personId))); 65 | } 66 | LOG.info("getPersonById({}) = {}", personId, person); 67 | return ResponseEntity.ok().body(person.toDto()); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /spring-boot/shared/src/test/java/org/fuin/cqrs4j/example/spring/shared/PersonIdTest.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.shared; 2 | 3 | import nl.jqno.equalsverifier.EqualsVerifier; 4 | import nl.jqno.equalsverifier.Warning; 5 | import org.fuin.ddd4j.core.EntityType; 6 | import org.fuin.ddd4j.core.StringBasedEntityType; 7 | import org.fuin.objects4j.common.ConstraintViolationException; 8 | import org.fuin.objects4j.jackson.ImmutableObjectMapper; 9 | import org.junit.jupiter.api.AfterAll; 10 | import org.junit.jupiter.api.BeforeAll; 11 | import org.junit.jupiter.api.Test; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.junit.jupiter.api.Assertions.fail; 15 | 16 | /** 17 | * Test for {@link PersonId} class. 18 | */ 19 | class PersonIdTest { 20 | 21 | private static final String PERSON_UUID = "84565d62-115e-4502-b7c9-38ad69c64b05"; 22 | 23 | private static ImmutableObjectMapper mapper; 24 | 25 | @BeforeAll 26 | static void beforeAll() { 27 | final SharedConfig factory = new SharedConfig(); 28 | final ImmutableObjectMapper.Builder mapperBuilder = factory.immutableObjectMapperBuilder(factory.entityIdFactory()); 29 | final ImmutableObjectMapper.Provider mapperProvider = factory.immutableObjectMapperProvider(mapperBuilder); 30 | mapper = mapperProvider.mapper(); 31 | } 32 | 33 | @AfterAll 34 | static void afterAll() { 35 | mapper = null; 36 | } 37 | 38 | @Test 39 | void testEquals() { 40 | EqualsVerifier.forClass(PersonId.class).suppress(Warning.NONFINAL_FIELDS).withNonnullFields("entityType", "uuid") 41 | .withPrefabValues(EntityType.class, new StringBasedEntityType("A"), new StringBasedEntityType("B")).verify(); 42 | } 43 | 44 | @Test 45 | void testValueOf() { 46 | final PersonId personId = PersonId.valueOf(PERSON_UUID); 47 | 48 | assertThat(personId.asString()).isEqualTo(PERSON_UUID); 49 | 50 | } 51 | 52 | @Test 53 | void testValueOfIllegalArgumentCharacter() { 54 | try { 55 | PersonId.valueOf("abc"); 56 | fail(); 57 | } catch (final ConstraintViolationException ex) { 58 | assertThat(ex.getMessage()).isEqualTo("The argument 'value' is not valid: 'abc'"); 59 | } 60 | } 61 | 62 | @Test 63 | final void testUnmarshal() throws Exception { 64 | 65 | // PREPARE 66 | final String json = """ 67 | { "id" : "PERSON 84565d62-115e-4502-b7c9-38ad69c64b05" } 68 | """; 69 | 70 | // TEST 71 | final Person copy = mapper.reader().readValue(json, Person.class); 72 | 73 | // VERIFY 74 | assertThat(copy.id()).isEqualTo(PersonId.valueOf(PERSON_UUID)); 75 | } 76 | 77 | @Test 78 | void testMarshal() throws Exception { 79 | 80 | final Person original = new Person(PersonId.valueOf(PERSON_UUID)); 81 | 82 | // TEST 83 | final String json = mapper.writer().writeValueAsString(original); 84 | 85 | // VERIFY 86 | final Person copy = mapper.reader().readValue(json, Person.class); 87 | assertThat(copy.id()).isEqualTo(original.id()); 88 | } 89 | 90 | public record Person(PersonId id) { 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /quarkus/shared/src/main/java/org/fuin/cqrs4j/example/quarkus/shared/CreatePersonCommand.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.shared; 2 | 3 | import jakarta.json.bind.annotation.JsonbProperty; 4 | import jakarta.validation.constraints.NotNull; 5 | import org.fuin.cqrs4j.jsonb.AbstractAggregateCommand; 6 | import org.fuin.ddd4j.core.DomainEventExpectedEntityIdPath; 7 | import org.fuin.ddd4j.core.EventType; 8 | import org.fuin.esc.api.SerializedDataType; 9 | import org.fuin.objects4j.common.Immutable; 10 | 11 | /** 12 | * A new person should be created in the system. 13 | */ 14 | @Immutable 15 | @DomainEventExpectedEntityIdPath(PersonId.class) 16 | public final class CreatePersonCommand extends AbstractAggregateCommand { 17 | 18 | private static final long serialVersionUID = 1000L; 19 | 20 | /** 21 | * Never changing unique event type name. 22 | */ 23 | public static final EventType TYPE = new EventType("CreatePersonCommand"); 24 | 25 | /** 26 | * Unique name used for marshalling/unmarshalling the event. 27 | */ 28 | public static final SerializedDataType SER_TYPE = new SerializedDataType(CreatePersonCommand.TYPE.asBaseType()); 29 | 30 | @NotNull 31 | @JsonbProperty("name") 32 | private PersonName name; 33 | 34 | /** 35 | * Protected default constructor for deserialization. 36 | */ 37 | protected CreatePersonCommand() { 38 | super(); 39 | } 40 | 41 | @Override 42 | public final EventType getEventType() { 43 | return CreatePersonCommand.TYPE; 44 | } 45 | 46 | /** 47 | * Returns: Name of a person. 48 | * 49 | * @return Current value. 50 | */ 51 | @NotNull 52 | public final PersonName getName() { 53 | return name; 54 | } 55 | 56 | @Override 57 | public final String toString() { 58 | return "Create person '" + name + "' with identifier '" + getAggregateRootId() + "'"; 59 | } 60 | 61 | /** 62 | * Builds an instance of the outer class. 63 | */ 64 | public static final class Builder extends AbstractAggregateCommand.Builder { 65 | 66 | private CreatePersonCommand delegate; 67 | 68 | public Builder() { 69 | super(new CreatePersonCommand()); 70 | delegate = delegate(); 71 | } 72 | 73 | public Builder id(PersonId personId) { 74 | entityIdPath(personId); 75 | return this; 76 | } 77 | 78 | public Builder name(String name) { 79 | delegate.name = new PersonName(name); 80 | return this; 81 | } 82 | 83 | public Builder name(PersonName name) { 84 | delegate.name = name; 85 | return this; 86 | } 87 | 88 | public CreatePersonCommand build() { 89 | ensureBuildableAbstractAggregateCommand(); 90 | ensureNotNull("name", delegate.name); 91 | 92 | final CreatePersonCommand result = delegate; 93 | delegate = new CreatePersonCommand(); 94 | resetAbstractAggregateCommand(delegate); 95 | return result; 96 | } 97 | 98 | } 99 | 100 | } -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/views/personlist/PersonListEntry.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.views.personlist; 2 | 3 | import io.quarkus.runtime.annotations.RegisterForReflection; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.Id; 7 | import jakarta.persistence.NamedQuery; 8 | import jakarta.persistence.Table; 9 | import jakarta.validation.constraints.NotNull; 10 | import jakarta.xml.bind.annotation.XmlAccessType; 11 | import jakarta.xml.bind.annotation.XmlAccessorType; 12 | import jakarta.xml.bind.annotation.XmlElement; 13 | import jakarta.xml.bind.annotation.XmlRootElement; 14 | import org.fuin.cqrs4j.example.shared.PersonId; 15 | import org.fuin.cqrs4j.example.shared.PersonName; 16 | import org.fuin.objects4j.common.Contract; 17 | 18 | /** 19 | * Represents a person that will be stored in the database. 20 | */ 21 | @Entity 22 | @Table(name = "QUARKUS_PERSON_LIST") 23 | @XmlAccessorType(XmlAccessType.FIELD) 24 | @XmlRootElement(name = "person") 25 | @NamedQuery(name = PersonListEntry.FIND_ALL, query = "SELECT p FROM PersonListEntry p") 26 | @RegisterForReflection 27 | public class PersonListEntry { 28 | 29 | public static final String FIND_ALL = "PersonListEntry.findAll"; 30 | 31 | @Id 32 | @Column(name = "ID", nullable = false, length = 36, updatable = false) 33 | @NotNull 34 | @XmlElement(name = "id") 35 | private String id; 36 | 37 | @Column(name = "NAME", nullable = false, length = PersonName.MAX_LENGTH, updatable = true) 38 | @NotNull 39 | @XmlElement(name = "name") 40 | private String name; 41 | 42 | /** 43 | * JAX-B constructor. 44 | */ 45 | protected PersonListEntry() { 46 | super(); 47 | } 48 | 49 | /** 50 | * Constructor with all data. 51 | * 52 | * @param id 53 | * Unique aggregate identifier. 54 | * @param name 55 | * Name of the created person 56 | */ 57 | public PersonListEntry(@NotNull final PersonId id, @NotNull final PersonName name) { 58 | super(); 59 | Contract.requireArgNotNull("id", id); 60 | Contract.requireArgNotNull("name", name); 61 | this.id = id.asString(); 62 | this.name = name.asString(); 63 | } 64 | 65 | /** 66 | * Returns the unique person identifier. 67 | * 68 | * @return Aggregate ID. 69 | */ 70 | @NotNull 71 | public PersonId getId() { 72 | return PersonId.valueOf(id); 73 | } 74 | 75 | /** 76 | * Returns the name of the person to create. 77 | * 78 | * @return the Person name 79 | */ 80 | @NotNull 81 | public PersonName getName() { 82 | return new PersonName(name); 83 | } 84 | 85 | /** 86 | * Sets the name of the person. 87 | * 88 | * @param name 89 | * Name to set. 90 | */ 91 | public void setName(@NotNull final PersonName name) { 92 | Contract.requireArgNotNull("name", name); 93 | this.name = name.asString(); 94 | } 95 | 96 | @Override 97 | public String toString() { 98 | return "PersonListEntry [id=" + id + ", name=" + name + "]"; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /quarkus/shared/src/main/java/org/fuin/cqrs4j/example/quarkus/shared/PersonDeletedEvent.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.shared; 2 | 3 | import jakarta.json.bind.annotation.JsonbProperty; 4 | import jakarta.validation.constraints.NotNull; 5 | import org.fuin.ddd4j.core.AggregateVersion; 6 | import org.fuin.ddd4j.core.EventType; 7 | import org.fuin.ddd4j.jsonb.AbstractDomainEvent; 8 | import org.fuin.esc.api.HasSerializedDataTypeConstant; 9 | import org.fuin.esc.api.SerializedDataType; 10 | import org.fuin.objects4j.common.Immutable; 11 | 12 | /** 13 | * A person was deleted from the system. 14 | */ 15 | @Immutable 16 | @HasSerializedDataTypeConstant 17 | public final class PersonDeletedEvent extends AbstractDomainEvent { 18 | 19 | private static final long serialVersionUID = 1000L; 20 | 21 | /** 22 | * Never changing unique event type name. 23 | */ 24 | public static final EventType TYPE = new EventType("PersonDeletedEvent"); 25 | 26 | /** 27 | * Unique name used for marshalling/unmarshalling the event. 28 | */ 29 | public static final SerializedDataType SER_TYPE = new SerializedDataType(PersonDeletedEvent.TYPE.asBaseType()); 30 | 31 | @NotNull 32 | @JsonbProperty("name") 33 | private PersonName name; 34 | 35 | /** 36 | * Protected default constructor for deserialization. 37 | */ 38 | protected PersonDeletedEvent() { 39 | super(); 40 | } 41 | 42 | @Override 43 | public final EventType getEventType() { 44 | return PersonDeletedEvent.TYPE; 45 | } 46 | 47 | /** 48 | * Returns: Name of a person. 49 | * 50 | * @return Current value. 51 | */ 52 | @NotNull 53 | public final PersonName getName() { 54 | return name; 55 | } 56 | 57 | @Override 58 | public final String toString() { 59 | return "Deleted person '" + name + "' (" + getEntityId() + ") [Event " + getEventId() + "]"; 60 | } 61 | 62 | /** 63 | * Builds an instance of the outer class. 64 | */ 65 | public static final class Builder extends AbstractDomainEvent.Builder { 66 | 67 | private PersonDeletedEvent delegate; 68 | 69 | public Builder() { 70 | super(new PersonDeletedEvent()); 71 | delegate = delegate(); 72 | } 73 | 74 | public Builder id(PersonId personId) { 75 | entityIdPath(personId); 76 | return this; 77 | } 78 | 79 | public Builder name(String name) { 80 | delegate.name = new PersonName(name); 81 | return this; 82 | } 83 | 84 | public Builder name(PersonName name) { 85 | delegate.name = name; 86 | return this; 87 | } 88 | 89 | public Builder version(int version) { 90 | aggregateVersion(AggregateVersion.valueOf(version)); 91 | return this; 92 | } 93 | 94 | public PersonDeletedEvent build() { 95 | ensureBuildableAbstractDomainEvent(); 96 | ensureNotNull("name", delegate.name); 97 | 98 | final PersonDeletedEvent result = delegate; 99 | delegate = new PersonDeletedEvent(); 100 | resetAbstractDomainEvent(delegate); 101 | return result; 102 | } 103 | 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /quarkus/shared/src/main/java/org/fuin/cqrs4j/example/quarkus/shared/PersonCreatedEvent.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.shared; 2 | 3 | import jakarta.json.bind.annotation.JsonbProperty; 4 | import jakarta.validation.constraints.NotNull; 5 | import org.fuin.ddd4j.core.AggregateVersion; 6 | import org.fuin.ddd4j.core.EventType; 7 | import org.fuin.ddd4j.jsonb.AbstractDomainEvent; 8 | import org.fuin.esc.api.HasSerializedDataTypeConstant; 9 | import org.fuin.esc.api.SerializedDataType; 10 | import org.fuin.objects4j.common.Immutable; 11 | 12 | /** 13 | * A new person was created in the system. 14 | */ 15 | @Immutable 16 | @HasSerializedDataTypeConstant 17 | public final class PersonCreatedEvent extends AbstractDomainEvent { 18 | 19 | private static final long serialVersionUID = 1000L; 20 | 21 | /** 22 | * Never changing unique event type name. 23 | */ 24 | public static final EventType TYPE = new EventType("PersonCreatedEvent"); 25 | 26 | /** 27 | * Unique name used for marshalling/unmarshalling the event. 28 | */ 29 | public static final SerializedDataType SER_TYPE = new SerializedDataType(PersonCreatedEvent.TYPE.asBaseType()); 30 | 31 | @NotNull 32 | @JsonbProperty("name") 33 | private PersonName name; 34 | 35 | /** 36 | * Protected default constructor for deserialization. 37 | */ 38 | protected PersonCreatedEvent() { 39 | super(); 40 | } 41 | 42 | @Override 43 | public final EventType getEventType() { 44 | return PersonCreatedEvent.TYPE; 45 | } 46 | 47 | /** 48 | * Returns: Name of a person. 49 | * 50 | * @return Current value. 51 | */ 52 | @NotNull 53 | public final PersonName getName() { 54 | return name; 55 | } 56 | 57 | @Override 58 | public final String toString() { 59 | return "Person '" + name + "' (" + getEntityId() + ") was created [Event " + getEventId() + "]"; 60 | } 61 | 62 | /** 63 | * Builds an instance of the outer class. 64 | */ 65 | public static final class Builder extends AbstractDomainEvent.Builder { 66 | 67 | private PersonCreatedEvent delegate; 68 | 69 | public Builder() { 70 | super(new PersonCreatedEvent()); 71 | delegate = delegate(); 72 | } 73 | 74 | public Builder id(PersonId personId) { 75 | entityIdPath(personId); 76 | return this; 77 | } 78 | 79 | public Builder name(String name) { 80 | delegate.name = new PersonName(name); 81 | return this; 82 | } 83 | 84 | public Builder name(PersonName name) { 85 | delegate.name = name; 86 | return this; 87 | } 88 | 89 | public Builder version(int version) { 90 | aggregateVersion(AggregateVersion.valueOf(version)); 91 | return this; 92 | } 93 | 94 | public PersonCreatedEvent build() { 95 | ensureBuildableAbstractDomainEvent(); 96 | ensureNotNull("name", delegate.name); 97 | 98 | final PersonCreatedEvent result = delegate; 99 | delegate = new PersonCreatedEvent(); 100 | resetAbstractDomainEvent(delegate); 101 | return result; 102 | } 103 | 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /quarkus/shared/src/main/java/org/fuin/cqrs4j/example/quarkus/shared/DeletePersonCommand.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.shared; 2 | 3 | import jakarta.json.bind.annotation.JsonbProperty; 4 | import jakarta.validation.constraints.NotNull; 5 | import org.fuin.cqrs4j.jsonb.AbstractAggregateCommand; 6 | import org.fuin.ddd4j.core.AggregateVersion; 7 | import org.fuin.ddd4j.core.DomainEventExpectedEntityIdPath; 8 | import org.fuin.ddd4j.core.EventType; 9 | import org.fuin.esc.api.SerializedDataType; 10 | import org.fuin.objects4j.common.Immutable; 11 | 12 | /** 13 | * A new person should be deleted from the system. 14 | */ 15 | @Immutable 16 | @DomainEventExpectedEntityIdPath(PersonId.class) 17 | public final class DeletePersonCommand extends AbstractAggregateCommand { 18 | 19 | private static final long serialVersionUID = 1000L; 20 | 21 | /** 22 | * Never changing unique event type name. 23 | */ 24 | public static final EventType TYPE = new EventType("DeletePersonCommand"); 25 | 26 | /** 27 | * Unique name used for marshalling/unmarshalling the event. 28 | */ 29 | public static final SerializedDataType SER_TYPE = new SerializedDataType(DeletePersonCommand.TYPE.asBaseType()); 30 | 31 | @NotNull 32 | @JsonbProperty("name") 33 | private PersonName name; 34 | 35 | /** 36 | * Protected default constructor for deserialization. 37 | */ 38 | protected DeletePersonCommand() { 39 | super(); 40 | } 41 | 42 | @Override 43 | public final EventType getEventType() { 44 | return DeletePersonCommand.TYPE; 45 | } 46 | 47 | /** 48 | * Returns: Name of a person. 49 | * 50 | * @return Current value. 51 | */ 52 | @NotNull 53 | public final PersonName getName() { 54 | return name; 55 | } 56 | 57 | @Override 58 | public final String toString() { 59 | return "Delete person '" + name + "' with identifier '" + getAggregateRootId() + "'"; 60 | } 61 | 62 | /** 63 | * Builds an instance of the outer class. 64 | */ 65 | public static final class Builder extends AbstractAggregateCommand.Builder { 66 | 67 | private DeletePersonCommand delegate; 68 | 69 | public Builder() { 70 | super(new DeletePersonCommand()); 71 | delegate = delegate(); 72 | } 73 | 74 | public Builder id(PersonId personId) { 75 | entityIdPath(personId); 76 | return this; 77 | } 78 | 79 | public Builder name(String name) { 80 | delegate.name = new PersonName(name); 81 | return this; 82 | } 83 | 84 | public Builder name(PersonName name) { 85 | delegate.name = name; 86 | return this; 87 | } 88 | 89 | public DeletePersonCommand build() { 90 | ensureBuildableAbstractAggregateCommand(); 91 | ensureNotNull("name", delegate.name); 92 | 93 | final DeletePersonCommand result = delegate; 94 | delegate = new DeletePersonCommand(); 95 | resetAbstractAggregateCommand(delegate); 96 | return result; 97 | } 98 | 99 | public Builder version(int version) { 100 | aggregateVersion(AggregateVersion.valueOf(version)); 101 | return this; 102 | } 103 | 104 | } 105 | 106 | } -------------------------------------------------------------------------------- /spring-boot/shared/src/main/java/org/fuin/cqrs4j/example/spring/shared/PersonDeletedEvent.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.shared; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import jakarta.validation.constraints.NotNull; 6 | import org.fuin.ddd4j.jackson.AbstractDomainEvent; 7 | import org.fuin.ddd4j.core.AggregateVersion; 8 | import org.fuin.ddd4j.core.EventType; 9 | import org.fuin.esc.api.HasSerializedDataTypeConstant; 10 | import org.fuin.esc.api.SerializedDataType; 11 | 12 | import javax.annotation.concurrent.Immutable; 13 | 14 | /** 15 | * A person was deleted from the system. 16 | */ 17 | @Immutable 18 | @HasSerializedDataTypeConstant 19 | public final class PersonDeletedEvent extends AbstractDomainEvent { 20 | 21 | private static final long serialVersionUID = 1000L; 22 | 23 | /** 24 | * Never changing unique event type name. 25 | */ 26 | public static final EventType TYPE = new EventType("PersonDeletedEvent"); 27 | 28 | /** 29 | * Unique name used for marshalling/unmarshalling the event. 30 | */ 31 | public static final SerializedDataType SER_TYPE = new SerializedDataType(PersonDeletedEvent.TYPE.asBaseType()); 32 | 33 | @NotNull 34 | @JsonProperty("name") 35 | private PersonName name; 36 | 37 | /** 38 | * Protected default constructor for deserialization. 39 | */ 40 | protected PersonDeletedEvent() { 41 | super(); 42 | } 43 | 44 | @Override 45 | @JsonIgnore 46 | public final EventType getEventType() { 47 | return PersonDeletedEvent.TYPE; 48 | } 49 | 50 | /** 51 | * Returns: Name of a person. 52 | * 53 | * @return Current value. 54 | */ 55 | @NotNull 56 | @JsonIgnore 57 | public final PersonName getName() { 58 | return name; 59 | } 60 | 61 | @Override 62 | public final String toString() { 63 | return "Deleted person '" + name + "' (" + getEntityId() + ") [Event " + getEventId() + "]"; 64 | } 65 | 66 | /** 67 | * Builds an instance of the outer class. 68 | */ 69 | public static final class Builder extends AbstractDomainEvent.Builder { 70 | 71 | private PersonDeletedEvent delegate; 72 | 73 | public Builder() { 74 | super(new PersonDeletedEvent()); 75 | delegate = delegate(); 76 | } 77 | 78 | public Builder id(PersonId personId) { 79 | entityIdPath(personId); 80 | return this; 81 | } 82 | 83 | public Builder name(String name) { 84 | delegate.name = new PersonName(name); 85 | return this; 86 | } 87 | 88 | public Builder name(PersonName name) { 89 | delegate.name = name; 90 | return this; 91 | } 92 | 93 | public Builder version(int version) { 94 | aggregateVersion(AggregateVersion.valueOf(version)); 95 | return this; 96 | } 97 | 98 | public PersonDeletedEvent build() { 99 | ensureBuildableAbstractDomainEvent(); 100 | ensureNotNull("name", delegate.name); 101 | 102 | final PersonDeletedEvent result = delegate; 103 | delegate = new PersonDeletedEvent(); 104 | resetAbstractDomainEvent(delegate); 105 | return result; 106 | } 107 | 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /spring-boot/shared/src/main/java/org/fuin/cqrs4j/example/spring/shared/PersonCreatedEvent.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.shared; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import jakarta.validation.constraints.NotNull; 6 | import org.fuin.ddd4j.jackson.AbstractDomainEvent; 7 | import org.fuin.ddd4j.core.AggregateVersion; 8 | import org.fuin.ddd4j.core.EventType; 9 | import org.fuin.esc.api.HasSerializedDataTypeConstant; 10 | import org.fuin.esc.api.SerializedDataType; 11 | 12 | import javax.annotation.concurrent.Immutable; 13 | 14 | /** 15 | * A new person was created in the system. 16 | */ 17 | @Immutable 18 | @HasSerializedDataTypeConstant 19 | public final class PersonCreatedEvent extends AbstractDomainEvent { 20 | 21 | private static final long serialVersionUID = 1000L; 22 | 23 | /** 24 | * Never changing unique event type name. 25 | */ 26 | public static final EventType TYPE = new EventType("PersonCreatedEvent"); 27 | 28 | /** 29 | * Unique name used for marshalling/unmarshalling the event. 30 | */ 31 | public static final SerializedDataType SER_TYPE = new SerializedDataType(PersonCreatedEvent.TYPE.asBaseType()); 32 | 33 | @NotNull 34 | @JsonProperty("name") 35 | private PersonName name; 36 | 37 | /** 38 | * Protected default constructor for deserialization. 39 | */ 40 | protected PersonCreatedEvent() { 41 | super(); 42 | } 43 | 44 | @Override 45 | @JsonIgnore 46 | public final EventType getEventType() { 47 | return PersonCreatedEvent.TYPE; 48 | } 49 | 50 | /** 51 | * Returns: Name of a person. 52 | * 53 | * @return Current value. 54 | */ 55 | @NotNull 56 | @JsonIgnore 57 | public final PersonName getName() { 58 | return name; 59 | } 60 | 61 | @Override 62 | public final String toString() { 63 | return "Person '" + name + "' (" + getEntityId() + ") was created [Event " + getEventId() + "]"; 64 | } 65 | 66 | /** 67 | * Builds an instance of the outer class. 68 | */ 69 | public static final class Builder extends AbstractDomainEvent.Builder { 70 | 71 | private PersonCreatedEvent delegate; 72 | 73 | public Builder() { 74 | super(new PersonCreatedEvent()); 75 | delegate = delegate(); 76 | } 77 | 78 | public Builder id(PersonId personId) { 79 | entityIdPath(personId); 80 | return this; 81 | } 82 | 83 | public Builder name(String name) { 84 | delegate.name = new PersonName(name); 85 | return this; 86 | } 87 | 88 | public Builder name(PersonName name) { 89 | delegate.name = name; 90 | return this; 91 | } 92 | 93 | public Builder version(int version) { 94 | aggregateVersion(AggregateVersion.valueOf(version)); 95 | return this; 96 | } 97 | 98 | public PersonCreatedEvent build() { 99 | ensureBuildableAbstractDomainEvent(); 100 | ensureNotNull("name", delegate.name); 101 | 102 | final PersonCreatedEvent result = delegate; 103 | delegate = new PersonCreatedEvent(); 104 | resetAbstractDomainEvent(delegate); 105 | return result; 106 | } 107 | 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /spring-boot/command/src/test/java/org/fuin/cqrs4j/example/spring/command/domain/PersonTest.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.command.domain; 2 | 3 | import org.fuin.cqrs4j.example.spring.shared.PersonCreatedEvent; 4 | import org.fuin.cqrs4j.example.spring.shared.PersonDeletedEvent; 5 | import org.fuin.cqrs4j.example.spring.shared.PersonId; 6 | import org.fuin.cqrs4j.example.spring.shared.PersonName; 7 | import org.fuin.ddd4j.core.AggregateDeletedException; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.util.Optional; 11 | import java.util.UUID; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.junit.jupiter.api.Assertions.fail; 15 | 16 | /** 17 | * Test for the {@link Person} class. 18 | */ 19 | class PersonTest { 20 | 21 | @Test 22 | void testCreateOK() throws DuplicatePersonNameException { 23 | 24 | // PREPARE 25 | final PersonId personId = new PersonId(UUID.fromString("f645969a-402d-41a9-882b-d2d8000d0f43")); 26 | final PersonName personName = new PersonName("Peter Parker"); 27 | 28 | // TEST 29 | final Person testee = new Person(personId, personName, pid -> { 30 | return Optional.empty(); 31 | }); 32 | 33 | // VERIFY 34 | assertThat(testee.getUncommittedChanges()).hasSize(1); 35 | assertThat(testee.getUncommittedChanges().get(0)).isInstanceOf(PersonCreatedEvent.class); 36 | final PersonCreatedEvent event = (PersonCreatedEvent) testee.getUncommittedChanges().get(0); 37 | assertThat(event.getEntityId()).isEqualTo(personId); 38 | assertThat(event.getAggregateVersionInteger()).isEqualTo(0); 39 | assertThat(event.getName()).isEqualTo(personName); 40 | 41 | } 42 | 43 | @Test 44 | void testCreateDuplicateName() { 45 | 46 | // PREPARE 47 | final PersonId personId = new PersonId(UUID.randomUUID()); 48 | final PersonName personName = new PersonName("Peter Parker"); 49 | final PersonId otherId = new PersonId(UUID.randomUUID()); 50 | 51 | // TEST & VERIFY 52 | try { 53 | new Person(personId, personName, pid -> { 54 | return Optional.of(otherId); 55 | }); 56 | fail("Excpected duplicate name exception"); 57 | } catch (final DuplicatePersonNameException ex) { 58 | assertThat(ex.getMessage()).isEqualTo("The name 'Peter Parker' already exists: " + otherId); 59 | } 60 | 61 | } 62 | 63 | @Test 64 | void testDeleteOK() throws DuplicatePersonNameException, AggregateDeletedException { 65 | 66 | // PREPARE 67 | final PersonId personId = new PersonId(UUID.randomUUID()); 68 | final PersonName personName = new PersonName("Peter Parker"); 69 | final PersonId otherId = new PersonId(UUID.randomUUID()); 70 | final Person testee = new Person(personId, personName, pid -> { 71 | return Optional.empty(); 72 | }); 73 | testee.markChangesAsCommitted(); 74 | 75 | // TEST 76 | testee.delete(); 77 | 78 | //VERIFY 79 | assertThat(testee.getUncommittedChanges()).hasSize(1); 80 | assertThat(testee.getUncommittedChanges().get(0)).isInstanceOf(PersonDeletedEvent.class); 81 | final PersonDeletedEvent event = (PersonDeletedEvent) testee.getUncommittedChanges().get(0); 82 | assertThat(event.getEntityId()).isEqualTo(personId); 83 | assertThat(event.getAggregateVersionInteger()).isEqualTo(1); 84 | assertThat(event.getName()).isEqualTo(personName); 85 | 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /quarkus/query/src/main/java/org/fuin/cqrs4j/example/quarkus/query/views/personlist/PersonListProjector.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.query.views.personlist; 2 | 3 | import jakarta.enterprise.context.ApplicationScoped; 4 | import jakarta.enterprise.event.ObservesAsync; 5 | import jakarta.inject.Inject; 6 | import org.fuin.cqrs4j.example.quarkus.query.app.QryCheckForViewUpdatesEvent; 7 | import org.fuin.ddd4j.core.EventType; 8 | import org.fuin.esc.api.ProjectionAdminEventStore; 9 | import org.fuin.esc.api.TypeName; 10 | import org.fuin.esc.esgrpc.IESGrpcEventStore; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.Set; 17 | import java.util.concurrent.Semaphore; 18 | 19 | import static org.fuin.utils4j.Utils4J.tryLocked; 20 | 21 | /** 22 | * Reads incoming events from an attached event store and dispatches them to the appropriate event handlers. 23 | */ 24 | @ApplicationScoped 25 | public class PersonListProjector { 26 | 27 | private static final Logger LOG = LoggerFactory.getLogger(PersonListProjector.class); 28 | 29 | /** Prevents more than one projector thread running at a time. */ 30 | private static final Semaphore LOCK = new Semaphore(1); 31 | 32 | // The following beans are NOT thread safe! 33 | // Above LOCK prevents multithreaded access 34 | 35 | @Inject 36 | IESGrpcEventStore eventstore; 37 | 38 | @Inject 39 | ProjectionAdminEventStore admin; 40 | 41 | @Inject 42 | PersonListEventChunkHandler chunkHandler; 43 | 44 | @Inject 45 | PersonListEventDispatcher dispatcher; 46 | 47 | /** 48 | * Listens for timer events. If a second timer event occurs while the previous call is still being executed, the method will simply be 49 | * skipped. 50 | * 51 | * @param event 52 | * Timer event. 53 | */ 54 | public void onEvent(@ObservesAsync final QryCheckForViewUpdatesEvent event) { 55 | tryLocked(LOCK, () -> { 56 | try { 57 | readStreamEvents(); 58 | } catch (final RuntimeException ex) { 59 | LOG.error("Error reading events from stream", ex); 60 | } 61 | }); 62 | } 63 | 64 | private void readStreamEvents() { 65 | 66 | // Create an event store projection if it does not exist. 67 | if (!admin.projectionExists(chunkHandler.getProjectionStreamId())) { 68 | final List typeNames = getEventTypeNames(); 69 | LOG.info("Create projection '{}' with events: {}", chunkHandler.getProjectionStreamId(), typeNames); 70 | admin.createProjection(chunkHandler.getProjectionStreamId(), true, typeNames); 71 | } 72 | 73 | // Read and dispatch events 74 | final Long nextEventNumber = chunkHandler.readNextEventNumber(); 75 | eventstore.readAllEventsForward(chunkHandler.getProjectionStreamId(), nextEventNumber, 100, 76 | currentSlice -> chunkHandler.handleChunk(currentSlice)); 77 | 78 | } 79 | 80 | /** 81 | * Returns a list of all event type names used for this projection. 82 | * 83 | * @return List of event names. 84 | */ 85 | public List getEventTypeNames() { 86 | final List typeNames = new ArrayList<>(); 87 | final Set eventTypes = dispatcher.getAllTypes(); 88 | for (final EventType eventType : eventTypes) { 89 | typeNames.add(new TypeName(eventType.asBaseType())); 90 | } 91 | return typeNames; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /quarkus/shared/src/main/java/org/fuin/cqrs4j/example/quarkus/shared/PersonId.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.shared; 2 | 3 | import jakarta.json.bind.adapter.JsonbAdapter; 4 | import jakarta.validation.constraints.NotNull; 5 | import org.fuin.ddd4j.core.AggregateRootUuid; 6 | import org.fuin.ddd4j.core.EntityType; 7 | import org.fuin.ddd4j.core.HasEntityTypeConstant; 8 | import org.fuin.ddd4j.core.StringBasedEntityType; 9 | import org.fuin.objects4j.common.HasPublicStaticValueOfMethod; 10 | import org.fuin.objects4j.common.Immutable; 11 | import org.fuin.objects4j.ui.Label; 12 | import org.fuin.objects4j.ui.ShortLabel; 13 | import org.fuin.objects4j.ui.Tooltip; 14 | 15 | import java.util.UUID; 16 | 17 | /** 18 | * Identifies uniquely a person. 19 | */ 20 | @ShortLabel(bundle = "ddd-cqrs-4-java-example", key = "PersonId.slabel", value = "PID") 21 | @Label(bundle = "ddd-cqrs-4-java-example", key = "PersonId.label", value = "Person's ID") 22 | @Tooltip(bundle = "ddd-cqrs-4-java-example", key = "PersonId.tooltip", value = "Unique identifier of a person") 23 | @Immutable 24 | @HasPublicStaticValueOfMethod 25 | @HasEntityTypeConstant 26 | public final class PersonId extends AggregateRootUuid { 27 | 28 | private static final long serialVersionUID = 1000L; 29 | 30 | /** 31 | * Unique name of the aggregate this identifier refers to. 32 | */ 33 | public static final EntityType TYPE = new StringBasedEntityType("PERSON"); 34 | 35 | /** 36 | * Default constructor. 37 | */ 38 | protected PersonId() { 39 | super(PersonId.TYPE); 40 | } 41 | 42 | /** 43 | * Constructor with all data. 44 | * 45 | * @param value Persistent value. 46 | */ 47 | public PersonId(@NotNull final UUID value) { 48 | super(PersonId.TYPE, value); 49 | } 50 | 51 | /** 52 | * Verifies if the given string can be converted into a Person ID. 53 | * 54 | * @param value String with valid UUID string. A null value is also valid. 55 | * @return {@literal true} if the string is a valid UUID. 56 | */ 57 | public static boolean isValid(final String value) { 58 | if (value == null) { 59 | return true; 60 | } 61 | return AggregateRootUuid.isValid(value); 62 | } 63 | 64 | /** 65 | * Parses a given string and returns a new instance of PersonId. 66 | * 67 | * @param value String with valid UUID to convert. A null value returns null. 68 | * @return Converted value. 69 | */ 70 | public static PersonId valueOf(final String value) { 71 | if (value == null) { 72 | return null; 73 | } 74 | AggregateRootUuid.requireArgValid("value", value); 75 | return new PersonId(UUID.fromString(value)); 76 | } 77 | 78 | /** 79 | * Converts the value object from/to UUID. 80 | */ 81 | public static final class Converter implements JsonbAdapter { 82 | 83 | @Override 84 | public final UUID adaptToJson(final PersonId obj) throws Exception { 85 | if (obj == null) { 86 | return null; 87 | } 88 | return obj.asBaseType(); 89 | } 90 | 91 | @Override 92 | public final PersonId adaptFromJson(final UUID value) throws Exception { 93 | if (value == null) { 94 | return null; 95 | } 96 | return new PersonId(value); 97 | } 98 | 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /spring-boot/shared/src/main/java/org/fuin/cqrs4j/example/spring/shared/Config.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.shared; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.stereotype.Component; 5 | 6 | @Component 7 | public class Config { 8 | 9 | private static final String EVENT_STORE_PROTOCOL = "http"; 10 | 11 | private static final String EVENT_STORE_HOST = "localhost"; 12 | 13 | private static final int EVENT_STORE_HTTP_PORT = 2113; 14 | 15 | private static final String EVENT_STORE_USER = "admin"; 16 | 17 | private static final String EVENT_STORE_PASSWORD = "changeit"; 18 | 19 | @Value("${EVENT_STORE_PROTOCOL:http}") 20 | private String eventStoreProtocol; 21 | 22 | @Value("${EVENT_STORE_HOST:localhost}") 23 | private String eventStoreHost; 24 | 25 | @Value("${EVENT_STORE_HTTP_PORT:2113}") 26 | private int eventStoreHttpPort; 27 | 28 | @Value("${EVENT_STORE_USER:admin}") 29 | private String eventStoreUser; 30 | 31 | @Value("${EVENT_STORE_PASSWORD:changeit}") 32 | private String eventStorePassword; 33 | 34 | /** 35 | * Constructor using default values internally. 36 | */ 37 | public Config() { 38 | super(); 39 | this.eventStoreProtocol = EVENT_STORE_PROTOCOL; 40 | this.eventStoreHost = EVENT_STORE_HOST; 41 | this.eventStoreHttpPort = EVENT_STORE_HTTP_PORT; 42 | this.eventStoreUser = EVENT_STORE_USER; 43 | this.eventStorePassword = EVENT_STORE_PASSWORD; 44 | } 45 | 46 | /** 47 | * Constructor with all data. 48 | * 49 | * @param eventStoreProtocol 50 | * Protocol. 51 | * @param eventStoreHost 52 | * Host. 53 | * @param eventStoreHttpPort 54 | * HTTP port 55 | * @param eventStoreUser 56 | * User. 57 | * @param eventStorePassword 58 | * Password. 59 | */ 60 | public Config(final String eventStoreProtocol, final String eventStoreHost, final int eventStoreHttpPort, 61 | final String eventStoreUser, final String eventStorePassword) { 62 | super(); 63 | this.eventStoreProtocol = eventStoreProtocol; 64 | this.eventStoreHost = eventStoreHost; 65 | this.eventStoreHttpPort = eventStoreHttpPort; 66 | this.eventStoreUser = eventStoreUser; 67 | this.eventStorePassword = eventStorePassword; 68 | } 69 | 70 | /** 71 | * Returns the protocol of the event store. 72 | * 73 | * @return Either http or https. 74 | */ 75 | public String getEventStoreProtocol() { 76 | return eventStoreProtocol; 77 | } 78 | 79 | /** 80 | * Returns the host name of the event store. 81 | * 82 | * @return Name. 83 | */ 84 | public String getEventStoreHost() { 85 | return eventStoreHost; 86 | } 87 | 88 | /** 89 | * Returns the HTTP port of the event store. 90 | * 91 | * @return Port. 92 | */ 93 | public int getEventStoreHttpPort() { 94 | return eventStoreHttpPort; 95 | } 96 | 97 | /** 98 | * Returns the username of the event store. 99 | * 100 | * @return Username. 101 | */ 102 | public String getEventStoreUser() { 103 | return eventStoreUser; 104 | } 105 | 106 | /** 107 | * Returns the password of the event store. 108 | * 109 | * @return Password. 110 | */ 111 | public String getEventStorePassword() { 112 | return eventStorePassword; 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /spring-boot/command/src/main/java/org/fuin/cqrs4j/example/spring/command/domain/CreatePersonCommand.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.command.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import jakarta.validation.constraints.NotNull; 6 | import org.fuin.cqrs4j.example.spring.shared.PersonId; 7 | import org.fuin.cqrs4j.example.spring.shared.PersonName; 8 | import org.fuin.cqrs4j.jackson.AbstractAggregateCommand; 9 | import org.fuin.ddd4j.core.DomainEventExpectedEntityIdPath; 10 | import org.fuin.ddd4j.core.EventType; 11 | import org.fuin.esc.api.HasSerializedDataTypeConstant; 12 | import org.fuin.esc.api.SerializedDataType; 13 | 14 | import javax.annotation.concurrent.Immutable; 15 | import java.io.Serial; 16 | 17 | /** 18 | * A new person should be created in the system. 19 | */ 20 | @Immutable 21 | @HasSerializedDataTypeConstant 22 | @DomainEventExpectedEntityIdPath(PersonId.class) 23 | public final class CreatePersonCommand extends AbstractAggregateCommand { 24 | 25 | @Serial 26 | private static final long serialVersionUID = 1000L; 27 | 28 | /** 29 | * Never changing unique event type name. 30 | */ 31 | public static final EventType TYPE = new EventType("CreatePersonCommand"); 32 | 33 | /** 34 | * Unique name used for marshalling/unmarshalling the event. 35 | */ 36 | public static final SerializedDataType SER_TYPE = new SerializedDataType(CreatePersonCommand.TYPE.asBaseType()); 37 | 38 | @NotNull 39 | @JsonProperty("name") 40 | private PersonName name; 41 | 42 | /** 43 | * Protected default constructor for deserialization. 44 | */ 45 | protected CreatePersonCommand() { 46 | super(); 47 | } 48 | 49 | @Override 50 | @JsonIgnore 51 | public final EventType getEventType() { 52 | return CreatePersonCommand.TYPE; 53 | } 54 | 55 | /** 56 | * Returns: Name of a person. 57 | * 58 | * @return Current value. 59 | */ 60 | @NotNull 61 | @JsonIgnore 62 | public final PersonName getName() { 63 | return name; 64 | } 65 | 66 | @Override 67 | public final String toString() { 68 | return "Create person '" + name + "' with identifier '" + getAggregateRootId() + "'"; 69 | } 70 | 71 | /** 72 | * Builds an instance of the outer class. 73 | */ 74 | public static final class Builder extends AbstractAggregateCommand.Builder { 75 | 76 | private CreatePersonCommand delegate; 77 | 78 | public Builder() { 79 | super(new CreatePersonCommand()); 80 | delegate = delegate(); 81 | } 82 | 83 | public Builder id(PersonId personId) { 84 | entityIdPath(personId); 85 | return this; 86 | } 87 | 88 | public Builder name(String name) { 89 | delegate.name = new PersonName(name); 90 | return this; 91 | } 92 | 93 | public Builder name(PersonName name) { 94 | delegate.name = name; 95 | return this; 96 | } 97 | 98 | public CreatePersonCommand build() { 99 | ensureBuildableAbstractAggregateCommand(); 100 | ensureNotNull("name", delegate.name); 101 | 102 | final CreatePersonCommand result = delegate; 103 | delegate = new CreatePersonCommand(); 104 | resetAbstractAggregateCommand(delegate); 105 | return result; 106 | } 107 | 108 | } 109 | 110 | } -------------------------------------------------------------------------------- /spring-boot/shared/src/test/java/org/fuin/cqrs4j/example/spring/shared/PersonCreatedEventTest.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.shared; 2 | 3 | import org.fuin.objects4j.jackson.ImmutableObjectMapper; 4 | import org.junit.jupiter.api.AfterAll; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.UUID; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.fuin.utils4j.Utils4J.deserialize; 12 | import static org.fuin.utils4j.Utils4J.serialize; 13 | 14 | // CHECKSTYLE:OFF 15 | class PersonCreatedEventTest { 16 | 17 | private static ImmutableObjectMapper mapper; 18 | 19 | @BeforeAll 20 | static void beforeAll() { 21 | final SharedConfig factory = new SharedConfig(); 22 | final ImmutableObjectMapper.Builder mapperBuilder = factory.immutableObjectMapperBuilder(factory.entityIdFactory()); 23 | final ImmutableObjectMapper.Provider mapperProvider = factory.immutableObjectMapperProvider(mapperBuilder); 24 | mapper = mapperProvider.mapper(); 25 | } 26 | 27 | @AfterAll 28 | static void afterAll() { 29 | mapper = null; 30 | } 31 | 32 | @Test 33 | void testSerializeDeserialize() { 34 | 35 | // PREPARE 36 | final PersonCreatedEvent original = createTestee(); 37 | 38 | // TEST 39 | final PersonCreatedEvent copy = deserialize(serialize(original)); 40 | 41 | // VERIFY 42 | assertThat(copy).isEqualTo(original); 43 | assertThat(copy.getName()).isEqualTo(original.getName()); 44 | 45 | } 46 | 47 | @Test 48 | void testMarshalUnmarshalJson() throws Exception { 49 | 50 | // PREPARE 51 | final PersonCreatedEvent original = createTestee(); 52 | 53 | // TEST 54 | final String json = mapper.writer().writeValueAsString(original); 55 | final PersonCreatedEvent copy = mapper.reader().readValue(json, PersonCreatedEvent.class); 56 | 57 | // VERIFY 58 | assertThat(copy).isEqualTo(original); 59 | assertThat(copy.getName()).isEqualTo(original.getName()); 60 | 61 | } 62 | 63 | @Test 64 | void testUnmarshalJson() throws Exception { 65 | 66 | // PREPARE 67 | final PersonCreatedEvent original = createTestee(); 68 | 69 | // TEST 70 | final String json = """ 71 | { 72 | "event-id": "a7b88543-ce32-40eb-a3fe-f49aec39b570", 73 | "event-timestamp": "2019-11-02T09:56:40.669Z[Etc/UTC]", 74 | "entity-id-path": "PERSON f645969a-402d-41a9-882b-d2d8000d0f43", 75 | "aggregate-version": 0, 76 | "name": "Peter Parker" 77 | } 78 | """; 79 | final PersonCreatedEvent copy = mapper.reader().readValue(json, PersonCreatedEvent.class); 80 | 81 | // VERIFY 82 | assertThat(copy.getEntityIdPath()).isEqualTo(original.getEntityIdPath()); 83 | assertThat(copy.getName()).isEqualTo(original.getName()); 84 | 85 | } 86 | 87 | @Test 88 | void testToString() { 89 | final PersonCreatedEvent testee = createTestee(); 90 | assertThat(testee) 91 | .hasToString("Person 'Peter Parker' (" + testee.getEntityId() + ") was created [Event " + testee.getEventId() + "]"); 92 | } 93 | 94 | private PersonCreatedEvent createTestee() { 95 | final PersonId personId = new PersonId(UUID.fromString("f645969a-402d-41a9-882b-d2d8000d0f43")); 96 | final PersonName personName = new PersonName("Peter Parker"); 97 | return new PersonCreatedEvent.Builder().id(personId).name(personName).version(0).build(); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /spring-boot/command/src/main/java/org/fuin/cqrs4j/example/spring/command/domain/DeletePersonCommand.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.command.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import jakarta.validation.constraints.NotNull; 6 | import org.fuin.cqrs4j.example.spring.shared.PersonId; 7 | import org.fuin.cqrs4j.example.spring.shared.PersonName; 8 | import org.fuin.cqrs4j.jackson.AbstractAggregateCommand; 9 | import org.fuin.ddd4j.core.AggregateVersion; 10 | import org.fuin.ddd4j.core.DomainEventExpectedEntityIdPath; 11 | import org.fuin.ddd4j.core.EventType; 12 | import org.fuin.esc.api.HasSerializedDataTypeConstant; 13 | import org.fuin.esc.api.SerializedDataType; 14 | 15 | import javax.annotation.concurrent.Immutable; 16 | import java.io.Serial; 17 | 18 | /** 19 | * A new person should be deleted from the system. 20 | */ 21 | @Immutable 22 | @HasSerializedDataTypeConstant 23 | @DomainEventExpectedEntityIdPath(PersonId.class) 24 | public final class DeletePersonCommand extends AbstractAggregateCommand { 25 | 26 | @Serial 27 | private static final long serialVersionUID = 1000L; 28 | 29 | /** 30 | * Never changing unique event type name. 31 | */ 32 | public static final EventType TYPE = new EventType("DeletePersonCommand"); 33 | 34 | /** 35 | * Unique name used for marshalling/unmarshalling the event. 36 | */ 37 | public static final SerializedDataType SER_TYPE = new SerializedDataType(DeletePersonCommand.TYPE.asBaseType()); 38 | 39 | @NotNull 40 | @JsonProperty("name") 41 | private PersonName name; 42 | 43 | /** 44 | * Protected default constructor for deserialization. 45 | */ 46 | protected DeletePersonCommand() { 47 | super(); 48 | } 49 | 50 | @Override 51 | @JsonIgnore 52 | public final EventType getEventType() { 53 | return DeletePersonCommand.TYPE; 54 | } 55 | 56 | /** 57 | * Returns: Name of a person. 58 | * 59 | * @return Current value. 60 | */ 61 | @NotNull 62 | @JsonIgnore 63 | public final PersonName getName() { 64 | return name; 65 | } 66 | 67 | @Override 68 | public final String toString() { 69 | return "Delete person '" + name + "' with identifier '" + getAggregateRootId() + "'"; 70 | } 71 | 72 | /** 73 | * Builds an instance of the outer class. 74 | */ 75 | public static final class Builder extends AbstractAggregateCommand.Builder { 76 | 77 | private DeletePersonCommand delegate; 78 | 79 | public Builder() { 80 | super(new DeletePersonCommand()); 81 | delegate = delegate(); 82 | } 83 | 84 | public Builder id(PersonId personId) { 85 | entityIdPath(personId); 86 | return this; 87 | } 88 | 89 | public Builder name(String name) { 90 | delegate.name = new PersonName(name); 91 | return this; 92 | } 93 | 94 | public Builder name(PersonName name) { 95 | delegate.name = name; 96 | return this; 97 | } 98 | 99 | public DeletePersonCommand build() { 100 | ensureBuildableAbstractAggregateCommand(); 101 | ensureNotNull("name", delegate.name); 102 | 103 | final DeletePersonCommand result = delegate; 104 | delegate = new DeletePersonCommand(); 105 | resetAbstractAggregateCommand(delegate); 106 | return result; 107 | } 108 | 109 | public Builder version(int version) { 110 | aggregateVersion(AggregateVersion.valueOf(version)); 111 | return this; 112 | } 113 | 114 | } 115 | 116 | } -------------------------------------------------------------------------------- /spring-boot/shared/src/test/java/org/fuin/cqrs4j/example/spring/shared/PersonDeletedEventTest.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.shared; 2 | 3 | import org.fuin.objects4j.jackson.ImmutableObjectMapper; 4 | import org.junit.jupiter.api.AfterAll; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.UUID; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.fuin.utils4j.Utils4J.deserialize; 12 | import static org.fuin.utils4j.Utils4J.serialize; 13 | 14 | class PersonDeletedEventTest { 15 | 16 | private static ImmutableObjectMapper mapper; 17 | 18 | @BeforeAll 19 | static void beforeAll() { 20 | final SharedConfig factory = new SharedConfig(); 21 | final ImmutableObjectMapper.Builder mapperBuilder = factory.immutableObjectMapperBuilder(factory.entityIdFactory()); 22 | final ImmutableObjectMapper.Provider mapperProvider = factory.immutableObjectMapperProvider(mapperBuilder); 23 | mapper = mapperProvider.mapper(); 24 | } 25 | 26 | @AfterAll 27 | static void afterAll() { 28 | mapper = null; 29 | } 30 | 31 | @Test 32 | void testSerializeDeserialize() { 33 | 34 | // PREPARE 35 | final PersonDeletedEvent original = createTestee(); 36 | 37 | // TEST 38 | final PersonDeletedEvent copy = deserialize(serialize(original)); 39 | 40 | // VERIFY 41 | assertThat(copy).isEqualTo(original); 42 | assertThat(copy.getName()).isEqualTo(original.getName()); 43 | 44 | } 45 | 46 | @Test 47 | void testMarshalUnmarshalJson() throws Exception { 48 | 49 | // PREPARE 50 | final PersonDeletedEvent original = createTestee(); 51 | 52 | // TEST 53 | final String json = mapper.writer().writeValueAsString(original); 54 | final PersonDeletedEvent copy = mapper.reader().readValue(json, PersonDeletedEvent.class); 55 | 56 | // VERIFY 57 | assertThat(copy).isEqualTo(original); 58 | assertThat(copy.getName()).isEqualTo(original.getName()); 59 | assertThat(copy.getAggregateVersionInteger()).isEqualTo(1L); 60 | 61 | } 62 | 63 | @Test 64 | void testUnmarshalJson() throws Exception { 65 | 66 | // PREPARE 67 | final PersonDeletedEvent original = createTestee(); 68 | 69 | // TEST 70 | final String json = """ 71 | { 72 | "event-id": "a7b88543-ce32-40eb-a3fe-f49aec39b570", 73 | "event-timestamp": "2019-11-02T09:56:40.669Z[Etc/UTC]", 74 | "entity-id-path": "PERSON f645969a-402d-41a9-882b-d2d8000d0f43", 75 | "aggregate-version": 1, 76 | "name": "Peter Parker" 77 | } 78 | """; 79 | final PersonDeletedEvent copy = mapper.reader().readValue(json, PersonDeletedEvent.class); 80 | 81 | // VERIFY 82 | assertThat(copy.getEntityIdPath()).isEqualTo(original.getEntityIdPath()); 83 | assertThat(copy.getName()).isEqualTo(original.getName()); 84 | assertThat(copy.getAggregateVersionInteger()).isEqualTo(1L); 85 | 86 | } 87 | 88 | @Test 89 | void testToString() { 90 | final PersonDeletedEvent testee = createTestee(); 91 | assertThat(testee) 92 | .hasToString("Deleted person 'Peter Parker' (" + testee.getEntityId() + ") [Event " + testee.getEventId() + "]"); 93 | } 94 | 95 | private PersonDeletedEvent createTestee() { 96 | final PersonId personId = new PersonId(UUID.fromString("f645969a-402d-41a9-882b-d2d8000d0f43")); 97 | final PersonName personName = new PersonName("Peter Parker"); 98 | return new PersonDeletedEvent.Builder().id(personId).name(personName).version(1).build(); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /quarkus/command/src/test/java/org/fuin/cqrs4j/example/quarkus/command/domain/PersonTest.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.command.domain; 2 | 3 | import org.fuin.cqrs4j.example.aggregates.DuplicatePersonNameException; 4 | import org.fuin.cqrs4j.example.aggregates.Person; 5 | import org.fuin.cqrs4j.example.shared.PersonCreatedEvent; 6 | import org.fuin.cqrs4j.example.shared.PersonDeletedEvent; 7 | import org.fuin.cqrs4j.example.shared.PersonId; 8 | import org.fuin.cqrs4j.example.shared.PersonName; 9 | import org.fuin.ddd4j.core.AggregateDeletedException; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import java.util.Optional; 13 | import java.util.UUID; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.junit.jupiter.api.Assertions.fail; 17 | 18 | /** 19 | * Test for the {@link org.fuin.cqrs4j.example.aggregates.Person} class. 20 | */ 21 | public class PersonTest { 22 | 23 | @Test 24 | public final void testCreateOK() throws org.fuin.cqrs4j.example.aggregates.DuplicatePersonNameException { 25 | 26 | // PREPARE 27 | final PersonId personId = new PersonId(UUID.fromString("f645969a-402d-41a9-882b-d2d8000d0f43")); 28 | final PersonName personName = new PersonName("Peter Parker"); 29 | 30 | // TEST 31 | final org.fuin.cqrs4j.example.aggregates.Person testee = new org.fuin.cqrs4j.example.aggregates.Person(personId, personName, pid -> { 32 | return Optional.empty(); 33 | }); 34 | 35 | // VERIFY 36 | assertThat(testee.getUncommittedChanges()).hasSize(1); 37 | assertThat(testee.getUncommittedChanges().get(0)).isInstanceOf(PersonCreatedEvent.class); 38 | final PersonCreatedEvent event = (PersonCreatedEvent) testee.getUncommittedChanges().get(0); 39 | assertThat(event.getEntityId()).isEqualTo(personId); 40 | assertThat(event.getAggregateVersionInteger()).isEqualTo(0); 41 | assertThat(event.getName()).isEqualTo(personName); 42 | 43 | } 44 | 45 | @Test 46 | public final void testCreateDuplicateName() { 47 | 48 | // PREPARE 49 | final PersonId personId = new PersonId(UUID.randomUUID()); 50 | final PersonName personName = new PersonName("Peter Parker"); 51 | final PersonId otherId = new PersonId(UUID.randomUUID()); 52 | 53 | // TEST & VERIFY 54 | try { 55 | new org.fuin.cqrs4j.example.aggregates.Person(personId, personName, pid -> { 56 | return Optional.of(otherId); 57 | }); 58 | fail("Excpected duplicate name exception"); 59 | } catch (final org.fuin.cqrs4j.example.aggregates.DuplicatePersonNameException ex) { 60 | assertThat(ex.getMessage()).isEqualTo("The name 'Peter Parker' already exists: " + otherId); 61 | } 62 | 63 | } 64 | 65 | @Test 66 | public void testDeleteOK() throws DuplicatePersonNameException, AggregateDeletedException { 67 | 68 | // PREPARE 69 | final PersonId personId = new PersonId(UUID.randomUUID()); 70 | final PersonName personName = new PersonName("Peter Parker"); 71 | final PersonId otherId = new PersonId(UUID.randomUUID()); 72 | final org.fuin.cqrs4j.example.aggregates.Person testee = new Person(personId, personName, pid -> { 73 | return Optional.empty(); 74 | }); 75 | testee.markChangesAsCommitted(); 76 | 77 | // TEST 78 | testee.delete(); 79 | 80 | //VERIFY 81 | assertThat(testee.getUncommittedChanges()).hasSize(1); 82 | assertThat(testee.getUncommittedChanges().get(0)).isInstanceOf(PersonDeletedEvent.class); 83 | final PersonDeletedEvent event = (PersonDeletedEvent) testee.getUncommittedChanges().get(0); 84 | assertThat(event.getEntityId()).isEqualTo(personId); 85 | assertThat(event.getAggregateVersionInteger()).isEqualTo(1); 86 | assertThat(event.getName()).isEqualTo(personName); 87 | 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /quarkus/command/src/main/java/org/fuin/cqrs4j/example/quarkus/command/domain/Person.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.command.domain; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | import org.fuin.cqrs4j.example.shared.PersonCreatedEvent; 5 | import org.fuin.cqrs4j.example.shared.PersonDeletedEvent; 6 | import org.fuin.cqrs4j.example.shared.PersonId; 7 | import org.fuin.cqrs4j.example.shared.PersonName; 8 | import org.fuin.ddd4j.core.AbstractAggregateRoot; 9 | import org.fuin.ddd4j.core.AggregateDeletedException; 10 | import org.fuin.ddd4j.core.ApplyEvent; 11 | import org.fuin.ddd4j.core.EntityType; 12 | import org.fuin.objects4j.common.Contract; 13 | 14 | import java.io.Serializable; 15 | import java.util.Optional; 16 | 17 | /** 18 | * Represents a natural person. 19 | */ 20 | public class Person extends AbstractAggregateRoot implements Serializable { 21 | 22 | private static final long serialVersionUID = 1000L; 23 | 24 | @NotNull 25 | private PersonId id; 26 | 27 | @NotNull 28 | private PersonName name; 29 | 30 | private boolean deleted; 31 | 32 | /** 33 | * Default constructor that is mandatory for aggregate roots. 34 | */ 35 | public Person() { 36 | super(); 37 | } 38 | 39 | /** 40 | * Constructor with all data. 41 | * 42 | * @param id 43 | * Unique identifier of the person. 44 | * @param name 45 | * Unique name of the person. 46 | * @param service 47 | * Service required by the method. 48 | * 49 | * @throws DuplicatePersonNameException 50 | * The name already exists for another person. 51 | */ 52 | public Person(@NotNull final PersonId id, @NotNull final PersonName name, final CreatePersonService service) 53 | throws DuplicatePersonNameException { 54 | super(); 55 | 56 | // VERIFY PRECONDITIONS 57 | Contract.requireArgNotNull("id", id); 58 | Contract.requireArgNotNull("name", name); 59 | 60 | // VERIFY BUSINESS RULES 61 | 62 | // Rule 1: The name of the person must be unique 63 | final Optional otherId = service.loadPersonIdByName(name); 64 | if (otherId.isPresent()) { 65 | throw new DuplicatePersonNameException(otherId.get(), name); 66 | } 67 | 68 | // CREATE EVENT 69 | apply(new PersonCreatedEvent.Builder().id(id).name(name).version(getNextVersion() + 1).build()); 70 | 71 | } 72 | 73 | /** 74 | * Deletes the person. 75 | * 76 | * @throws AggregateDeletedException The aggregate was already deleted. 77 | */ 78 | public void delete() throws AggregateDeletedException { 79 | if (deleted) { 80 | throw new AggregateDeletedException(PersonId.TYPE, id); 81 | } 82 | apply(new PersonDeletedEvent.Builder().id(id).name(name).version(getNextVersion() + 1).build()); 83 | } 84 | 85 | @Override 86 | public PersonId getId() { 87 | return id; 88 | } 89 | 90 | @Override 91 | public EntityType getType() { 92 | return PersonId.TYPE; 93 | } 94 | 95 | @ApplyEvent 96 | public void applyEvent(final PersonCreatedEvent event) { 97 | this.id = event.getEntityId(); 98 | this.name = event.getName(); 99 | } 100 | 101 | @ApplyEvent 102 | public void applyEvent(final PersonDeletedEvent event) { 103 | this.deleted = true; 104 | } 105 | 106 | /** 107 | * Service for the constructor. 108 | */ 109 | public static interface CreatePersonService { 110 | 111 | /** 112 | * Loads the person's identifier for a given name. 113 | * 114 | * @param name 115 | * Person's name. 116 | * 117 | * @return Office identifier or empty if not found. 118 | */ 119 | public Optional loadPersonIdByName(@NotNull PersonName name); 120 | 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /spring-boot/command/src/main/java/org/fuin/cqrs4j/example/spring/command/domain/Person.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.command.domain; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | import org.fuin.cqrs4j.example.spring.shared.PersonCreatedEvent; 5 | import org.fuin.cqrs4j.example.spring.shared.PersonDeletedEvent; 6 | import org.fuin.cqrs4j.example.spring.shared.PersonId; 7 | import org.fuin.cqrs4j.example.spring.shared.PersonName; 8 | import org.fuin.ddd4j.core.AbstractAggregateRoot; 9 | import org.fuin.ddd4j.core.AggregateDeletedException; 10 | import org.fuin.ddd4j.core.ApplyEvent; 11 | import org.fuin.ddd4j.core.EntityType; 12 | import org.fuin.objects4j.common.Contract; 13 | 14 | import java.io.Serializable; 15 | import java.util.Optional; 16 | 17 | /** 18 | * Represents a natural person. 19 | */ 20 | public class Person extends AbstractAggregateRoot implements Serializable { 21 | 22 | private static final long serialVersionUID = 1000L; 23 | 24 | @NotNull 25 | private PersonId id; 26 | 27 | @NotNull 28 | private PersonName name; 29 | 30 | private boolean deleted; 31 | 32 | /** 33 | * Default constructor that is mandatory for aggregate roots. 34 | */ 35 | public Person() { 36 | super(); 37 | } 38 | 39 | /** 40 | * Constructor with all data. 41 | * 42 | * @param id 43 | * Unique identifier of the person. 44 | * @param name 45 | * Unique name of the person. 46 | * @param service 47 | * Service required by the method. 48 | * 49 | * @throws DuplicatePersonNameException 50 | * The name already exists for another person. 51 | */ 52 | public Person(@NotNull final PersonId id, @NotNull final PersonName name, final CreatePersonService service) 53 | throws DuplicatePersonNameException { 54 | super(); 55 | 56 | // VERIFY PRECONDITIONS 57 | Contract.requireArgNotNull("id", id); 58 | Contract.requireArgNotNull("name", name); 59 | 60 | // VERIFY BUSINESS RULES 61 | 62 | // Rule 1: The name of the person must be unique 63 | final Optional otherId = service.loadPersonIdByName(name); 64 | if (otherId.isPresent()) { 65 | throw new DuplicatePersonNameException(otherId.get(), name); 66 | } 67 | 68 | // CREATE EVENT 69 | apply(new PersonCreatedEvent.Builder().id(id).name(name).version(getNextVersion() + 1).build()); 70 | 71 | } 72 | 73 | /** 74 | * Deletes the person. 75 | * 76 | * @throws AggregateDeletedException The aggregate was already deleted. 77 | */ 78 | public void delete() throws AggregateDeletedException { 79 | if (deleted) { 80 | throw new AggregateDeletedException(PersonId.TYPE, id); 81 | } 82 | apply(new PersonDeletedEvent.Builder().id(id).name(name).version(getNextVersion() + 1).build()); 83 | } 84 | 85 | @Override 86 | public PersonId getId() { 87 | return id; 88 | } 89 | 90 | @Override 91 | public EntityType getType() { 92 | return PersonId.TYPE; 93 | } 94 | 95 | @ApplyEvent 96 | public void applyEvent(final PersonCreatedEvent event) { 97 | this.id = event.getEntityId(); 98 | this.name = event.getName(); 99 | } 100 | 101 | @ApplyEvent 102 | public void applyEvent(final PersonDeletedEvent event) { 103 | this.deleted = true; 104 | } 105 | 106 | /** 107 | * Service for the constructor. 108 | */ 109 | public static interface CreatePersonService { 110 | 111 | /** 112 | * Loads the person's identifier for a given name. 113 | * 114 | * @param name 115 | * Person's name. 116 | * 117 | * @return Office identifier or empty if not found. 118 | */ 119 | public Optional loadPersonIdByName(@NotNull PersonName name); 120 | 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /spring-boot/command/src/test/java/org/fuin/cqrs4j/example/spring/command/domain/CreatePersonCommandTest.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.spring.command.domain; 2 | 3 | import org.fuin.cqrs4j.example.spring.shared.PersonId; 4 | import org.fuin.cqrs4j.example.spring.shared.PersonName; 5 | import org.fuin.cqrs4j.example.spring.shared.SharedConfig; 6 | import org.fuin.objects4j.jackson.ImmutableObjectMapper; 7 | import org.fuin.utils4j.Utils4J; 8 | import org.junit.jupiter.api.AfterAll; 9 | import org.junit.jupiter.api.BeforeAll; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import java.time.ZonedDateTime; 13 | import java.util.UUID; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | 17 | /** 18 | * Test for {@link CreatePersonCommand} class. 19 | */ 20 | class CreatePersonCommandTest { 21 | 22 | private static final String PERSON_UUID = "84565d62-115e-4502-b7c9-38ad69c64b05"; 23 | 24 | private static ImmutableObjectMapper mapper; 25 | 26 | @BeforeAll 27 | static void beforeAll() { 28 | final SharedConfig factory = new SharedConfig(); 29 | final ImmutableObjectMapper.Builder mapperBuilder = factory.immutableObjectMapperBuilder(factory.entityIdFactory()); 30 | final ImmutableObjectMapper.Provider mapperProvider = factory.immutableObjectMapperProvider(mapperBuilder); 31 | mapper = mapperProvider.mapper(); 32 | } 33 | 34 | @AfterAll 35 | static void afterAll() { 36 | mapper = null; 37 | } 38 | 39 | @Test 40 | void testSerializeDeserialize() { 41 | 42 | // PREPARE 43 | final CreatePersonCommand original = createTestee(); 44 | 45 | // TEST 46 | final CreatePersonCommand copy = Utils4J.deserialize(Utils4J.serialize(original)); 47 | 48 | // VERIFY 49 | assertThat(copy).isEqualTo(original); 50 | assertThat(copy.getAggregateRootId()).isEqualTo(original.getAggregateRootId()); 51 | assertThat(copy.getName()).isEqualTo(original.getName()); 52 | 53 | } 54 | 55 | @Test 56 | void testMarshalUnmarshalJson() throws Exception { 57 | 58 | // PREPARE 59 | final CreatePersonCommand original = createTestee(); 60 | 61 | // TEST 62 | final String json = mapper.writer().writeValueAsString(original); 63 | final CreatePersonCommand copy = mapper.reader().readValue(json, CreatePersonCommand.class); 64 | 65 | // VERIFY 66 | assertThat(copy).isEqualTo(original); 67 | assertThat(copy.getAggregateRootId()).isEqualTo(original.getAggregateRootId()); 68 | assertThat(copy.getName()).isEqualTo(original.getName()); 69 | 70 | } 71 | 72 | @Test 73 | void testUnmarshalJsonFromFile() throws Exception { 74 | 75 | // PREPARE 76 | final String json = """ 77 | { 78 | "event-id": "109a77b2-1de2-46fc-aee1-97fa7740a552", 79 | "event-timestamp": "2019-11-17T10:27:13.183+01:00[Europe/Berlin]", 80 | "entity-id-path": "PERSON 84565d62-115e-4502-b7c9-38ad69c64b05", 81 | "name": "Peter Parker" 82 | } 83 | """; 84 | 85 | // TEST 86 | final CreatePersonCommand copy = mapper.reader().readValue(json, CreatePersonCommand.class); 87 | 88 | // VERIFY 89 | assertThat(copy.getEventId().asBaseType()).isEqualTo(UUID.fromString("109a77b2-1de2-46fc-aee1-97fa7740a552")); 90 | assertThat(copy.getEventTimestamp()).isEqualTo(ZonedDateTime.parse("2019-11-17T10:27:13.183+01:00[Europe/Berlin]")); 91 | assertThat(copy.getAggregateRootId().asString()).isEqualTo(PERSON_UUID); 92 | assertThat(copy.getName().asString()).isEqualTo("Peter Parker"); 93 | 94 | } 95 | 96 | private CreatePersonCommand createTestee() { 97 | final org.fuin.cqrs4j.example.spring.shared.PersonId personId = new PersonId(UUID.fromString(PERSON_UUID)); 98 | final org.fuin.cqrs4j.example.spring.shared.PersonName personName = new PersonName("Peter Parker"); 99 | return new CreatePersonCommand.Builder().id(personId).name(personName).build(); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /quarkus/shared/src/test/java/org/fuin/cqrs4j/example/quarkus/shared/PersonCreatedEventTest.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.shared; 2 | 3 | import jakarta.json.bind.Jsonb; 4 | import jakarta.json.bind.JsonbBuilder; 5 | import jakarta.json.bind.JsonbConfig; 6 | import org.apache.commons.io.IOUtils; 7 | import org.eclipse.yasson.FieldAccessStrategy; 8 | import org.fuin.cqrs4j.example.shared.PersonCreatedEvent; 9 | import org.fuin.cqrs4j.example.shared.PersonId; 10 | import org.fuin.cqrs4j.example.shared.PersonName; 11 | import org.fuin.cqrs4j.example.shared.SharedUtils; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import java.nio.charset.StandardCharsets; 15 | import java.util.Objects; 16 | import java.util.UUID; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.fuin.utils4j.Utils4J.deserialize; 20 | import static org.fuin.utils4j.Utils4J.serialize; 21 | 22 | // CHECKSTYLE:OFF 23 | public final class PersonCreatedEventTest { 24 | 25 | @Test 26 | public final void testSerializeDeserialize() { 27 | 28 | // PREPARE 29 | final org.fuin.cqrs4j.example.shared.PersonCreatedEvent original = createTestee(); 30 | 31 | // TEST 32 | final org.fuin.cqrs4j.example.shared.PersonCreatedEvent copy = deserialize(serialize(original)); 33 | 34 | // VERIFY 35 | assertThat(copy).isEqualTo(original); 36 | assertThat(copy.getName()).isEqualTo(original.getName()); 37 | 38 | } 39 | 40 | @Test 41 | public final void testMarshalUnmarshalJson() throws Exception { 42 | 43 | // PREPARE 44 | final org.fuin.cqrs4j.example.shared.PersonCreatedEvent original = createTestee(); 45 | 46 | final JsonbConfig config = new JsonbConfig().withAdapters(org.fuin.cqrs4j.example.shared.SharedUtils.getJsonbAdapters()) 47 | .withPropertyVisibilityStrategy(new FieldAccessStrategy()); 48 | try (final Jsonb jsonb = JsonbBuilder.create(config)) { 49 | 50 | // TEST 51 | final String json = jsonb.toJson(original, org.fuin.cqrs4j.example.shared.PersonCreatedEvent.class); 52 | final org.fuin.cqrs4j.example.shared.PersonCreatedEvent copy = jsonb.fromJson(json, org.fuin.cqrs4j.example.shared.PersonCreatedEvent.class); 53 | 54 | // VERIFY 55 | assertThat(copy).isEqualTo(original); 56 | assertThat(copy.getName()).isEqualTo(original.getName()); 57 | 58 | } 59 | } 60 | 61 | @Test 62 | public final void testUnmarshalJson() throws Exception { 63 | 64 | // PREPARE 65 | final org.fuin.cqrs4j.example.shared.PersonCreatedEvent original = createTestee(); 66 | final JsonbConfig config = new JsonbConfig().withAdapters(SharedUtils.getJsonbAdapters()) 67 | .withPropertyVisibilityStrategy(new FieldAccessStrategy()); 68 | try (final Jsonb jsonb = JsonbBuilder.create(config)) { 69 | 70 | // TEST 71 | final String json = IOUtils.toString(Objects.requireNonNull(this.getClass().getResourceAsStream("/events/PersonCreatedEvent.json")), 72 | StandardCharsets.UTF_8); 73 | final org.fuin.cqrs4j.example.shared.PersonCreatedEvent copy = jsonb.fromJson(json, org.fuin.cqrs4j.example.shared.PersonCreatedEvent.class); 74 | 75 | // VERIFY 76 | assertThat(copy.getEntityIdPath()).isEqualTo(original.getEntityIdPath()); 77 | assertThat(copy.getName()).isEqualTo(original.getName()); 78 | } 79 | 80 | } 81 | 82 | @Test 83 | public final void testToString() { 84 | final org.fuin.cqrs4j.example.shared.PersonCreatedEvent testee = createTestee(); 85 | assertThat(testee) 86 | .hasToString("Person 'Peter Parker' (" + testee.getEntityId() + ") was created [Event " + testee.getEventId() + "]"); 87 | } 88 | 89 | private org.fuin.cqrs4j.example.shared.PersonCreatedEvent createTestee() { 90 | final org.fuin.cqrs4j.example.shared.PersonId personId = new PersonId(UUID.fromString("f645969a-402d-41a9-882b-d2d8000d0f43")); 91 | final org.fuin.cqrs4j.example.shared.PersonName personName = new PersonName("Peter Parker"); 92 | return new PersonCreatedEvent.Builder().id(personId).name(personName).version(0).build(); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /quarkus/shared/src/test/java/org/fuin/cqrs4j/example/quarkus/shared/CreatePersonCommandTest.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.shared; 2 | 3 | import jakarta.json.bind.Jsonb; 4 | import jakarta.json.bind.JsonbBuilder; 5 | import jakarta.json.bind.JsonbConfig; 6 | import org.apache.commons.io.IOUtils; 7 | import org.eclipse.yasson.FieldAccessStrategy; 8 | import org.fuin.cqrs4j.example.shared.CreatePersonCommand; 9 | import org.fuin.cqrs4j.example.shared.PersonId; 10 | import org.fuin.cqrs4j.example.shared.PersonName; 11 | import org.fuin.cqrs4j.example.shared.SharedUtils; 12 | import org.fuin.utils4j.Utils4J; 13 | import org.junit.jupiter.api.Test; 14 | 15 | import java.nio.charset.StandardCharsets; 16 | import java.time.ZonedDateTime; 17 | import java.util.Objects; 18 | import java.util.UUID; 19 | 20 | import static org.assertj.core.api.Assertions.assertThat; 21 | 22 | // CHECKSTYLE:OFF 23 | public final class CreatePersonCommandTest { 24 | 25 | private static final String PERSON_UUID = "84565d62-115e-4502-b7c9-38ad69c64b05"; 26 | 27 | @Test 28 | public final void testSerializeDeserialize() { 29 | 30 | // PREPARE 31 | final org.fuin.cqrs4j.example.shared.CreatePersonCommand original = createTestee(); 32 | 33 | // TEST 34 | final org.fuin.cqrs4j.example.shared.CreatePersonCommand copy = Utils4J.deserialize(Utils4J.serialize(original)); 35 | 36 | // VERIFY 37 | assertThat(copy).isEqualTo(original); 38 | assertThat(copy.getAggregateRootId()).isEqualTo(original.getAggregateRootId()); 39 | assertThat(copy.getName()).isEqualTo(original.getName()); 40 | 41 | } 42 | 43 | @Test 44 | public final void testMarshalUnmarshalJson() throws Exception { 45 | 46 | // PREPARE 47 | final org.fuin.cqrs4j.example.shared.CreatePersonCommand original = createTestee(); 48 | 49 | final JsonbConfig config = new JsonbConfig().withAdapters(org.fuin.cqrs4j.example.shared.SharedUtils.getJsonbAdapters()) 50 | .withPropertyVisibilityStrategy(new FieldAccessStrategy()); 51 | try (final Jsonb jsonb = JsonbBuilder.create(config)) { 52 | 53 | // TEST 54 | final String json = jsonb.toJson(original, org.fuin.cqrs4j.example.shared.CreatePersonCommand.class); 55 | final org.fuin.cqrs4j.example.shared.CreatePersonCommand copy = jsonb.fromJson(json, org.fuin.cqrs4j.example.shared.CreatePersonCommand.class); 56 | 57 | // VERIFY 58 | assertThat(copy).isEqualTo(original); 59 | assertThat(copy.getAggregateRootId()).isEqualTo(original.getAggregateRootId()); 60 | assertThat(copy.getName()).isEqualTo(original.getName()); 61 | 62 | } 63 | 64 | } 65 | 66 | @Test 67 | public final void testUnmarshalJsonFromFile() throws Exception { 68 | 69 | // PREPARE 70 | final String json = IOUtils.toString(Objects.requireNonNull(this.getClass().getResourceAsStream("/commands/CreatePersonCommand.json")), 71 | StandardCharsets.UTF_8); 72 | final JsonbConfig config = new JsonbConfig().withAdapters(SharedUtils.getJsonbAdapters()) 73 | .withPropertyVisibilityStrategy(new FieldAccessStrategy()); 74 | try (final Jsonb jsonb = JsonbBuilder.create(config)) { 75 | 76 | // TEST 77 | final org.fuin.cqrs4j.example.shared.CreatePersonCommand copy = jsonb.fromJson(json, org.fuin.cqrs4j.example.shared.CreatePersonCommand.class); 78 | 79 | // VERIFY 80 | assertThat(copy.getEventId().asBaseType()).isEqualTo(UUID.fromString("109a77b2-1de2-46fc-aee1-97fa7740a552")); 81 | assertThat(copy.getEventTimestamp()).isEqualTo(ZonedDateTime.parse("2019-11-17T10:27:13.183+01:00[Europe/Berlin]")); 82 | assertThat(copy.getAggregateRootId().asString()).isEqualTo(PERSON_UUID); 83 | assertThat(copy.getName().asString()).isEqualTo("Peter Parker"); 84 | 85 | } 86 | 87 | } 88 | 89 | private org.fuin.cqrs4j.example.shared.CreatePersonCommand createTestee() { 90 | final org.fuin.cqrs4j.example.shared.PersonId personId = new PersonId(UUID.fromString(PERSON_UUID)); 91 | final org.fuin.cqrs4j.example.shared.PersonName personName = new PersonName("Peter Parker"); 92 | return new CreatePersonCommand.Builder().id(personId).name(personName).build(); 93 | } 94 | 95 | } 96 | // CHECKSTYLE:ON 97 | -------------------------------------------------------------------------------- /quarkus/shared/src/test/java/org/fuin/cqrs4j/example/quarkus/shared/PersonDeletedEventTest.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.shared; 2 | 3 | import jakarta.json.bind.Jsonb; 4 | import jakarta.json.bind.JsonbBuilder; 5 | import jakarta.json.bind.JsonbConfig; 6 | import org.apache.commons.io.IOUtils; 7 | import org.eclipse.yasson.FieldAccessStrategy; 8 | import org.fuin.cqrs4j.example.shared.PersonDeletedEvent; 9 | import org.fuin.cqrs4j.example.shared.PersonId; 10 | import org.fuin.cqrs4j.example.shared.PersonName; 11 | import org.fuin.cqrs4j.example.shared.SharedUtils; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import java.nio.charset.StandardCharsets; 15 | import java.util.Objects; 16 | import java.util.UUID; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.fuin.utils4j.Utils4J.deserialize; 20 | import static org.fuin.utils4j.Utils4J.serialize; 21 | 22 | // CHECKSTYLE:OFF 23 | public final class PersonDeletedEventTest { 24 | 25 | @Test 26 | public final void testSerializeDeserialize() { 27 | 28 | // PREPARE 29 | final org.fuin.cqrs4j.example.shared.PersonDeletedEvent original = createTestee(); 30 | 31 | // TEST 32 | final org.fuin.cqrs4j.example.shared.PersonDeletedEvent copy = deserialize(serialize(original)); 33 | 34 | // VERIFY 35 | assertThat(copy).isEqualTo(original); 36 | assertThat(copy.getName()).isEqualTo(original.getName()); 37 | 38 | } 39 | 40 | @Test 41 | public final void testMarshalUnmarshalJson() throws Exception { 42 | 43 | // PREPARE 44 | final org.fuin.cqrs4j.example.shared.PersonDeletedEvent original = createTestee(); 45 | 46 | final JsonbConfig config = new JsonbConfig().withAdapters(org.fuin.cqrs4j.example.shared.SharedUtils.getJsonbAdapters()) 47 | .withPropertyVisibilityStrategy(new FieldAccessStrategy()); 48 | try (final Jsonb jsonb = JsonbBuilder.create(config)) { 49 | 50 | // TEST 51 | final String json = jsonb.toJson(original, org.fuin.cqrs4j.example.shared.PersonDeletedEvent.class); 52 | final org.fuin.cqrs4j.example.shared.PersonDeletedEvent copy = jsonb.fromJson(json, org.fuin.cqrs4j.example.shared.PersonDeletedEvent.class); 53 | 54 | // VERIFY 55 | assertThat(copy).isEqualTo(original); 56 | assertThat(copy.getName()).isEqualTo(original.getName()); 57 | assertThat(copy.getAggregateVersionInteger()).isEqualTo(1L); 58 | 59 | } 60 | } 61 | 62 | @Test 63 | public final void testUnmarshalJson() throws Exception { 64 | 65 | // PREPARE 66 | final org.fuin.cqrs4j.example.shared.PersonDeletedEvent original = createTestee(); 67 | final JsonbConfig config = new JsonbConfig().withAdapters(SharedUtils.getJsonbAdapters()) 68 | .withPropertyVisibilityStrategy(new FieldAccessStrategy()); 69 | try (final Jsonb jsonb = JsonbBuilder.create(config)) { 70 | 71 | // TEST 72 | final String json = IOUtils.toString(Objects.requireNonNull(this.getClass().getResourceAsStream("/events/PersonDeletedEvent.json")), 73 | StandardCharsets.UTF_8); 74 | final org.fuin.cqrs4j.example.shared.PersonDeletedEvent copy = jsonb.fromJson(json, org.fuin.cqrs4j.example.shared.PersonDeletedEvent.class); 75 | 76 | // VERIFY 77 | assertThat(copy.getEntityIdPath()).isEqualTo(original.getEntityIdPath()); 78 | assertThat(copy.getName()).isEqualTo(original.getName()); 79 | assertThat(copy.getAggregateVersionInteger()).isEqualTo(1L); 80 | 81 | } 82 | 83 | } 84 | 85 | @Test 86 | public final void testToString() { 87 | final org.fuin.cqrs4j.example.shared.PersonDeletedEvent testee = createTestee(); 88 | assertThat(testee) 89 | .hasToString("Deleted person 'Peter Parker' (" + testee.getEntityId() + ") [Event " + testee.getEventId() + "]"); 90 | } 91 | 92 | private org.fuin.cqrs4j.example.shared.PersonDeletedEvent createTestee() { 93 | final org.fuin.cqrs4j.example.shared.PersonId personId = new PersonId(UUID.fromString("f645969a-402d-41a9-882b-d2d8000d0f43")); 94 | final org.fuin.cqrs4j.example.shared.PersonName personName = new PersonName("Peter Parker"); 95 | return new PersonDeletedEvent.Builder().id(personId).name(personName).version(1).build(); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /quarkus/command/src/main/java/org/fuin/cqrs4j/example/quarkus/command/api/PersonResource.java: -------------------------------------------------------------------------------- 1 | package org.fuin.cqrs4j.example.quarkus.command.api; 2 | 3 | import jakarta.inject.Inject; 4 | import jakarta.validation.ConstraintViolation; 5 | import jakarta.validation.ConstraintViolationException; 6 | import jakarta.validation.Validator; 7 | import jakarta.ws.rs.Consumes; 8 | import jakarta.ws.rs.DELETE; 9 | import jakarta.ws.rs.POST; 10 | import jakarta.ws.rs.Path; 11 | import jakarta.ws.rs.PathParam; 12 | import jakarta.ws.rs.Produces; 13 | import jakarta.ws.rs.core.Context; 14 | import jakarta.ws.rs.core.MediaType; 15 | import jakarta.ws.rs.core.Response; 16 | import jakarta.ws.rs.core.UriInfo; 17 | import org.fuin.cqrs4j.core.CommandExecutionFailedException; 18 | import org.fuin.cqrs4j.jsonb.SimpleResult; 19 | import org.fuin.cqrs4j.example.aggregates.DuplicatePersonNameException; 20 | import org.fuin.cqrs4j.example.aggregates.Person; 21 | import org.fuin.cqrs4j.example.aggregates.PersonRepository; 22 | import org.fuin.cqrs4j.example.shared.CreatePersonCommand; 23 | import org.fuin.cqrs4j.example.shared.DeletePersonCommand; 24 | import org.fuin.ddd4j.core.AggregateAlreadyExistsException; 25 | import org.fuin.ddd4j.core.AggregateDeletedException; 26 | import org.fuin.ddd4j.core.AggregateNotFoundException; 27 | import org.fuin.ddd4j.core.AggregateVersionConflictException; 28 | import org.fuin.ddd4j.core.AggregateVersionNotFoundException; 29 | 30 | import java.util.Optional; 31 | import java.util.Set; 32 | import java.util.UUID; 33 | 34 | import java.util.Optional; 35 | import java.util.Set; 36 | 37 | @Path("/persons") 38 | public class PersonResource { 39 | 40 | @Inject 41 | PersonRepository repo; 42 | 43 | @Inject 44 | Validator validator; 45 | 46 | @Context 47 | UriInfo uriInfo; 48 | 49 | @POST 50 | @Consumes(MediaType.APPLICATION_JSON) 51 | @Produces(MediaType.APPLICATION_JSON) 52 | @Path("/create") 53 | public Response create(final CreatePersonCommand cmd) 54 | throws AggregateAlreadyExistsException, AggregateDeletedException, CommandExecutionFailedException { 55 | 56 | // Verify preconditions 57 | final Set> violations = validator.validate(cmd); 58 | if (!violations.isEmpty()) { 59 | throw new ConstraintViolationException(violations); 60 | } 61 | 62 | try { 63 | // Create aggregate 64 | final Person person = new Person(cmd.getAggregateRootId(), cmd.getName(), name -> { 65 | // TODO Execute a call to the query side to verify if the name already exists 66 | return Optional.empty(); 67 | }); 68 | repo.add(person); 69 | 70 | // Send OK response 71 | return Response.ok(SimpleResult.ok()).build(); 72 | } catch (final DuplicatePersonNameException ex) { 73 | throw new CommandExecutionFailedException(ex); 74 | } 75 | 76 | } 77 | 78 | @DELETE 79 | @Consumes(MediaType.APPLICATION_JSON) 80 | @Produces(MediaType.APPLICATION_JSON) 81 | @Path("/{personId}") 82 | public Response delete(@PathParam("personId") final UUID personId, final DeletePersonCommand cmd) 83 | throws AggregateVersionConflictException, AggregateVersionNotFoundException, 84 | AggregateDeletedException, AggregateNotFoundException { 85 | 86 | // Verify preconditions 87 | final Set> violations = validator.validate(cmd); 88 | if (!violations.isEmpty()) { 89 | throw new ConstraintViolationException(violations); 90 | } 91 | 92 | // Read last known entity version 93 | final Person person = repo.read(cmd.getAggregateRootId(), cmd.getAggregateVersionInteger()); 94 | 95 | // Try to delete the aggregate 96 | // Internally just sets a 'deleted' flag 97 | person.delete(); 98 | 99 | // Write resulting events back to the repository 100 | // DO NOT call "repo.delete(..)! If you would do, you would never see a "deleted" event... 101 | // The repository "delete" really removes the stream and is more like a "purge". 102 | repo.update(person); 103 | 104 | // Send OK response 105 | return Response.ok(SimpleResult.ok()).build(); 106 | 107 | } 108 | 109 | } 110 | --------------------------------------------------------------------------------