Skip to content

Commit f962991

Browse files
committed
Split event sourcing and application code. Fix #8
1 parent 9fd8581 commit f962991

File tree

87 files changed

+747
-457
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+747
-457
lines changed

README.md

+56-22
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,17 @@
2525
- [Database polling alternative](#4-7-3)
2626
- [Adding new asynchronous event handlers](#4-8)
2727
- [Drawbacks](#4-9)
28-
- [Class diagrams](#4-10)
29-
- [Class diagram of the domain model](#4-10-1)
30-
- [Class diagram of the projections](#4-10-2)
31-
- [Class diagram of the service layer](#4-10-3)
32-
- [How to run the sample?](#5)
33-
34-
<!-- Table of contents is made with https://github.com/evgeniy-khist/markdown-toc -->
28+
- [Project structure](#5)
29+
- [Gradle subprojects](#5-1)
30+
- [Database schema migrations](#5-2)
31+
- [Class diagrams](#5-3)
32+
- [Class diagram of the domain model](#5-3-1)
33+
- [Class diagram of the projections](#5-3-2)
34+
- [Class diagram of the service layer](#5-3-3)
35+
- [How to adapt it to your domain?](#6)
36+
- [How to run the sample?](#7)
37+
38+
<!-- Table of contents is made with https://github.com/eugene-khyst/md-toc-cli -->
3539

3640
## <a id="1"></a>Introduction
3741

@@ -62,16 +66,17 @@ But PostgreSQL, the world's most advanced open-source database, is also suitable
6266
You can use PostgreSQL as an event store without additional frameworks or extensions
6367
instead of setting up and maintaining a separate specialized database for event sourcing.
6468

65-
This repository provides a reference implementation of an event-sourced system that uses PostgreSQL as an event store.
66-
You can also [fork](https://github.com/evgeniy-khist/postgresql-event-sourcing/fork) the repo
67-
and use it as a template for your projects.
69+
This repository provides a reference implementation of an event-sourced system
70+
that uses PostgreSQL as an event store built with Spring Boot.
71+
[Fork](https://github.com/eugene-khyst/postgresql-event-sourcing/fork) the repository and use it as a template for your projects.
72+
Or clone the repository and run end-to-end tests to see how everything works together.
6873

6974
![PostgreSQL Logo](img/potgresql-logo.png)
7075

7176
See also
7277

73-
* [Event Sourcing with EventStoreDB](https://github.com/evgeniy-khist/eventstoredb-event-sourcing)
74-
* [Event Sourcing with Kafka and ksqlDB](https://github.com/evgeniy-khist/ksqldb-event-souring)
78+
* [Event Sourcing with EventStoreDB](https://github.com/eugene-khyst/eventstoredb-event-sourcing)
79+
* [Event Sourcing with Kafka and ksqlDB](https://github.com/eugene-khyst/ksqldb-event-souring)
7580

7681
## <a id="2"></a>Example domain
7782

@@ -483,7 +488,7 @@ This mechanism is used by default as more efficient.
483488
After restarting the backend, existing subscriptions will only process new events after the last processed event
484489
and not everything from the first one.
485490
486-
> [!WARNING]
491+
> **WARNING**
487492
> Critical content demanding immediate user attention due to potential risks.
488493
New subscriptions (event handlers) in the first poll will read and process all events.
489494
Be careful, if there are too many events, they may take a long time to process.
@@ -508,43 +513,72 @@ Using PostgreSQL as an event store has a lot of advantages, but there are also d
508513
and events created by all later transactions will be read by the event subscription processor
509514
only after this long-running transaction is committed.
510515
511-
### <a id="4-10"></a>Class diagrams
516+
## <a id="5"></a>Project structure
517+
518+
### <a id="5-1"></a>Gradle subprojects
512519
513520
This reference implementation can be easily extended to comply with your domain model.
514521
515-
#### <a id="4-10-1"></a>Class diagram of the domain model
522+
Event sourcing related code and application specific code are located in separate Gradle subprojects:
523+
* [`postgresql-event-sourcing-core`](postgresql-event-sourcing-core): event sourcing and PostgreSQL related code, a shared library, `eventsourcing.postgresql` package,
524+
* [`event-sourcing-app`](event-sourcing-app): application specific code, a simplified ride-hailing sample, `com.example.eventsourcing` package.
525+
526+
`event-sourcing-app` depends on `postgresql-event-sourcing-core`:
527+
```groovy
528+
dependencies {
529+
implementation project(':postgresql-event-sourcing-core')
530+
}
531+
```
532+
533+
### <a id="5-2"></a>Database schema migrations
534+
535+
Event sourcing related database schema migrations:
536+
* [V1__eventsourcing_tables.sql](event-sourcing-app/src/main/resources/db/migration/V1__eventsourcing_tables.sql)
537+
* [V2__notify_trigger.sql](event-sourcing-app/src/main/resources/db/migration/V2__notify_trigger.sql)
538+
539+
Application specific projections database schema migration:
540+
* [V3__projection_tables.sql](event-sourcing-app/src/main/resources/db/migration/V3__projection_tables.sql)
541+
542+
### <a id="5-3"></a>Class diagrams
543+
544+
#### <a id="5-3-1"></a>Class diagram of the domain model
516545
517546
![Class diagram of the domain model](img/class-domain.svg)
518547
519-
#### <a id="4-10-2"></a>Class diagram of the projections
548+
#### <a id="5-3-2"></a>Class diagram of the projections
520549
521550
![Class diagram of the projections](img/class-projection.svg)
522551
523-
#### <a id="4-10-3"></a>Class diagram of the service layer
552+
#### <a id="5-3-3"></a>Class diagram of the service layer
524553
525554
![Class diagram of the service layer](img/class-service.svg)
526555
527-
## <a id="5"></a>How to run the sample?
556+
## <a id="6"></a>How to adapt it to your domain?
557+
558+
To adapt this sample to your domain model, make changes to `event-sourcing-app` subproject.
559+
No changes to `postgresql-event-sourcing-core` subproject are required.
560+
561+
## <a id="7"></a>How to run the sample?
528562
529563
1. Download & install [SDKMAN!](https://sdkman.io/install).
530564
531-
2. Install JDK 17
565+
2. Install JDK 21
532566
```bash
533567
sdk list java
534-
sdk install java 17.0.x-tem
568+
sdk install java 21-tem
535569
```
536570
537571
3. Install [Docker](https://docs.docker.com/engine/install/)
538572
and [Docker Compose](https://docs.docker.com/compose/install/).
539573
540574
4. Build Java project and Docker image
541575
```bash
542-
./gradlew clean build bootBuildImage -i
576+
./gradlew clean build jibDockerBuild -i
543577
```
544578
545579
5. Run PostgreSQL, Kafka and event-sourcing-app
546580
```bash
547-
docker compose up -d --scale event-sourcing-app=2
581+
docker compose --env-file gradle.properties up -d --scale event-sourcing-app=2
548582
# wait a few minutes
549583
```
550584

build.gradle

+10-55
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,16 @@
1-
plugins {
2-
id 'java'
3-
id 'org.springframework.boot' version '3.0.4'
4-
id 'io.spring.dependency-management' version '1.1.0'
1+
allprojects {
2+
group = 'com.example'
3+
version = project.properties['project_version']
54
}
65

7-
group = 'com.example'
8-
version = '2.0.0'
9-
sourceCompatibility = '17'
6+
subprojects {
7+
apply plugin: 'java'
108

11-
repositories {
12-
mavenCentral()
13-
}
14-
15-
configurations {
16-
compileOnly {
17-
extendsFrom annotationProcessor
9+
java {
10+
sourceCompatibility = '21'
1811
}
19-
}
20-
21-
ext {
22-
set('postgresql_version', '42.5.4')
23-
set('mapstruct_version', '1.5.3.Final')
24-
set('lombok_mapstruct_binding_version', '0.2.0')
25-
set('testcontainers_version', '1.17.6')
26-
}
27-
28-
dependencies {
29-
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
30-
implementation 'org.springframework.boot:spring-boot-starter-web'
31-
implementation 'org.springframework.boot:spring-boot-starter-validation'
32-
implementation 'org.springframework.kafka:spring-kafka'
33-
34-
implementation "org.postgresql:postgresql:${postgresql_version}"
35-
implementation "org.mapstruct:mapstruct:${mapstruct_version}"
3612

37-
implementation 'org.flywaydb:flyway-core'
38-
39-
compileOnly 'org.projectlombok:lombok'
40-
annotationProcessor 'org.projectlombok:lombok'
41-
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:${lombok_mapstruct_binding_version}"
42-
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstruct_version}"
43-
44-
testCompileOnly 'org.projectlombok:lombok'
45-
testAnnotationProcessor 'org.projectlombok:lombok'
46-
testImplementation 'org.springframework.boot:spring-boot-starter-test'
47-
testImplementation 'org.springframework.kafka:spring-kafka-test'
48-
testImplementation 'org.testcontainers:junit-jupiter'
49-
testImplementation 'org.testcontainers:postgresql'
50-
testImplementation 'org.testcontainers:kafka'
51-
}
52-
53-
dependencyManagement {
54-
imports {
55-
mavenBom "org.testcontainers:testcontainers-bom:${testcontainers_version}"
13+
repositories {
14+
mavenCentral()
5615
}
57-
}
58-
59-
test {
60-
useJUnitPlatform()
61-
}
16+
}

docker-compose.yml

+7-7
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ services:
3030
condition: service_healthy
3131

3232
kafka:
33-
image: confluentinc/cp-kafka:7.3.2
33+
image: confluentinc/cp-kafka:7.5.1
3434
environment:
35-
KAFKA_BROKER_ID: 1
35+
KAFKA_NODE_ID: 1
3636
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'
3737
KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092'
3838
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
@@ -42,19 +42,19 @@ services:
4242
KAFKA_JMX_PORT: 9101
4343
KAFKA_JMX_HOSTNAME: localhost
4444
KAFKA_PROCESS_ROLES: 'broker,controller'
45-
KAFKA_NODE_ID: 1
4645
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093'
4746
KAFKA_LISTENERS: 'PLAINTEXT://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092'
4847
KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'
4948
KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
5049
KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'
50+
# Replace CLUSTER_ID with a unique base64 UUID using "bin/kafka-storage.sh random-uuid"
51+
# See https://docs.confluent.io/kafka/operations-tools/kafka-tools.html#kafka-storage-sh
52+
CLUSTER_ID: 'MkU3OEVBNTcwNTJENDM2Qk'
5153
ports:
5254
- "9092:9092"
5355
- "9101:9101"
5456
volumes:
5557
- kafka-data:/var/lib/kafka/data
56-
- ./kafka/update_run.sh:/tmp/update_run.sh
57-
command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'"
5858
healthcheck:
5959
test: [ "CMD", "nc", "-z", "localhost", "9092" ]
6060
start_period: 15s
@@ -63,14 +63,14 @@ services:
6363
retries: 5
6464

6565
nginx:
66-
image: nginx:1.23-alpine
66+
image: nginx:1.25-alpine
6767
ports:
6868
- "8080:80"
6969
volumes:
7070
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
7171

7272
event-sourcing-app:
73-
image: evgeniy-khyst/postgresql-event-sourcing:2.0.0
73+
image: eugene-khyst/postgresql-event-sourcing:${project_version}
7474
environment:
7575
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/postgres
7676
SPRING_DATASOURCE_USERNAME: admin

event-sourcing-app/build.gradle

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
plugins {
2+
id 'org.springframework.boot' version "${spring_boot_version}"
3+
id 'io.spring.dependency-management' version "${spring_dependency_management_version}"
4+
id 'com.google.cloud.tools.jib' version "${jib_version}"
5+
}
6+
7+
configurations {
8+
compileOnly {
9+
extendsFrom annotationProcessor
10+
}
11+
}
12+
13+
dependencyManagement {
14+
imports {
15+
mavenBom "org.testcontainers:testcontainers-bom:${testcontainers_version}"
16+
}
17+
}
18+
19+
dependencies {
20+
implementation project(':postgresql-event-sourcing-core')
21+
22+
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
23+
implementation 'org.springframework.boot:spring-boot-starter-web'
24+
implementation 'org.springframework.kafka:spring-kafka'
25+
26+
implementation 'org.flywaydb:flyway-core'
27+
implementation "org.mapstruct:mapstruct:${mapstruct_version}"
28+
29+
compileOnly 'org.projectlombok:lombok'
30+
annotationProcessor 'org.projectlombok:lombok'
31+
32+
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstruct_version}"
33+
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:${lombok_mapstruct_binding_version}"
34+
35+
testImplementation 'org.springframework.boot:spring-boot-starter-test'
36+
testImplementation 'org.springframework.kafka:spring-kafka-test'
37+
testImplementation 'org.testcontainers:junit-jupiter'
38+
testImplementation 'org.testcontainers:postgresql'
39+
testImplementation 'org.testcontainers:kafka'
40+
41+
testCompileOnly 'org.projectlombok:lombok'
42+
testAnnotationProcessor 'org.projectlombok:lombok'
43+
}
44+
45+
tasks.named('test') {
46+
useJUnitPlatform()
47+
}
48+
49+
jib {
50+
to.image = "eugene-khyst/postgresql-event-sourcing:${project.version}"
51+
from.image = "eclipse-temurin:21-jre"
52+
}

src/main/java/com/example/eventsourcing/PostgresEventSourcingApplication.java renamed to event-sourcing-app/src/main/java/com/example/eventsourcing/PostgresEventSourcingApplication.java

+7
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.boot.autoconfigure.domain.EntityScan;
6+
import org.springframework.context.annotation.ComponentScan;
7+
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
58
import org.springframework.scheduling.annotation.EnableAsync;
69
import org.springframework.scheduling.annotation.EnableScheduling;
710

811
@SpringBootApplication
12+
@ComponentScan("eventsourcing.postgresql")
13+
@ComponentScan
14+
@EntityScan
15+
@EnableJpaRepositories
916
@EnableScheduling
1017
@EnableAsync
1118
public class PostgresEventSourcingApplication {

src/main/java/com/example/eventsourcing/controller/OrdersController.java renamed to event-sourcing-app/src/main/java/com/example/eventsourcing/controller/OrdersController.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package com.example.eventsourcing.controller;
22

33
import com.example.eventsourcing.domain.command.*;
4-
import com.example.eventsourcing.projection.OrderProjection;
54
import com.example.eventsourcing.dto.OrderStatus;
5+
import com.example.eventsourcing.projection.OrderProjection;
66
import com.example.eventsourcing.repository.OrderProjectionRepository;
7-
import com.example.eventsourcing.service.CommandProcessor;
87
import com.fasterxml.jackson.core.type.TypeReference;
98
import com.fasterxml.jackson.databind.JsonNode;
109
import com.fasterxml.jackson.databind.ObjectMapper;
10+
import eventsourcing.postgresql.service.CommandProcessor;
1111
import lombok.RequiredArgsConstructor;
1212
import org.springframework.http.ResponseEntity;
1313
import org.springframework.web.bind.annotation.*;

src/main/java/com/example/eventsourcing/controller/RestResponseEntityExceptionHandler.java renamed to event-sourcing-app/src/main/java/com/example/eventsourcing/controller/RestResponseEntityExceptionHandler.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package com.example.eventsourcing.controller;
22

3-
import com.example.eventsourcing.error.Error;
43
import com.fasterxml.jackson.databind.JsonNode;
54
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import eventsourcing.postgresql.error.AggregateStateException;
66
import lombok.RequiredArgsConstructor;
77
import org.springframework.http.ResponseEntity;
88
import org.springframework.web.bind.annotation.ControllerAdvice;
@@ -15,9 +15,10 @@ public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionH
1515

1616
private final ObjectMapper objectMapper;
1717

18-
@ExceptionHandler(value = {Error.class})
19-
protected ResponseEntity<JsonNode> handleError(Error error) {
18+
@ExceptionHandler(value = {AggregateStateException.class})
19+
protected ResponseEntity<JsonNode> handleException(AggregateStateException aggregateStateException) {
2020
return ResponseEntity.badRequest()
21-
.body(objectMapper.createObjectNode().put("error", error.getMessage()));
21+
.body(objectMapper.createObjectNode()
22+
.put("error", aggregateStateException.getMessage()));
2223
}
2324
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.example.eventsourcing.domain;
2+
3+
import eventsourcing.postgresql.domain.Aggregate;
4+
import lombok.AccessLevel;
5+
import lombok.Getter;
6+
import lombok.RequiredArgsConstructor;
7+
8+
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
9+
@Getter
10+
public enum AggregateType {
11+
12+
ORDER(OrderAggregate.class);
13+
14+
private final Class<? extends Aggregate> aggregateClass;
15+
}

0 commit comments

Comments
 (0)