├── README.adoc
├── SagaPattern.png
├── SagaPatternArchitecture.png
├── allocate
├── .dockerignore
├── .gitignore
├── README.adoc
├── pom.xml
└── src
│ ├── main
│ ├── docker
│ │ ├── Dockerfile.jvm
│ │ ├── Dockerfile.legacy-jar
│ │ ├── Dockerfile.native
│ │ └── Dockerfile.native-distroless
│ ├── java
│ │ └── com
│ │ │ └── refactorizando
│ │ │ └── sample
│ │ │ └── saga
│ │ │ ├── AllocateApplication.java
│ │ │ ├── AllocateResource.java
│ │ │ ├── events
│ │ │ ├── AllocateConsumer.java
│ │ │ ├── AllocateProducer.java
│ │ │ └── compensation
│ │ │ │ └── PaymentProducerCompensation.java
│ │ │ ├── model
│ │ │ ├── Payment.java
│ │ │ ├── Seat.java
│ │ │ └── User.java
│ │ │ ├── repository
│ │ │ ├── PaymentRepository.java
│ │ │ ├── SeatRepository.java
│ │ │ └── UserRepository.java
│ │ │ ├── service
│ │ │ ├── PaymentService.java
│ │ │ └── SeatService.java
│ │ │ └── usecase
│ │ │ └── AllocateUseCase.java
│ └── resources
│ │ ├── application.properties
│ │ └── load-data.sql
│ └── test
│ └── java
│ └── com
│ └── refactorizando
│ └── sample
│ └── saga
│ ├── NativeOrderSeatResourceIT.java
│ └── OrderSeatResourceTest.java
├── order
├── .dockerignore
├── .gitignore
├── README.adoc
├── pom.xml
└── src
│ ├── main
│ ├── docker
│ │ ├── Dockerfile.jvm
│ │ ├── Dockerfile.legacy-jar
│ │ ├── Dockerfile.native
│ │ └── Dockerfile.native-distroless
│ ├── java
│ │ └── com
│ │ │ └── refactorizando
│ │ │ └── sample
│ │ │ └── saga
│ │ │ ├── OrderSeatResource.java
│ │ │ ├── OrderServiceApplication.java
│ │ │ ├── events
│ │ │ ├── SeatEventConsumer.java
│ │ │ └── SeatEventProducer.java
│ │ │ ├── model
│ │ │ ├── Seat.java
│ │ │ └── User.java
│ │ │ ├── repository
│ │ │ ├── SeatRepository.java
│ │ │ └── UserRepository.java
│ │ │ ├── service
│ │ │ └── SeatService.java
│ │ │ └── usecase
│ │ │ └── ReservedSeat.java
│ └── resources
│ │ ├── application.properties
│ │ └── load-data.sql
│ └── test
│ └── java
│ └── com
│ └── refactorizando
│ └── sample
│ └── saga
│ ├── NativeOrderSeatResourceIT.java
│ └── OrderSeatResourceTest.java
└── payment
├── .dockerignore
├── .gitignore
├── README.adoc
├── pom.xml
└── src
├── main
├── docker
│ ├── Dockerfile.jvm
│ ├── Dockerfile.legacy-jar
│ ├── Dockerfile.native
│ └── Dockerfile.native-distroless
├── java
│ └── com
│ │ └── refactorizando
│ │ └── sample
│ │ └── saga
│ │ ├── PaymentApplication.java
│ │ ├── PaymentResource.java
│ │ ├── events
│ │ ├── PaymentConsumer.java
│ │ ├── PaymentProducer.java
│ │ └── compensation
│ │ │ ├── PaymentConsumerCompensation.java
│ │ │ └── SeatEventProducer.java
│ │ ├── model
│ │ ├── Payment.java
│ │ ├── Seat.java
│ │ └── User.java
│ │ ├── repository
│ │ ├── PaymentRepository.java
│ │ ├── SeatRepository.java
│ │ └── UserRepository.java
│ │ ├── service
│ │ ├── PaymentService.java
│ │ └── SeatService.java
│ │ └── usecase
│ │ ├── DeletePaymentUseCase.java
│ │ └── MakePaymentUseCase.java
└── resources
│ ├── application.properties
│ └── load-data.sql
└── test
└── java
└── com
└── refactorizando
└── sample
└── saga
├── NativeOrderSeatResourceIT.java
└── OrderSeatResourceTest.java
/README.adoc:
--------------------------------------------------------------------------------
1 | = Saga Pattern example with Quarkus and Kafka =
2 | {localdatetime}
3 | :toc:
4 | :doctype: book
5 | :docinfo:
6 |
7 |
8 | This project is a simple example about how to make a saga architecture in Quarkus with Apache Kafka and H2 as a database in memory.
9 |
10 |
11 | If you need more information you can take a look into:
12 |
13 | * Spanish: https://refactorizando.com/patron-saga-quarkus-kafka
14 | * English: https://refactorizando.com/en/saga-pattern-with-quarkus-and-kafka/
15 |
16 | == Introduction
17 |
18 | This example about Saga in Quarkus consists of three services, every service works as and independent service and all services together reach a goal. The communication between them is based in event sourcing, which means, that every time a state changes in the microservice, a new event will be triggered and sent to a microservice.
19 |
20 |
21 | We can say that a Saga Pattern is a sequence of local transactions, and every transaction update data inside of a single or independect service.
22 |
23 |
24 | So we are going to have three microservices, order-service, payment-service and the last one allocate-service.
25 |
26 | image::SagaPattern.png[]
27 |
28 |
29 | And every service communicate each other through events using kafka.
30 |
31 | image::SagaPatternArchitecture.png[]
32 |
33 |
34 | == Order Service
35 |
36 | https://github.com/refactorizando-web/saga-pattern-example-quarkus-kafka/tree/master/order
37 |
38 | This is the entry point in our architecture. This service expect a seat in a json format to start a new transaction to reserve a seat in the cinema. When a user make a new reserve a new procces start
39 | and the seat is locked, then a status change and a new event will be triggered to make a payment. If every step goes well then a seat will change it status to OCCUPIED.
40 |
41 | In the case of something fails, for example you are making a new payment and the client has not enough money in the account, then a compensation will start to restart seat status. We are going to have three different status: FREE, LOCKED and OCCUPIED.
42 |
43 |
44 |
45 | == Payment Service
46 |
47 | https://github.com/refactorizando-web/saga-pattern-example-quarkus-kafka/tree/master/payment
48 |
49 | The goal of this service is save a new payment in the database. When a new event from order service is received then create and save a new payment.
50 |
51 | If the payment is saved successfully then a event to change the seat status is sent. In other case a compensation procces start to make a rollback, delete the payment and change to FREE the status seat.
52 |
53 |
54 | == Allocate Service
55 |
56 | https://github.com/refactorizando-web/saga-pattern-example-quarkus-kafka/tree/master/allocate
57 |
58 | Allocate service is going to be our last service involved in our architecture. When a payment is saved, then a new event is triggered. Allocate Service processes that event and change the status seat to OCCUPIED and notifies this change to order service.
59 |
60 |
61 | == More interesting articles
62 |
63 | https://refactorizando.com/patron-saga-en-arquitectura-de-microservicios/
64 |
65 | https://refactorizando.com/hibernate-panache-quarkus/
66 |
67 | https://refactorizando.com/introduccion-arquitectura-event-driven/
68 |
--------------------------------------------------------------------------------
/SagaPattern.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/refactorizando-web/saga-pattern-example-quarkus-kafka/cc4fac322fdd193eeeabffb74cae2cee51e346d1/SagaPattern.png
--------------------------------------------------------------------------------
/SagaPatternArchitecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/refactorizando-web/saga-pattern-example-quarkus-kafka/cc4fac322fdd193eeeabffb74cae2cee51e346d1/SagaPatternArchitecture.png
--------------------------------------------------------------------------------
/allocate/.dockerignore:
--------------------------------------------------------------------------------
1 | *
2 | !target/*-runner
3 | !target/*-runner.jar
4 | !target/lib/*
5 | !target/quarkus-app/*
--------------------------------------------------------------------------------
/allocate/.gitignore:
--------------------------------------------------------------------------------
1 | #Maven
2 | target/
3 | pom.xml.tag
4 | pom.xml.releaseBackup
5 | pom.xml.versionsBackup
6 | release.properties
7 |
8 | # Eclipse
9 | .project
10 | .classpath
11 | .settings/
12 | bin/
13 |
14 | # IntelliJ
15 | .idea
16 | *.ipr
17 | *.iml
18 | *.iws
19 |
20 | # NetBeans
21 | nb-configuration.xml
22 |
23 | # Visual Studio Code
24 | .vscode
25 | .factorypath
26 |
27 | # OSX
28 | .DS_Store
29 |
30 | # Vim
31 | *.swp
32 | *.swo
33 |
34 | # patch
35 | *.orig
36 | *.rej
37 |
38 | # Local environment
39 | .env
40 |
--------------------------------------------------------------------------------
/allocate/README.adoc:
--------------------------------------------------------------------------------
1 | = Allocate Service =
2 | {localdatetime}
3 | :toc:
4 | :doctype: book
5 | :docinfo:
6 |
7 | This microservice has born with the aim of change and process status seat after receive an event from payment service.
8 |
9 | If you need more information you can take a look into:
10 | https://refactorizando.com/patron-saga-quarkus-kafka
11 |
12 | == Introduction
13 |
14 | This microservice is responsible for receiving an event and processing and change the status seat to OCCUPIED.
15 | Then, if all goes well, a new event will be sent to change the status of the seat in the order service. Otherwise,
16 | a compensation process, and a rollback will be triggered to leave the seat in its original state (FREE).
17 |
18 |
19 | == How does it work?
20 |
21 | === Run kafka
22 | The first thing you need to do is launch a kafka broker that you can do with the following docker:
23 |
24 | docker run --rm -p 2181:2181 -p 3030:3030 -p 8081-8083:8081-8083 -p 9581-9585:9581-9585 -p 9092:9092 -e ADV_HOST=localhost landoop/fast-data-dev:latest
25 |
26 | === Run the application
27 |
28 | ==== Running the application in dev mode
29 |
30 | You can run your application in dev mode that enables live coding using:
31 | ```shell script
32 | ./mvnw compile quarkus:dev
33 | ```
34 |
35 | > **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8085/q/dev/.
36 |
37 | ==== From your IDE
38 |
39 | You can run your application using your IDE and running the class:
40 | ```shell script
41 | AllocateApplication.java
42 | ```
43 |
44 | ==== Packaging and running the application
45 |
46 | The application can be packaged using:
47 | ```shell script
48 | ./mvnw package
49 | ```
50 | It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory.
51 | Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory.
52 |
53 | If you want to build an _über-jar_, execute the following command:
54 | ```shell script
55 | ./mvnw package -Dquarkus.package.type=uber-jar
56 | ```
57 |
58 | The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`.
59 |
60 | ==== Creating a native executable
61 |
62 | You can create a native executable using:
63 | ```shell script
64 | ./mvnw package -Pnative
65 | ```
66 |
67 | Or, if you don't have GraalVM installed, you can run the native executable build in a container using:
68 | ```shell script
69 | ./mvnw package -Pnative -Dquarkus.native.container-build=true
70 | ```
71 |
72 | You can then execute your native executable with: `./target/quarkus-saga-1.0.0-SNAPSHOT-runner`
73 |
74 | === Test the application
75 |
76 | You can make a request to the next endpoint http://localhost:8085/allocates/1, to check status seat in your database.
77 |
--------------------------------------------------------------------------------
/allocate/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 | com.refactorizando.sample.saga
6 | allocate-service
7 | 1.0.0-SNAPSHOT
8 |
9 | 3.8.1
10 | true
11 | 11
12 | 11
13 | UTF-8
14 | UTF-8
15 | quarkus-universe-bom
16 | io.quarkus
17 | 2.0.1.Final
18 | 3.0.0-M5
19 |
20 |
21 |
22 |
23 | ${quarkus.platform.group-id}
24 | ${quarkus.platform.artifact-id}
25 | ${quarkus.platform.version}
26 | pom
27 | import
28 |
29 |
30 |
31 |
32 |
33 | io.quarkus
34 | quarkus-jdbc-h2
35 |
36 |
37 | io.quarkus
38 | quarkus-smallrye-reactive-messaging-kafka
39 |
40 |
41 | io.quarkus
42 | quarkus-resteasy-jackson
43 |
44 |
45 | io.quarkus
46 | quarkus-arc
47 |
48 |
49 | io.quarkus
50 | quarkus-resteasy
51 |
52 |
53 | io.quarkus
54 | quarkus-junit5
55 | test
56 |
57 |
58 | io.rest-assured
59 | rest-assured
60 | test
61 |
62 |
63 |
64 | io.quarkus
65 | quarkus-hibernate-orm-panache
66 |
67 |
68 |
69 | org.projectlombok
70 | lombok
71 | 1.18.20
72 | provided
73 |
74 |
75 |
76 | com.fasterxml.jackson.core
77 | jackson-databind
78 | 2.11.1
79 |
80 |
81 |
82 |
83 |
84 |
85 | ${quarkus.platform.group-id}
86 | quarkus-maven-plugin
87 | ${quarkus.platform.version}
88 | true
89 |
90 |
91 |
92 | build
93 | generate-code
94 | generate-code-tests
95 |
96 |
97 |
98 |
99 |
100 | maven-compiler-plugin
101 | ${compiler-plugin.version}
102 |
103 | ${maven.compiler.parameters}
104 |
105 |
106 |
107 | maven-surefire-plugin
108 | ${surefire-plugin.version}
109 |
110 |
111 | org.jboss.logmanager.LogManager
112 | ${maven.home}
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | native
121 |
122 |
123 | native
124 |
125 |
126 |
127 |
128 |
129 | maven-failsafe-plugin
130 | ${surefire-plugin.version}
131 |
132 |
133 |
134 | integration-test
135 | verify
136 |
137 |
138 |
139 | ${project.build.directory}/${project.build.finalName}-runner
140 | org.jboss.logmanager.LogManager
141 | ${maven.home}
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | native
151 |
152 |
153 |
154 |
155 |
--------------------------------------------------------------------------------
/allocate/src/main/docker/Dockerfile.jvm:
--------------------------------------------------------------------------------
1 | ####
2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
3 | #
4 | # Before building the container image run:
5 | #
6 | # ./mvnw package
7 | #
8 | # Then, build the image with:
9 | #
10 | # docker build -f src/main/docker/Dockerfile.jvm -t quarkus/quarkus-saga-jvm .
11 | #
12 | # Then run the container using:
13 | #
14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus-saga-jvm
15 | #
16 | # If you want to include the debug port into your docker image
17 | # you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005
18 | #
19 | # Then run the container using :
20 | #
21 | # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/quarkus-saga-jvm
22 | #
23 | ###
24 | FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4
25 |
26 | ARG JAVA_PACKAGE=java-11-openjdk-headless
27 | ARG RUN_JAVA_VERSION=1.3.8
28 | ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
29 | # Install java and the run-java script
30 | # Also set up permissions for user `1001`
31 | RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
32 | && microdnf update \
33 | && microdnf clean all \
34 | && mkdir /deployments \
35 | && chown 1001 /deployments \
36 | && chmod "g+rwX" /deployments \
37 | && chown 1001:root /deployments \
38 | && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
39 | && chown 1001 /deployments/run-java.sh \
40 | && chmod 540 /deployments/run-java.sh \
41 | && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/conf/security/java.security
42 |
43 | # Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.
44 | ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
45 | # We make four distinct layers so if there are application changes the library layers can be re-used
46 | COPY --chown=1001 target/quarkus-app/lib/ /deployments/lib/
47 | COPY --chown=1001 target/quarkus-app/*.jar /deployments/
48 | COPY --chown=1001 target/quarkus-app/app/ /deployments/app/
49 | COPY --chown=1001 target/quarkus-app/quarkus/ /deployments/quarkus/
50 |
51 | EXPOSE 8080
52 | USER 1001
53 |
54 | ENTRYPOINT [ "/deployments/run-java.sh" ]
55 |
56 |
--------------------------------------------------------------------------------
/allocate/src/main/docker/Dockerfile.legacy-jar:
--------------------------------------------------------------------------------
1 | ####
2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
3 | #
4 | # Before building the container image run:
5 | #
6 | # ./mvnw package -Dquarkus.package.type=legacy-jar
7 | #
8 | # Then, build the image with:
9 | #
10 | # docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/quarkus-saga-legacy-jar .
11 | #
12 | # Then run the container using:
13 | #
14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus-saga-legacy-jar
15 | #
16 | # If you want to include the debug port into your docker image
17 | # you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005
18 | #
19 | # Then run the container using :
20 | #
21 | # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/quarkus-saga-legacy-jar
22 | #
23 | ###
24 | FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4
25 |
26 | ARG JAVA_PACKAGE=java-11-openjdk-headless
27 | ARG RUN_JAVA_VERSION=1.3.8
28 | ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
29 | # Install java and the run-java script
30 | # Also set up permissions for user `1001`
31 | RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
32 | && microdnf update \
33 | && microdnf clean all \
34 | && mkdir /deployments \
35 | && chown 1001 /deployments \
36 | && chmod "g+rwX" /deployments \
37 | && chown 1001:root /deployments \
38 | && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
39 | && chown 1001 /deployments/run-java.sh \
40 | && chmod 540 /deployments/run-java.sh \
41 | && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/conf/security/java.security
42 |
43 | # Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.
44 | ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
45 | COPY target/lib/* /deployments/lib/
46 | COPY target/*-runner.jar /deployments/app.jar
47 |
48 | EXPOSE 8080
49 | USER 1001
50 |
51 | ENTRYPOINT [ "/deployments/run-java.sh" ]
52 |
--------------------------------------------------------------------------------
/allocate/src/main/docker/Dockerfile.native:
--------------------------------------------------------------------------------
1 | ####
2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode
3 | #
4 | # Before building the container image run:
5 | #
6 | # ./mvnw package -Pnative
7 | #
8 | # Then, build the image with:
9 | #
10 | # docker build -f src/main/docker/Dockerfile.native -t quarkus/quarkus-saga .
11 | #
12 | # Then run the container using:
13 | #
14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus-saga
15 | #
16 | ###
17 | FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4
18 | WORKDIR /work/
19 | RUN chown 1001 /work \
20 | && chmod "g+rwX" /work \
21 | && chown 1001:root /work
22 | COPY --chown=1001:root target/*-runner /work/application
23 |
24 | EXPOSE 8080
25 | USER 1001
26 |
27 | CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
28 |
--------------------------------------------------------------------------------
/allocate/src/main/docker/Dockerfile.native-distroless:
--------------------------------------------------------------------------------
1 | ####
2 | # This Dockerfile is used in order to build a distroless container that runs the Quarkus application in native (no JVM) mode
3 | #
4 | # Before building the container image run:
5 | #
6 | # ./mvnw package -Pnative
7 | #
8 | # Then, build the image with:
9 | #
10 | # docker build -f src/main/docker/Dockerfile.native-distroless -t quarkus/quarkus-saga .
11 | #
12 | # Then run the container using:
13 | #
14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus-saga
15 | #
16 | ###
17 | FROM quay.io/quarkus/quarkus-distroless-image:1.0
18 | COPY target/*-runner /application
19 |
20 | EXPOSE 8080
21 | USER nonroot
22 |
23 | CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
24 |
--------------------------------------------------------------------------------
/allocate/src/main/java/com/refactorizando/sample/saga/AllocateApplication.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga;
2 |
3 | import io.quarkus.runtime.Quarkus;
4 | import io.quarkus.runtime.QuarkusApplication;
5 | import io.quarkus.runtime.annotations.QuarkusMain;
6 | import lombok.extern.slf4j.Slf4j;
7 |
8 | @QuarkusMain
9 | @Slf4j
10 | public class AllocateApplication {
11 |
12 |
13 | public static void main(String... args) {
14 | Quarkus.run(MyApp.class, args);
15 | }
16 |
17 | public static class MyApp implements QuarkusApplication {
18 |
19 |
20 | @Override
21 | public int run(String... args) throws Exception {
22 | System.out.println("Do startup logic here");
23 |
24 | Quarkus.waitForExit();
25 | return 0;
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/allocate/src/main/java/com/refactorizando/sample/saga/AllocateResource.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga;
2 |
3 | import com.refactorizando.sample.saga.service.SeatService;
4 | import lombok.RequiredArgsConstructor;
5 | import lombok.extern.slf4j.Slf4j;
6 |
7 | import javax.ws.rs.*;
8 | import javax.ws.rs.core.MediaType;
9 | import javax.ws.rs.core.Response;
10 |
11 | @Path("/allocates")
12 | @RequiredArgsConstructor
13 | @Produces(MediaType.APPLICATION_JSON)
14 | @Consumes(MediaType.APPLICATION_JSON)
15 | @Slf4j
16 | public class AllocateResource {
17 |
18 | private final SeatService seatService;
19 |
20 | @GET
21 | @Path("/{id}")
22 | public Response allocateSeat(@PathParam("id") Long id) {
23 |
24 | log.info("Seat status by id");
25 |
26 | return Response.status(Response.Status.OK)
27 | .entity(seatService.findById(id)).build();
28 | }
29 |
30 |
31 | }
--------------------------------------------------------------------------------
/allocate/src/main/java/com/refactorizando/sample/saga/events/AllocateConsumer.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.events;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.refactorizando.sample.saga.model.Payment;
5 | import com.refactorizando.sample.saga.usecase.AllocateUseCase;
6 | import io.smallrye.reactive.messaging.kafka.Record;
7 | import lombok.RequiredArgsConstructor;
8 | import lombok.SneakyThrows;
9 | import lombok.extern.slf4j.Slf4j;
10 | import org.eclipse.microprofile.reactive.messaging.Incoming;
11 |
12 | import javax.enterprise.context.ApplicationScoped;
13 |
14 | @ApplicationScoped
15 | @RequiredArgsConstructor
16 | @Slf4j
17 | public class AllocateConsumer {
18 |
19 | private final AllocateUseCase allocateUseCase;
20 |
21 | @SneakyThrows
22 | @Incoming("allocates-in")
23 | public void receive(Record record) {
24 |
25 | log.info("Event received with key: {}", record.key());
26 |
27 | ObjectMapper objectMapper = new ObjectMapper();
28 |
29 | var payment = objectMapper.readValue(record.value(), Payment.class);
30 |
31 | var seat = payment.getSeat();
32 |
33 | allocateUseCase.updateSeat(seat);
34 |
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/allocate/src/main/java/com/refactorizando/sample/saga/events/AllocateProducer.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.events;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.refactorizando.sample.saga.model.Seat;
5 | import io.smallrye.reactive.messaging.kafka.Record;
6 | import lombok.SneakyThrows;
7 | import lombok.extern.slf4j.Slf4j;
8 | import org.eclipse.microprofile.reactive.messaging.Channel;
9 | import org.eclipse.microprofile.reactive.messaging.Emitter;
10 |
11 | import javax.enterprise.context.ApplicationScoped;
12 |
13 | @ApplicationScoped
14 | @Slf4j
15 | public class AllocateProducer {
16 |
17 | @Channel("seats-out")
18 | Emitter> emitter;
19 |
20 | @SneakyThrows
21 | public void sendSeatEvent(Seat seat) {
22 | log.info("Event sent seat with id{}", seat.getId());
23 |
24 | ObjectMapper objectMapper = new ObjectMapper();
25 | var seatJson = objectMapper.writeValueAsString(seat);
26 |
27 | emitter.send(Record.of(seat.getId(), seatJson))
28 | .whenComplete((success, failure) -> {
29 | if (failure != null) {
30 | log.error("D'oh! " + failure.getMessage());
31 | } else {
32 | log.info("Message processed successfully");
33 | }
34 | });
35 | }
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/allocate/src/main/java/com/refactorizando/sample/saga/events/compensation/PaymentProducerCompensation.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.events.compensation;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.refactorizando.sample.saga.model.Payment;
5 | import com.refactorizando.sample.saga.model.Seat;
6 | import com.refactorizando.sample.saga.service.PaymentService;
7 | import io.smallrye.reactive.messaging.kafka.Record;
8 | import lombok.RequiredArgsConstructor;
9 | import lombok.SneakyThrows;
10 | import lombok.extern.slf4j.Slf4j;
11 | import org.eclipse.microprofile.reactive.messaging.Channel;
12 | import org.eclipse.microprofile.reactive.messaging.Emitter;
13 | import org.eclipse.microprofile.reactive.messaging.Incoming;
14 |
15 | import javax.enterprise.context.ApplicationScoped;
16 | import javax.inject.Inject;
17 |
18 | @ApplicationScoped
19 | @RequiredArgsConstructor
20 | @Slf4j
21 | public class PaymentProducerCompensation {
22 |
23 | @Channel("payments-out")
24 | Emitter> emitter;
25 |
26 | @SneakyThrows
27 | public void sendPaymentEvent(Payment payment) {
28 | log.info("Event sent {}", payment.getId());
29 |
30 | ObjectMapper objectMapper = new ObjectMapper();
31 | var paymentJson = objectMapper.writeValueAsString(payment);
32 |
33 | emitter.send(Record.of(payment.getId(), paymentJson))
34 | .whenComplete((success, failure) -> {
35 | if (failure != null) {
36 | log.error("D'oh! " + failure.getMessage());
37 | } else {
38 | log.info("Message processed successfully");
39 | }
40 | });
41 | }
42 | }
--------------------------------------------------------------------------------
/allocate/src/main/java/com/refactorizando/sample/saga/model/Payment.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.model;
2 |
3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize;
5 | import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
6 | import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
7 | import lombok.Getter;
8 | import lombok.Setter;
9 | import lombok.ToString;
10 | import org.hibernate.annotations.SQLDelete;
11 |
12 | import javax.persistence.*;
13 | import java.math.BigDecimal;
14 | import java.time.LocalDate;
15 |
16 | @Getter
17 | @Setter
18 | @Entity
19 | @SQLDelete(sql = "UPDATE Payment SET status=deleted WHERE id = ?")
20 | @ToString
21 | public class Payment {
22 |
23 | @Id
24 | @GeneratedValue(strategy = GenerationType.IDENTITY)
25 | private Long id;
26 |
27 | private String status;
28 |
29 | @OneToOne
30 | private User user;
31 |
32 | @OneToOne
33 | private Seat seat;
34 |
35 | @JsonDeserialize(using = LocalDateDeserializer.class)
36 | @JsonSerialize(using = LocalDateSerializer.class)
37 | private LocalDate date;
38 |
39 | private BigDecimal amount;
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/allocate/src/main/java/com/refactorizando/sample/saga/model/Seat.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 | import lombok.Getter;
5 | import lombok.Setter;
6 | import lombok.ToString;
7 |
8 | import javax.persistence.*;
9 | import java.net.URL;
10 | import java.time.LocalDate;
11 | import java.util.List;
12 |
13 | @Entity
14 | @Getter
15 | @Setter
16 | @ToString
17 | @JsonIgnoreProperties(ignoreUnknown = true)
18 | public class Seat {
19 |
20 | @Id
21 | @GeneratedValue(strategy = GenerationType.IDENTITY)
22 | private Long id;
23 |
24 | private Integer column;
25 |
26 | private Integer row;
27 |
28 | private String ref;
29 |
30 | private String status;
31 |
32 | @OneToOne
33 | private User user;
34 |
35 | @OneToOne
36 | private Payment payment;
37 | }
38 |
--------------------------------------------------------------------------------
/allocate/src/main/java/com/refactorizando/sample/saga/model/User.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.model;
2 |
3 | import java.time.LocalDate;
4 | import java.util.List;
5 |
6 | import lombok.Getter;
7 | import lombok.NoArgsConstructor;
8 | import lombok.Setter;
9 | import lombok.ToString;
10 |
11 | import javax.persistence.*;
12 | import javax.validation.constraints.NotNull;
13 |
14 | @Getter
15 | @Setter
16 | @NoArgsConstructor
17 | @Entity
18 | @ToString
19 | public class User {
20 |
21 | @Id
22 | @GeneratedValue(strategy = GenerationType.IDENTITY)
23 | private Long id;
24 |
25 | @NotNull
26 | private String name;
27 |
28 | private String surname;
29 |
30 | private LocalDate birth;
31 |
32 | private String email;
33 |
34 | @OneToOne
35 | private Seat seat;
36 |
37 | @OneToOne
38 | private Payment payment;
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/allocate/src/main/java/com/refactorizando/sample/saga/repository/PaymentRepository.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.repository;
2 |
3 | import com.refactorizando.sample.saga.model.Payment;
4 | import io.quarkus.hibernate.orm.panache.PanacheRepository;
5 |
6 | import javax.enterprise.context.ApplicationScoped;
7 |
8 | @ApplicationScoped
9 | public class PaymentRepository implements PanacheRepository {
10 | }
11 |
--------------------------------------------------------------------------------
/allocate/src/main/java/com/refactorizando/sample/saga/repository/SeatRepository.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.repository;
2 |
3 | import com.refactorizando.sample.saga.model.Seat;
4 | import io.quarkus.hibernate.orm.panache.PanacheRepository;
5 |
6 | import javax.enterprise.context.ApplicationScoped;
7 |
8 | @ApplicationScoped
9 | public class SeatRepository implements PanacheRepository {
10 |
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/allocate/src/main/java/com/refactorizando/sample/saga/repository/UserRepository.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.repository;
2 |
3 | import com.refactorizando.sample.saga.model.Seat;
4 | import com.refactorizando.sample.saga.model.User;
5 | import io.quarkus.hibernate.orm.panache.PanacheRepository;
6 |
7 | import javax.enterprise.context.ApplicationScoped;
8 |
9 | @ApplicationScoped
10 | public class UserRepository implements PanacheRepository {
11 |
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/allocate/src/main/java/com/refactorizando/sample/saga/service/PaymentService.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.service;
2 |
3 | import com.refactorizando.sample.saga.events.compensation.PaymentProducerCompensation;
4 | import com.refactorizando.sample.saga.model.Payment;
5 | import com.refactorizando.sample.saga.repository.PaymentRepository;
6 | import lombok.RequiredArgsConstructor;
7 | import lombok.extern.slf4j.Slf4j;
8 |
9 | import javax.enterprise.context.ApplicationScoped;
10 | import javax.transaction.Transactional;
11 |
12 | @ApplicationScoped
13 | @RequiredArgsConstructor
14 | @Slf4j
15 | public class PaymentService {
16 |
17 | private final PaymentRepository paymentRepository;
18 |
19 | private final PaymentProducerCompensation paymentProducerCompensation;
20 |
21 | @Transactional
22 | public Payment savePayment(Payment payment) {
23 |
24 | try {
25 |
26 | paymentRepository.persistAndFlush(payment);
27 |
28 | } catch (Exception ex) {
29 |
30 | paymentProducerCompensation.sendPaymentEvent(payment);
31 |
32 | }
33 |
34 | return payment;
35 | }
36 |
37 | public void deletePayment(Long paymentId) {
38 |
39 | try {
40 | paymentRepository.deleteById(paymentId);
41 |
42 | } catch (Exception ex) {
43 |
44 | paymentProducerCompensation.sendPaymentEvent(paymentRepository.findById(paymentId));
45 |
46 | }
47 | //Refund money to user
48 |
49 | }
50 |
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/allocate/src/main/java/com/refactorizando/sample/saga/service/SeatService.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.service;
2 |
3 | import com.refactorizando.sample.saga.model.Seat;
4 | import com.refactorizando.sample.saga.repository.SeatRepository;
5 | import lombok.RequiredArgsConstructor;
6 | import lombok.extern.slf4j.Slf4j;
7 |
8 | import javax.enterprise.context.ApplicationScoped;
9 | import javax.transaction.Transactional;
10 |
11 | import static javax.transaction.Transactional.TxType.REQUIRES_NEW;
12 |
13 | @ApplicationScoped
14 | @RequiredArgsConstructor
15 | @Slf4j
16 | public class SeatService {
17 |
18 | private final SeatRepository seatRepository;
19 |
20 |
21 | @Transactional(REQUIRES_NEW)
22 | public void updateSeat(Seat seat) {
23 |
24 | log.info("Block a seat {}", seat.toString());
25 |
26 | seatRepository.update("status = 'OCCUPIED' where id = ?1", seat.getId());
27 |
28 | }
29 |
30 |
31 | public Seat findById(Long id) {
32 |
33 | return seatRepository.findById(id);
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/allocate/src/main/java/com/refactorizando/sample/saga/usecase/AllocateUseCase.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.usecase;
2 |
3 | import com.refactorizando.sample.saga.events.AllocateProducer;
4 | import com.refactorizando.sample.saga.events.compensation.PaymentProducerCompensation;
5 | import com.refactorizando.sample.saga.model.Seat;
6 | import com.refactorizando.sample.saga.service.SeatService;
7 | import lombok.RequiredArgsConstructor;
8 | import lombok.extern.slf4j.Slf4j;
9 |
10 | import javax.enterprise.context.ApplicationScoped;
11 |
12 | @ApplicationScoped
13 | @RequiredArgsConstructor
14 | @Slf4j
15 | public class AllocateUseCase {
16 |
17 | private final SeatService seatService;
18 |
19 | private final PaymentProducerCompensation paymentProducerCompensation;
20 |
21 | private final AllocateProducer allocateProducer;
22 |
23 |
24 | public Seat updateSeat(Seat seat) {
25 |
26 | log.info("Save seat {}", seat.toString());
27 |
28 | try {
29 | seatService.updateSeat(seat);
30 |
31 | allocateProducer.sendSeatEvent(seat);
32 |
33 | } catch (Exception ex) {
34 |
35 | paymentProducerCompensation.sendPaymentEvent(seat.getPayment());
36 |
37 | }
38 |
39 | return seatService.findById(seat.getId());
40 |
41 | }
42 |
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/allocate/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | quarkus.http.port=8085
2 |
3 |
4 | #Configuring database
5 | quarkus.application.name=order-service
6 | quarkus.datasource.db-kind=h2
7 | quarkus.datasource.username=sa
8 | quarkus.datasource.password=password
9 | quarkus.datasource.jdbc.url=jdbc:h2:mem:testdb
10 | quarkus.hibernate-orm.database.generation=drop-and-create
11 | quarkus.hibernate-orm.log.sql=true
12 |
13 |
14 | # The Kafka broker location (defaults to localhost:9092)
15 | kafka.bootstrap.servers=localhost:9092
16 |
17 |
18 | # Configuring the incoming channel (reading from Kafka)
19 | mp.messaging.incoming.allocates-in.connector=smallrye-kafka
20 | mp.messaging.incoming.allocates-in.topic=payment
21 | mp.messaging.incoming.allocates-in.key.deserializer=org.apache.kafka.common.serialization.LongDeserializer
22 | mp.messaging.incoming.allocates-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
23 |
24 | # Configuring compensation
25 | # Configuring the incoming channel (reading from Kafka)
26 | mp.messaging.outgoing.allocates-out.connector=smallrye-kafka
27 | mp.messaging.outgoing.allocates-out.topic=status-update
28 | mp.messaging.outgoing.allocates-out.key.serializer=org.apache.kafka.common.serialization.LongSerializer
29 | mp.messaging.outgoing.allocates-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer
30 |
31 | # Configuring compensation
32 | # Configuring the incoming channel (reading from Kafka)
33 | mp.messaging.outgoing.payments-out.connector=smallrye-kafka
34 | mp.messaging.outgoing.payments-out.topic=payment-update
35 | mp.messaging.outgoing.payments-out.key.serializer=org.apache.kafka.common.serialization.LongSerializer
36 | mp.messaging.outgoing.payments-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer
37 |
38 |
39 | quarkus.hibernate-orm.sql-load-script=load-data.sql
40 | quarkus.log.level=INFO
--------------------------------------------------------------------------------
/allocate/src/main/resources/load-data.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO USER(id, name, surname, email) VALUES(1, 'noel', 'rodriguez', 'refactorizando.web@gmail.com');
2 | INSERT INTO SEAT(id, column, row, status, user_id) VALUES(1, 1, 1, 'FREE',1);
3 |
4 |
5 |
--------------------------------------------------------------------------------
/allocate/src/test/java/com/refactorizando/sample/saga/NativeOrderSeatResourceIT.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga;
2 |
3 | import io.quarkus.test.junit.NativeImageTest;
4 |
5 | //@NativeImageTest
6 | public class NativeOrderSeatResourceIT extends OrderSeatResourceTest {
7 |
8 | // Execute the same tests but in native mode.
9 | }
--------------------------------------------------------------------------------
/allocate/src/test/java/com/refactorizando/sample/saga/OrderSeatResourceTest.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga;
2 |
3 | import io.quarkus.test.junit.QuarkusTest;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import static io.restassured.RestAssured.given;
7 | import static org.hamcrest.CoreMatchers.is;
8 |
9 | //@QuarkusTest
10 | public class OrderSeatResourceTest {
11 |
12 |
13 | }
--------------------------------------------------------------------------------
/order/.dockerignore:
--------------------------------------------------------------------------------
1 | *
2 | !target/*-runner
3 | !target/*-runner.jar
4 | !target/lib/*
5 | !target/quarkus-app/*
--------------------------------------------------------------------------------
/order/.gitignore:
--------------------------------------------------------------------------------
1 | #Maven
2 | target/
3 | pom.xml.tag
4 | pom.xml.releaseBackup
5 | pom.xml.versionsBackup
6 | release.properties
7 |
8 | # Eclipse
9 | .project
10 | .classpath
11 | .settings/
12 | bin/
13 |
14 | # IntelliJ
15 | .idea
16 | *.ipr
17 | *.iml
18 | *.iws
19 |
20 | # NetBeans
21 | nb-configuration.xml
22 |
23 | # Visual Studio Code
24 | .vscode
25 | .factorypath
26 |
27 | # OSX
28 | .DS_Store
29 |
30 | # Vim
31 | *.swp
32 | *.swo
33 |
34 | # patch
35 | *.orig
36 | *.rej
37 |
38 | # Local environment
39 | .env
40 |
--------------------------------------------------------------------------------
/order/README.adoc:
--------------------------------------------------------------------------------
1 | = Order Service =
2 | {localdatetime}
3 | :toc:
4 | :doctype: book
5 | :docinfo:
6 |
7 | This microservice has born with the aim of placing a new order to get a seat
8 | to see a good movie.
9 |
10 | If you need more information you can take a look into:
11 | https://refactorizando.com/patron-saga-quarkus-kafka
12 |
13 | == Introduction
14 |
15 | This microservice is responsible for receiving the request in json format for
16 | take a seat. When the request is received, a transaction will be initiated for
17 | achieve that goal.
18 |
19 | == How does it work?
20 |
21 | === Run kafka
22 | The first thing you need to do is launch a kafka broker that you can do with the following docker:
23 |
24 | docker run --rm -p 2181:2181 -p 3030:3030 -p 8081-8083:8081-8083 -p 9581-9585:9581-9585 -p 9092:9092 -e ADV_HOST=localhost landoop/fast-data-dev:latest
25 |
26 | === Run the application
27 |
28 | ==== Running the application in dev mode
29 |
30 | You can run your application in dev mode that enables live coding using:
31 | ```shell script
32 | ./mvnw compile quarkus:dev
33 | ```
34 |
35 | > **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8090/q/dev/.
36 |
37 | ==== From your IDE
38 |
39 | You can run your application using your IDE and running the class:
40 | ```shell script
41 | AllocateApplication.java
42 | ```
43 |
44 | ==== Packaging and running the application
45 |
46 | The application can be packaged using:
47 | ```shell script
48 | ./mvnw package
49 | ```
50 | It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory.
51 | Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory.
52 |
53 | If you want to build an _über-jar_, execute the following command:
54 | ```shell script
55 | ./mvnw package -Dquarkus.package.type=uber-jar
56 | ```
57 |
58 | The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`.
59 |
60 | ==== Creating a native executable
61 |
62 | You can create a native executable using:
63 | ```shell script
64 | ./mvnw package -Pnative
65 | ```
66 |
67 | Or, if you don't have GraalVM installed, you can run the native executable build in a container using:
68 | ```shell script
69 | ./mvnw package -Pnative -Dquarkus.native.container-build=true
70 | ```
71 |
72 | You can then execute your native executable with: `./target/quarkus-saga-1.0.0-SNAPSHOT-runner`
73 |
74 | === Test the application
75 |
76 | If you want to try and verify how it works you can use your postman and make a POST in http://localhost:8090/seats with the next body:
77 |
78 | {
79 | "id":1,
80 | "column": 1,
81 | "row":1,
82 | "user":{
83 | "id":1,
84 | "name":"noel"
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/order/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 | com.refactorizando.sample.saga
6 | order-service
7 | 1.0.0-SNAPSHOT
8 |
9 | 3.8.1
10 | true
11 | 11
12 | 11
13 | UTF-8
14 | UTF-8
15 | quarkus-universe-bom
16 | io.quarkus
17 | 2.0.1.Final
18 | 3.0.0-M5
19 |
20 |
21 |
22 |
23 | ${quarkus.platform.group-id}
24 | ${quarkus.platform.artifact-id}
25 | ${quarkus.platform.version}
26 | pom
27 | import
28 |
29 |
30 |
31 |
32 |
33 | io.quarkus
34 | quarkus-jdbc-h2
35 |
36 |
37 | io.quarkus
38 | quarkus-smallrye-reactive-messaging-kafka
39 |
40 |
41 | io.quarkus
42 | quarkus-resteasy-jackson
43 |
44 |
45 | io.quarkus
46 | quarkus-arc
47 |
48 |
49 | io.quarkus
50 | quarkus-resteasy
51 |
52 |
53 | io.quarkus
54 | quarkus-junit5
55 | test
56 |
57 |
58 | io.rest-assured
59 | rest-assured
60 | test
61 |
62 |
63 |
64 | io.quarkus
65 | quarkus-hibernate-orm-panache
66 |
67 |
68 | org.projectlombok
69 | lombok
70 | 1.18.20
71 | provided
72 |
73 |
74 |
75 |
76 |
77 | ${quarkus.platform.group-id}
78 | quarkus-maven-plugin
79 | ${quarkus.platform.version}
80 | true
81 |
82 |
83 |
84 | build
85 | generate-code
86 | generate-code-tests
87 |
88 |
89 |
90 |
91 |
92 | maven-compiler-plugin
93 | ${compiler-plugin.version}
94 |
95 | ${maven.compiler.parameters}
96 |
97 |
98 |
99 | maven-surefire-plugin
100 | ${surefire-plugin.version}
101 |
102 |
103 | org.jboss.logmanager.LogManager
104 | ${maven.home}
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | native
113 |
114 |
115 | native
116 |
117 |
118 |
119 |
120 |
121 | maven-failsafe-plugin
122 | ${surefire-plugin.version}
123 |
124 |
125 |
126 | integration-test
127 | verify
128 |
129 |
130 |
131 | ${project.build.directory}/${project.build.finalName}-runner
132 | org.jboss.logmanager.LogManager
133 | ${maven.home}
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 | native
143 |
144 |
145 |
146 |
147 |
--------------------------------------------------------------------------------
/order/src/main/docker/Dockerfile.jvm:
--------------------------------------------------------------------------------
1 | ####
2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
3 | #
4 | # Before building the container image run:
5 | #
6 | # ./mvnw package
7 | #
8 | # Then, build the image with:
9 | #
10 | # docker build -f src/main/docker/Dockerfile.jvm -t quarkus/quarkus-saga-jvm .
11 | #
12 | # Then run the container using:
13 | #
14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus-saga-jvm
15 | #
16 | # If you want to include the debug port into your docker image
17 | # you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005
18 | #
19 | # Then run the container using :
20 | #
21 | # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/quarkus-saga-jvm
22 | #
23 | ###
24 | FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4
25 |
26 | ARG JAVA_PACKAGE=java-11-openjdk-headless
27 | ARG RUN_JAVA_VERSION=1.3.8
28 | ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
29 | # Install java and the run-java script
30 | # Also set up permissions for user `1001`
31 | RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
32 | && microdnf update \
33 | && microdnf clean all \
34 | && mkdir /deployments \
35 | && chown 1001 /deployments \
36 | && chmod "g+rwX" /deployments \
37 | && chown 1001:root /deployments \
38 | && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
39 | && chown 1001 /deployments/run-java.sh \
40 | && chmod 540 /deployments/run-java.sh \
41 | && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/conf/security/java.security
42 |
43 | # Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.
44 | ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
45 | # We make four distinct layers so if there are application changes the library layers can be re-used
46 | COPY --chown=1001 target/quarkus-app/lib/ /deployments/lib/
47 | COPY --chown=1001 target/quarkus-app/*.jar /deployments/
48 | COPY --chown=1001 target/quarkus-app/app/ /deployments/app/
49 | COPY --chown=1001 target/quarkus-app/quarkus/ /deployments/quarkus/
50 |
51 | EXPOSE 8080
52 | USER 1001
53 |
54 | ENTRYPOINT [ "/deployments/run-java.sh" ]
55 |
56 |
--------------------------------------------------------------------------------
/order/src/main/docker/Dockerfile.legacy-jar:
--------------------------------------------------------------------------------
1 | ####
2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
3 | #
4 | # Before building the container image run:
5 | #
6 | # ./mvnw package -Dquarkus.package.type=legacy-jar
7 | #
8 | # Then, build the image with:
9 | #
10 | # docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/quarkus-saga-legacy-jar .
11 | #
12 | # Then run the container using:
13 | #
14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus-saga-legacy-jar
15 | #
16 | # If you want to include the debug port into your docker image
17 | # you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005
18 | #
19 | # Then run the container using :
20 | #
21 | # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/quarkus-saga-legacy-jar
22 | #
23 | ###
24 | FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4
25 |
26 | ARG JAVA_PACKAGE=java-11-openjdk-headless
27 | ARG RUN_JAVA_VERSION=1.3.8
28 | ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
29 | # Install java and the run-java script
30 | # Also set up permissions for user `1001`
31 | RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
32 | && microdnf update \
33 | && microdnf clean all \
34 | && mkdir /deployments \
35 | && chown 1001 /deployments \
36 | && chmod "g+rwX" /deployments \
37 | && chown 1001:root /deployments \
38 | && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
39 | && chown 1001 /deployments/run-java.sh \
40 | && chmod 540 /deployments/run-java.sh \
41 | && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/conf/security/java.security
42 |
43 | # Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.
44 | ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
45 | COPY target/lib/* /deployments/lib/
46 | COPY target/*-runner.jar /deployments/app.jar
47 |
48 | EXPOSE 8080
49 | USER 1001
50 |
51 | ENTRYPOINT [ "/deployments/run-java.sh" ]
52 |
--------------------------------------------------------------------------------
/order/src/main/docker/Dockerfile.native:
--------------------------------------------------------------------------------
1 | ####
2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode
3 | #
4 | # Before building the container image run:
5 | #
6 | # ./mvnw package -Pnative
7 | #
8 | # Then, build the image with:
9 | #
10 | # docker build -f src/main/docker/Dockerfile.native -t quarkus/quarkus-saga .
11 | #
12 | # Then run the container using:
13 | #
14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus-saga
15 | #
16 | ###
17 | FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4
18 | WORKDIR /work/
19 | RUN chown 1001 /work \
20 | && chmod "g+rwX" /work \
21 | && chown 1001:root /work
22 | COPY --chown=1001:root target/*-runner /work/application
23 |
24 | EXPOSE 8080
25 | USER 1001
26 |
27 | CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
28 |
--------------------------------------------------------------------------------
/order/src/main/docker/Dockerfile.native-distroless:
--------------------------------------------------------------------------------
1 | ####
2 | # This Dockerfile is used in order to build a distroless container that runs the Quarkus application in native (no JVM) mode
3 | #
4 | # Before building the container image run:
5 | #
6 | # ./mvnw package -Pnative
7 | #
8 | # Then, build the image with:
9 | #
10 | # docker build -f src/main/docker/Dockerfile.native-distroless -t quarkus/quarkus-saga .
11 | #
12 | # Then run the container using:
13 | #
14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus-saga
15 | #
16 | ###
17 | FROM quay.io/quarkus/quarkus-distroless-image:1.0
18 | COPY target/*-runner /application
19 |
20 | EXPOSE 8080
21 | USER nonroot
22 |
23 | CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
24 |
--------------------------------------------------------------------------------
/order/src/main/java/com/refactorizando/sample/saga/OrderSeatResource.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga;
2 |
3 | import com.refactorizando.sample.saga.model.Seat;
4 | import com.refactorizando.sample.saga.repository.SeatRepository;
5 | import com.refactorizando.sample.saga.repository.UserRepository;
6 | import com.refactorizando.sample.saga.service.SeatService;
7 | import com.refactorizando.sample.saga.usecase.ReservedSeat;
8 | import lombok.RequiredArgsConstructor;
9 | import lombok.extern.slf4j.Slf4j;
10 |
11 | import javax.inject.Inject;
12 | import javax.ws.rs.*;
13 | import javax.ws.rs.core.MediaType;
14 | import javax.ws.rs.core.Response;
15 |
16 | @Path("/seats")
17 | @RequiredArgsConstructor
18 | @Produces(MediaType.APPLICATION_JSON)
19 | @Consumes(MediaType.APPLICATION_JSON)
20 | @Slf4j
21 | public class OrderSeatResource {
22 |
23 | @Inject
24 | private final ReservedSeat service;
25 |
26 | @Inject
27 | private final UserRepository userRepository;
28 |
29 | @Inject
30 | private final SeatRepository seatRepository;
31 |
32 |
33 | private final SeatService seatService;
34 |
35 | @POST
36 | public Response orderSeat(Seat seat) {
37 |
38 | log.info("New order received ");
39 |
40 | return Response.status(Response.Status.CREATED)
41 | .entity(service.reservedSeat(seat)).build();
42 | }
43 |
44 | @GET
45 | @Path("/{id}")
46 | public Response orderSeat(@PathParam("id") Long id) {
47 |
48 | log.info("Seat status by id");
49 |
50 | return Response.status(Response.Status.OK)
51 | .entity(seatService.findById(id)).build();
52 | }
53 |
54 | }
--------------------------------------------------------------------------------
/order/src/main/java/com/refactorizando/sample/saga/OrderServiceApplication.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga;
2 |
3 | import io.quarkus.runtime.Quarkus;
4 | import io.quarkus.runtime.QuarkusApplication;
5 | import io.quarkus.runtime.annotations.QuarkusMain;
6 | import lombok.extern.slf4j.Slf4j;
7 |
8 |
9 | @QuarkusMain
10 | @Slf4j
11 | public class OrderServiceApplication {
12 |
13 |
14 | public static void main(String... args) {
15 | Quarkus.run(MyApp.class, args);
16 | }
17 |
18 | public static class MyApp implements QuarkusApplication {
19 |
20 |
21 | @Override
22 | public int run(String... args) throws Exception {
23 | Quarkus.waitForExit();
24 | return 0;
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/order/src/main/java/com/refactorizando/sample/saga/events/SeatEventConsumer.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.events;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.refactorizando.sample.saga.model.Seat;
5 | import com.refactorizando.sample.saga.service.SeatService;
6 | import io.smallrye.reactive.messaging.kafka.Record;
7 | import lombok.RequiredArgsConstructor;
8 | import lombok.SneakyThrows;
9 | import lombok.extern.slf4j.Slf4j;
10 | import org.eclipse.microprofile.reactive.messaging.Incoming;
11 |
12 | import javax.enterprise.context.ApplicationScoped;
13 |
14 | @ApplicationScoped
15 | @RequiredArgsConstructor
16 | @Slf4j
17 | public class SeatEventConsumer {
18 |
19 | private final SeatService seatService;
20 |
21 | @SneakyThrows
22 | @Incoming("seats-in")
23 | public void receive(Record record) {
24 |
25 | log.info("record es: {}", record.key());
26 |
27 | ObjectMapper objectMapper = new ObjectMapper();
28 |
29 | var seat = objectMapper.readValue(record.value(), Seat.class);
30 |
31 | if (null != seat.getType() && seat.getType().equalsIgnoreCase("compensation")) {
32 |
33 | seatService.unlockSeat(seat.getId());
34 |
35 | } else {
36 |
37 | seatService.busySeat(seat.getId());
38 | }
39 |
40 | }
41 |
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/order/src/main/java/com/refactorizando/sample/saga/events/SeatEventProducer.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.events;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.refactorizando.sample.saga.model.Seat;
5 | import io.smallrye.reactive.messaging.kafka.Record;
6 | import lombok.SneakyThrows;
7 | import lombok.extern.slf4j.Slf4j;
8 | import org.eclipse.microprofile.reactive.messaging.Channel;
9 | import org.eclipse.microprofile.reactive.messaging.Emitter;
10 |
11 | import javax.enterprise.context.ApplicationScoped;
12 | import javax.inject.Inject;
13 |
14 | @ApplicationScoped
15 | @Slf4j
16 | public class SeatEventProducer {
17 |
18 |
19 | @Inject
20 | @Channel("seats-out")
21 | Emitter> emitter;
22 |
23 | @SneakyThrows
24 | public void sendOrder(Seat seat) {
25 |
26 | log.info("Event sent {}", seat.getId());
27 |
28 | ObjectMapper objectMapper = new ObjectMapper();
29 |
30 | var seatJson = objectMapper.writeValueAsString(seat);
31 |
32 | emitter.send(Record.of(seat.getId(), seatJson))
33 | .whenComplete((success, failure) -> {
34 | if (failure != null) {
35 | log.error("D'oh! " + failure.getMessage());
36 | } else {
37 | log.info("Message processed successfully");
38 | }
39 | });
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/order/src/main/java/com/refactorizando/sample/saga/model/Seat.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 | import lombok.Getter;
5 | import lombok.Setter;
6 | import lombok.ToString;
7 |
8 | import javax.persistence.*;
9 |
10 | @Entity
11 | @Getter
12 | @Setter
13 | @ToString
14 | @JsonIgnoreProperties(ignoreUnknown = true)
15 | public class Seat {
16 |
17 | @Id
18 | @GeneratedValue(strategy = GenerationType.AUTO)
19 | private Long id;
20 |
21 | @Transient
22 | private String type;
23 |
24 | private Integer column;
25 |
26 | private Integer row;
27 |
28 | private String ref;
29 |
30 | private String status;
31 |
32 | @OneToOne(cascade = CascadeType.ALL)
33 | @JoinColumn(name = "user_id", referencedColumnName = "id")
34 | private User user;
35 | }
36 |
--------------------------------------------------------------------------------
/order/src/main/java/com/refactorizando/sample/saga/model/User.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.model;
2 |
3 | import java.time.LocalDate;
4 | import java.util.List;
5 |
6 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
7 | import lombok.Getter;
8 | import lombok.NoArgsConstructor;
9 | import lombok.Setter;
10 | import lombok.ToString;
11 |
12 | import javax.persistence.*;
13 | import javax.validation.constraints.NotNull;
14 |
15 | @Getter
16 | @Setter
17 | @NoArgsConstructor
18 | @Entity
19 | @ToString
20 | @JsonIgnoreProperties(ignoreUnknown = true)
21 | public class User {
22 |
23 | @Id
24 | @GeneratedValue(strategy = GenerationType.AUTO)
25 | private Long id;
26 |
27 | @NotNull
28 | private String name;
29 |
30 | private String surname;
31 |
32 | private LocalDate birth;
33 |
34 | private String email;
35 |
36 | @OneToOne
37 | private Seat seat;
38 |
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/order/src/main/java/com/refactorizando/sample/saga/repository/SeatRepository.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.repository;
2 |
3 | import com.refactorizando.sample.saga.model.Seat;
4 | import io.quarkus.hibernate.orm.panache.PanacheRepository;
5 |
6 | import javax.enterprise.context.ApplicationScoped;
7 |
8 | @ApplicationScoped
9 | public class SeatRepository implements PanacheRepository {
10 |
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/order/src/main/java/com/refactorizando/sample/saga/repository/UserRepository.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.repository;
2 |
3 | import com.refactorizando.sample.saga.model.Seat;
4 | import com.refactorizando.sample.saga.model.User;
5 | import io.quarkus.hibernate.orm.panache.PanacheRepository;
6 |
7 | import javax.enterprise.context.ApplicationScoped;
8 |
9 | @ApplicationScoped
10 | public class UserRepository implements PanacheRepository {
11 |
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/order/src/main/java/com/refactorizando/sample/saga/service/SeatService.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.service;
2 |
3 | import com.refactorizando.sample.saga.model.Seat;
4 | import com.refactorizando.sample.saga.repository.SeatRepository;
5 | import lombok.RequiredArgsConstructor;
6 | import lombok.extern.slf4j.Slf4j;
7 |
8 | import javax.enterprise.context.ApplicationScoped;
9 | import javax.transaction.Transactional;
10 |
11 | import static javax.transaction.Transactional.TxType.REQUIRES_NEW;
12 |
13 | @ApplicationScoped
14 | @RequiredArgsConstructor
15 | @Slf4j
16 | public class SeatService {
17 |
18 | private final SeatRepository seatRepository;
19 |
20 | @Transactional(REQUIRES_NEW)
21 | public Seat lockSeat(Long id) {
22 |
23 | log.info("Update seat with id", id);
24 |
25 | seatRepository.update("status = 'LOCKED' where id = ?1", id);
26 |
27 | return seatRepository.findById(id);
28 | }
29 |
30 | @Transactional(REQUIRES_NEW)
31 | public Seat unlockSeat (Long id) {
32 |
33 | log.info("Update seat with id", id);
34 |
35 | seatRepository.update("status = 'FREE' where id = ?1", id);
36 |
37 | return seatRepository.findById(id);
38 | }
39 |
40 | @Transactional(REQUIRES_NEW)
41 | public Seat busySeat (Long id) {
42 |
43 | log.info("Update seat with id", id);
44 |
45 | seatRepository.update("status = 'BUSY' where id = ?1", id);
46 |
47 | return seatRepository.findById(id);
48 | }
49 |
50 | public Seat findById(Long id) {
51 |
52 | return seatRepository.findById(id);
53 | }
54 |
55 | public Seat findFree() {
56 |
57 | return seatRepository.find("status= 'FREE' ").firstResult();
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/order/src/main/java/com/refactorizando/sample/saga/usecase/ReservedSeat.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.usecase;
2 |
3 | import com.refactorizando.sample.saga.events.SeatEventProducer;
4 | import com.refactorizando.sample.saga.model.Seat;
5 | import com.refactorizando.sample.saga.repository.UserRepository;
6 | import com.refactorizando.sample.saga.service.SeatService;
7 | import lombok.RequiredArgsConstructor;
8 | import lombok.extern.slf4j.Slf4j;
9 |
10 | import javax.enterprise.context.ApplicationScoped;
11 | import javax.inject.Inject;
12 |
13 | @ApplicationScoped
14 | @RequiredArgsConstructor
15 | @Slf4j
16 | public class ReservedSeat {
17 |
18 | @Inject
19 | private final SeatService seatService;
20 |
21 | @Inject
22 | private SeatEventProducer seatEventProducer;
23 |
24 | private final UserRepository userRepository;
25 |
26 | public Seat reservedSeat(Seat seat) {
27 |
28 | log.info("Update seat {}", seat.getId());
29 |
30 | var seatToSave = seatService.lockSeat(seat.getId());
31 |
32 | seatEventProducer.sendOrder(seatToSave);
33 |
34 | return seatToSave;
35 | }
36 |
37 |
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/order/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | quarkus.http.port=8090
2 |
3 | #Configuring database
4 | quarkus.application.name=order-service
5 | quarkus.datasource.db-kind=h2
6 | quarkus.datasource.username=sa
7 | quarkus.datasource.password=password
8 | quarkus.datasource.jdbc.url=jdbc:h2:mem:testdb
9 | quarkus.hibernate-orm.database.generation=drop-and-create
10 | quarkus.hibernate-orm.log.sql=true
11 |
12 |
13 | # The Kafka broker location (defaults to localhost:9092)
14 | kafka.bootstrap.servers=localhost:9092
15 |
16 | # Configuring the incoming channel (reading from Kafka)
17 | mp.messaging.incoming.seats-in.connector=smallrye-kafka
18 | mp.messaging.incoming.seats-in.topic=status-update
19 | mp.messaging.incoming.seats-in.key.deserializer=org.apache.kafka.common.serialization.LongDeserializer
20 | mp.messaging.incoming.seats-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
21 |
22 | # Configuring the outgoing channel (writing to Kafka)
23 | mp.messaging.outgoing.seats-out.connector=smallrye-kafka
24 | mp.messaging.outgoing.seats-out.topic=seats
25 | mp.messaging.outgoing.seats-out.key.serializer=org.apache.kafka.common.serialization.LongSerializer
26 | mp.messaging.outgoing.seats-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer
27 |
28 | quarkus.log.level=INFO
29 | quarkus.hibernate-orm.sql-load-script=load-data.sql
30 |
--------------------------------------------------------------------------------
/order/src/main/resources/load-data.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO USER(id, name, surname, email) VALUES(1, 'noel', 'rodriguez', 'refactorizando.web@gmail.com');
2 | INSERT INTO SEAT(id, column, row, status, user_id) VALUES(1, 1, 1, 'FREE',1);
3 |
4 |
5 |
--------------------------------------------------------------------------------
/order/src/test/java/com/refactorizando/sample/saga/NativeOrderSeatResourceIT.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga;
2 |
3 | import io.quarkus.test.junit.NativeImageTest;
4 |
5 | //@NativeImageTest
6 | public class NativeOrderSeatResourceIT extends OrderSeatResourceTest {
7 |
8 | // Execute the same tests but in native mode.
9 | }
--------------------------------------------------------------------------------
/order/src/test/java/com/refactorizando/sample/saga/OrderSeatResourceTest.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga;
2 |
3 | import io.quarkus.test.junit.QuarkusTest;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import static io.restassured.RestAssured.given;
7 | import static org.hamcrest.CoreMatchers.is;
8 |
9 | //@QuarkusTest
10 | public class OrderSeatResourceTest {
11 |
12 |
13 | }
--------------------------------------------------------------------------------
/payment/.dockerignore:
--------------------------------------------------------------------------------
1 | *
2 | !target/*-runner
3 | !target/*-runner.jar
4 | !target/lib/*
5 | !target/quarkus-app/*
--------------------------------------------------------------------------------
/payment/.gitignore:
--------------------------------------------------------------------------------
1 | #Maven
2 | target/
3 | pom.xml.tag
4 | pom.xml.releaseBackup
5 | pom.xml.versionsBackup
6 | release.properties
7 |
8 | # Eclipse
9 | .project
10 | .classpath
11 | .settings/
12 | bin/
13 |
14 | # IntelliJ
15 | .idea
16 | *.ipr
17 | *.iml
18 | *.iws
19 |
20 | # NetBeans
21 | nb-configuration.xml
22 |
23 | # Visual Studio Code
24 | .vscode
25 | .factorypath
26 |
27 | # OSX
28 | .DS_Store
29 |
30 | # Vim
31 | *.swp
32 | *.swo
33 |
34 | # patch
35 | *.orig
36 | *.rej
37 |
38 | # Local environment
39 | .env
40 |
--------------------------------------------------------------------------------
/payment/README.adoc:
--------------------------------------------------------------------------------
1 | = Payment Service =
2 | {localdatetime}
3 | :toc:
4 | :doctype: book
5 | :docinfo:
6 |
7 | This microservice has born with the aim of make a new payment after lock a seat in the previous transaction.
8 |
9 | If you need more information you can take a look into:
10 | https://refactorizando.com/patron-saga-quarkus-kafka
11 |
12 | == Introduction
13 |
14 | This microservice is responsible for receiving an event and processing and saving a new payment. Then, if all goes well,
15 | a new event will be sent to change the status of the seat. Otherwise, a compensation process, and a rollback will be triggered
16 | to leave the seat in its original state (FREE).
17 |
18 |
19 | == How does it work?
20 |
21 | === Run kafka
22 | The first thing you need to do is launch a kafka broker that you can do with the following docker:
23 |
24 | docker run --rm -p 2181:2181 -p 3030:3030 -p 8081-8083:8081-8083 -p 9581-9585:9581-9585 -p 9092:9092 -e ADV_HOST=localhost landoop/fast-data-dev:latest
25 |
26 | === Run the application
27 |
28 | ==== Running the application in dev mode
29 |
30 | You can run your application in dev mode that enables live coding using:
31 | ```shell script
32 | ./mvnw compile quarkus:dev
33 | ```
34 |
35 | > **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/.
36 |
37 | ==== Packaging and running the application
38 |
39 | The application can be packaged using:
40 | ```shell script
41 | ./mvnw package
42 | ```
43 | It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory.
44 | Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory.
45 |
46 | If you want to build an _über-jar_, execute the following command:
47 | ```shell script
48 | ./mvnw package -Dquarkus.package.type=uber-jar
49 | ```
50 |
51 | The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`.
52 |
53 | ==== Creating a native executable
54 |
55 | You can create a native executable using:
56 | ```shell script
57 | ./mvnw package -Pnative
58 | ```
59 |
60 | Or, if you don't have GraalVM installed, you can run the native executable build in a container using:
61 | ```shell script
62 | ./mvnw package -Pnative -Dquarkus.native.container-build=true
63 | ```
64 |
65 | You can then execute your native executable with: `./target/quarkus-saga-1.0.0-SNAPSHOT-runner`
66 |
67 | === Test the application
68 |
69 | You can make a request to the next endpoint http://localhost:8080/payments, to check payments in your database.
70 |
--------------------------------------------------------------------------------
/payment/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 | com.refactorizando.sample.saga
6 | payment-service
7 | 1.0.0-SNAPSHOT
8 |
9 | 3.8.1
10 | true
11 | 11
12 | 11
13 | UTF-8
14 | UTF-8
15 | quarkus-universe-bom
16 | io.quarkus
17 | 2.0.1.Final
18 | 3.0.0-M5
19 |
20 |
21 |
22 |
23 | ${quarkus.platform.group-id}
24 | ${quarkus.platform.artifact-id}
25 | ${quarkus.platform.version}
26 | pom
27 | import
28 |
29 |
30 |
31 |
32 |
33 | io.quarkus
34 | quarkus-jdbc-h2
35 |
36 |
37 | io.quarkus
38 | quarkus-smallrye-reactive-messaging-kafka
39 |
40 |
41 | io.quarkus
42 | quarkus-resteasy-jackson
43 |
44 |
45 | io.quarkus
46 | quarkus-arc
47 |
48 |
49 | io.quarkus
50 | quarkus-resteasy
51 |
52 |
53 | io.quarkus
54 | quarkus-junit5
55 | test
56 |
57 |
58 | io.rest-assured
59 | rest-assured
60 | test
61 |
62 |
63 |
64 | io.quarkus
65 | quarkus-hibernate-orm-panache
66 |
67 |
68 |
69 | org.projectlombok
70 | lombok
71 | 1.18.20
72 | provided
73 |
74 |
75 |
76 | com.fasterxml.jackson.core
77 | jackson-databind
78 | 2.11.1
79 |
80 |
81 |
82 |
83 |
84 |
85 | ${quarkus.platform.group-id}
86 | quarkus-maven-plugin
87 | ${quarkus.platform.version}
88 | true
89 |
90 |
91 |
92 | build
93 | generate-code
94 | generate-code-tests
95 |
96 |
97 |
98 |
99 |
100 | maven-compiler-plugin
101 | ${compiler-plugin.version}
102 |
103 | ${maven.compiler.parameters}
104 |
105 |
106 |
107 | maven-surefire-plugin
108 | ${surefire-plugin.version}
109 |
110 |
111 | org.jboss.logmanager.LogManager
112 | ${maven.home}
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | native
121 |
122 |
123 | native
124 |
125 |
126 |
127 |
128 |
129 | maven-failsafe-plugin
130 | ${surefire-plugin.version}
131 |
132 |
133 |
134 | integration-test
135 | verify
136 |
137 |
138 |
139 | ${project.build.directory}/${project.build.finalName}-runner
140 | org.jboss.logmanager.LogManager
141 | ${maven.home}
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | native
151 |
152 |
153 |
154 |
155 |
--------------------------------------------------------------------------------
/payment/src/main/docker/Dockerfile.jvm:
--------------------------------------------------------------------------------
1 | ####
2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
3 | #
4 | # Before building the container image run:
5 | #
6 | # ./mvnw package
7 | #
8 | # Then, build the image with:
9 | #
10 | # docker build -f src/main/docker/Dockerfile.jvm -t quarkus/quarkus-saga-jvm .
11 | #
12 | # Then run the container using:
13 | #
14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus-saga-jvm
15 | #
16 | # If you want to include the debug port into your docker image
17 | # you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005
18 | #
19 | # Then run the container using :
20 | #
21 | # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/quarkus-saga-jvm
22 | #
23 | ###
24 | FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4
25 |
26 | ARG JAVA_PACKAGE=java-11-openjdk-headless
27 | ARG RUN_JAVA_VERSION=1.3.8
28 | ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
29 | # Install java and the run-java script
30 | # Also set up permissions for user `1001`
31 | RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
32 | && microdnf update \
33 | && microdnf clean all \
34 | && mkdir /deployments \
35 | && chown 1001 /deployments \
36 | && chmod "g+rwX" /deployments \
37 | && chown 1001:root /deployments \
38 | && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
39 | && chown 1001 /deployments/run-java.sh \
40 | && chmod 540 /deployments/run-java.sh \
41 | && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/conf/security/java.security
42 |
43 | # Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.
44 | ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
45 | # We make four distinct layers so if there are application changes the library layers can be re-used
46 | COPY --chown=1001 target/quarkus-app/lib/ /deployments/lib/
47 | COPY --chown=1001 target/quarkus-app/*.jar /deployments/
48 | COPY --chown=1001 target/quarkus-app/app/ /deployments/app/
49 | COPY --chown=1001 target/quarkus-app/quarkus/ /deployments/quarkus/
50 |
51 | EXPOSE 8080
52 | USER 1001
53 |
54 | ENTRYPOINT [ "/deployments/run-java.sh" ]
55 |
56 |
--------------------------------------------------------------------------------
/payment/src/main/docker/Dockerfile.legacy-jar:
--------------------------------------------------------------------------------
1 | ####
2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
3 | #
4 | # Before building the container image run:
5 | #
6 | # ./mvnw package -Dquarkus.package.type=legacy-jar
7 | #
8 | # Then, build the image with:
9 | #
10 | # docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/quarkus-saga-legacy-jar .
11 | #
12 | # Then run the container using:
13 | #
14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus-saga-legacy-jar
15 | #
16 | # If you want to include the debug port into your docker image
17 | # you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005
18 | #
19 | # Then run the container using :
20 | #
21 | # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/quarkus-saga-legacy-jar
22 | #
23 | ###
24 | FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4
25 |
26 | ARG JAVA_PACKAGE=java-11-openjdk-headless
27 | ARG RUN_JAVA_VERSION=1.3.8
28 | ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
29 | # Install java and the run-java script
30 | # Also set up permissions for user `1001`
31 | RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
32 | && microdnf update \
33 | && microdnf clean all \
34 | && mkdir /deployments \
35 | && chown 1001 /deployments \
36 | && chmod "g+rwX" /deployments \
37 | && chown 1001:root /deployments \
38 | && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
39 | && chown 1001 /deployments/run-java.sh \
40 | && chmod 540 /deployments/run-java.sh \
41 | && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/conf/security/java.security
42 |
43 | # Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.
44 | ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
45 | COPY target/lib/* /deployments/lib/
46 | COPY target/*-runner.jar /deployments/app.jar
47 |
48 | EXPOSE 8080
49 | USER 1001
50 |
51 | ENTRYPOINT [ "/deployments/run-java.sh" ]
52 |
--------------------------------------------------------------------------------
/payment/src/main/docker/Dockerfile.native:
--------------------------------------------------------------------------------
1 | ####
2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode
3 | #
4 | # Before building the container image run:
5 | #
6 | # ./mvnw package -Pnative
7 | #
8 | # Then, build the image with:
9 | #
10 | # docker build -f src/main/docker/Dockerfile.native -t quarkus/quarkus-saga .
11 | #
12 | # Then run the container using:
13 | #
14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus-saga
15 | #
16 | ###
17 | FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4
18 | WORKDIR /work/
19 | RUN chown 1001 /work \
20 | && chmod "g+rwX" /work \
21 | && chown 1001:root /work
22 | COPY --chown=1001:root target/*-runner /work/application
23 |
24 | EXPOSE 8080
25 | USER 1001
26 |
27 | CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
28 |
--------------------------------------------------------------------------------
/payment/src/main/docker/Dockerfile.native-distroless:
--------------------------------------------------------------------------------
1 | ####
2 | # This Dockerfile is used in order to build a distroless container that runs the Quarkus application in native (no JVM) mode
3 | #
4 | # Before building the container image run:
5 | #
6 | # ./mvnw package -Pnative
7 | #
8 | # Then, build the image with:
9 | #
10 | # docker build -f src/main/docker/Dockerfile.native-distroless -t quarkus/quarkus-saga .
11 | #
12 | # Then run the container using:
13 | #
14 | # docker run -i --rm -p 8080:8080 quarkus/quarkus-saga
15 | #
16 | ###
17 | FROM quay.io/quarkus/quarkus-distroless-image:1.0
18 | COPY target/*-runner /application
19 |
20 | EXPOSE 8080
21 | USER nonroot
22 |
23 | CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
24 |
--------------------------------------------------------------------------------
/payment/src/main/java/com/refactorizando/sample/saga/PaymentApplication.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga;
2 |
3 | import io.quarkus.runtime.Quarkus;
4 | import io.quarkus.runtime.QuarkusApplication;
5 | import io.quarkus.runtime.annotations.QuarkusMain;
6 | import lombok.extern.slf4j.Slf4j;
7 |
8 |
9 | @QuarkusMain
10 | @Slf4j
11 | public class PaymentApplication {
12 |
13 |
14 | public static void main(String... args) {
15 | Quarkus.run(MyApp.class, args);
16 | }
17 |
18 | public static class MyApp implements QuarkusApplication {
19 |
20 |
21 | @Override
22 | public int run(String... args) throws Exception {
23 | Quarkus.waitForExit();
24 | return 0;
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/payment/src/main/java/com/refactorizando/sample/saga/PaymentResource.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga;
2 |
3 | import com.refactorizando.sample.saga.repository.SeatRepository;
4 | import com.refactorizando.sample.saga.repository.UserRepository;
5 | import com.refactorizando.sample.saga.service.PaymentService;
6 | import lombok.RequiredArgsConstructor;
7 | import lombok.extern.slf4j.Slf4j;
8 |
9 |
10 | import javax.ws.rs.*;
11 | import javax.ws.rs.core.MediaType;
12 | import javax.ws.rs.core.Response;
13 |
14 | @Path("/payments")
15 | @RequiredArgsConstructor
16 | @Produces(MediaType.APPLICATION_JSON)
17 | @Consumes(MediaType.APPLICATION_JSON)
18 | @Slf4j
19 | public class PaymentResource {
20 |
21 | private final PaymentService paymentService;
22 |
23 | @GET
24 | public Response getPayments() {
25 |
26 | log.info("Find All payments");
27 |
28 | return Response.status(Response.Status.OK)
29 | .entity(paymentService.findAll()).build();
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/payment/src/main/java/com/refactorizando/sample/saga/events/PaymentConsumer.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.events;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.refactorizando.sample.saga.model.Seat;
5 | import com.refactorizando.sample.saga.service.PaymentService;
6 | import com.refactorizando.sample.saga.usecase.MakePaymentUseCase;
7 | import io.smallrye.reactive.messaging.kafka.Record;
8 | import lombok.RequiredArgsConstructor;
9 | import lombok.SneakyThrows;
10 | import lombok.extern.slf4j.Slf4j;
11 | import org.eclipse.microprofile.reactive.messaging.Incoming;
12 |
13 | import javax.enterprise.context.ApplicationScoped;
14 |
15 |
16 | /**
17 | * This class receive a message with the seat that has been selected.
18 | * After recive the event this class is going to save and send a message to update the status of the seat.
19 | */
20 | @ApplicationScoped
21 | @RequiredArgsConstructor
22 | @Slf4j
23 | public class PaymentConsumer {
24 |
25 | private final MakePaymentUseCase makePaymentUseCase;
26 |
27 | @SneakyThrows
28 | @Incoming("payments-in")
29 | public void receive(Record record) {
30 |
31 | log.info("record es: {}", record.key());
32 |
33 | ObjectMapper objectMapper = new ObjectMapper();
34 |
35 | var seat = objectMapper.readValue(record.value(), Seat.class);
36 |
37 | makePaymentUseCase.makeAPayment(seat);
38 |
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/payment/src/main/java/com/refactorizando/sample/saga/events/PaymentProducer.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.events;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.refactorizando.sample.saga.events.compensation.SeatEventProducer;
5 | import com.refactorizando.sample.saga.model.Payment;
6 | import com.refactorizando.sample.saga.usecase.DeletePaymentUseCase;
7 | import io.smallrye.reactive.messaging.kafka.Record;
8 | import lombok.RequiredArgsConstructor;
9 | import lombok.SneakyThrows;
10 | import lombok.extern.slf4j.Slf4j;
11 | import org.eclipse.microprofile.reactive.messaging.Channel;
12 | import org.eclipse.microprofile.reactive.messaging.Emitter;
13 |
14 | import javax.enterprise.context.ApplicationScoped;
15 |
16 | @ApplicationScoped
17 | @Slf4j
18 | @RequiredArgsConstructor
19 | public class PaymentProducer {
20 |
21 | @Channel("payments-out")
22 | Emitter> emitter;
23 |
24 | private final SeatEventProducer seatEventProducer;
25 |
26 | private final DeletePaymentUseCase deletePaymentUseCase;
27 |
28 |
29 | @SneakyThrows
30 | public void sendPaymentEvent(Payment payment) {
31 | log.info("Event sent {}", payment.getId());
32 |
33 | ObjectMapper objectMapper = new ObjectMapper();
34 |
35 | var paymentJson = objectMapper.writeValueAsString(payment);
36 |
37 | emitter.send(Record.of(payment.getId(), paymentJson))
38 | .whenComplete((success, failure) -> {
39 | if (failure != null) {
40 | log.error("D'oh! " + failure.getMessage());
41 |
42 | seatEventProducer.sendSeatEvent(payment.getSeat());
43 |
44 | deletePaymentUseCase.deletePayment(payment.getId());
45 |
46 | } else {
47 |
48 | log.info("Message processed successfully");
49 |
50 | }
51 | });
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/payment/src/main/java/com/refactorizando/sample/saga/events/compensation/PaymentConsumerCompensation.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.events.compensation;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.refactorizando.sample.saga.model.Payment;
5 | import com.refactorizando.sample.saga.usecase.DeletePaymentUseCase;
6 | import io.smallrye.reactive.messaging.kafka.Record;
7 | import lombok.RequiredArgsConstructor;
8 | import lombok.SneakyThrows;
9 | import lombok.extern.slf4j.Slf4j;
10 | import org.eclipse.microprofile.reactive.messaging.Incoming;
11 |
12 | import javax.enterprise.context.ApplicationScoped;
13 |
14 | @ApplicationScoped
15 | @RequiredArgsConstructor
16 | @Slf4j
17 | public class PaymentConsumerCompensation {
18 |
19 | private final DeletePaymentUseCase deletePaymentUseCase;
20 |
21 | private final SeatEventProducer seatEventProducer;
22 |
23 | @SneakyThrows
24 | @Incoming("allocate-in")
25 | public void receive(Record record) {
26 |
27 | log.info("Payment compensation with key {}", record.key());
28 |
29 | ObjectMapper objectMapper = new ObjectMapper();
30 |
31 | var payment = objectMapper.readValue(record.value(), Payment.class);
32 |
33 | deletePaymentUseCase.deletePayment(record.key());
34 |
35 | seatEventProducer.sendSeatEvent(payment.getSeat());
36 |
37 | }
38 | }
--------------------------------------------------------------------------------
/payment/src/main/java/com/refactorizando/sample/saga/events/compensation/SeatEventProducer.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.events.compensation;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.refactorizando.sample.saga.model.Seat;
5 | import io.smallrye.reactive.messaging.kafka.Record;
6 | import lombok.RequiredArgsConstructor;
7 | import lombok.SneakyThrows;
8 | import lombok.extern.slf4j.Slf4j;
9 | import org.eclipse.microprofile.reactive.messaging.Channel;
10 | import org.eclipse.microprofile.reactive.messaging.Emitter;
11 |
12 | import javax.enterprise.context.ApplicationScoped;
13 | import javax.inject.Inject;
14 |
15 | @ApplicationScoped
16 | @RequiredArgsConstructor
17 | @Slf4j
18 | public class SeatEventProducer {
19 |
20 | @Inject
21 | @Channel("seats-out")
22 | Emitter> emitter;
23 |
24 | @SneakyThrows
25 | public void sendSeatEvent(Seat seat) {
26 | log.info("Sent event to order service to compensate with seatId {}", seat.getId());
27 |
28 | seat.setType("compensation");
29 |
30 | ObjectMapper objectMapper = new ObjectMapper();
31 |
32 | var seatJson = objectMapper.writeValueAsString(seat);
33 |
34 | emitter.send(Record.of(seat.getId(), seatJson))
35 | .whenComplete((success, failure) -> {
36 | if (failure != null) {
37 | log.error("D'oh! " + failure.getMessage());
38 | } else {
39 | log.info("Message processed successfully");
40 | }
41 | });
42 | }
43 | }
--------------------------------------------------------------------------------
/payment/src/main/java/com/refactorizando/sample/saga/model/Payment.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.model;
2 |
3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize;
5 | import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
6 | import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
7 | import lombok.Getter;
8 | import lombok.Setter;
9 | import lombok.ToString;
10 | import org.hibernate.annotations.SQLDelete;
11 |
12 | import javax.persistence.*;
13 | import java.math.BigDecimal;
14 | import java.time.LocalDate;
15 |
16 | @Getter
17 | @Setter
18 | @Entity
19 | @SQLDelete(sql = "UPDATE Payment SET status=deleted WHERE id = ?")
20 | @ToString
21 | public class Payment {
22 |
23 | @Id
24 | @GeneratedValue(strategy = GenerationType.IDENTITY)
25 | private Long id;
26 |
27 | private String status;
28 |
29 | @OneToOne
30 | private User user;
31 |
32 | @OneToOne
33 | private Seat seat;
34 |
35 | @JsonDeserialize(using = LocalDateDeserializer.class)
36 | @JsonSerialize(using = LocalDateSerializer.class)
37 | private LocalDate date;
38 |
39 | private BigDecimal amount;
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/payment/src/main/java/com/refactorizando/sample/saga/model/Seat.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.model;
2 |
3 | import lombok.Getter;
4 | import lombok.Setter;
5 | import lombok.ToString;
6 |
7 | import javax.persistence.*;
8 | import java.net.URL;
9 | import java.time.LocalDate;
10 | import java.util.List;
11 |
12 | @Entity
13 | @Getter
14 | @Setter
15 | @ToString
16 | public class Seat {
17 |
18 | @Id
19 | @GeneratedValue(strategy = GenerationType.IDENTITY)
20 | private Long id;
21 |
22 | @Transient
23 | private String type;
24 |
25 | private Integer column;
26 |
27 | private Integer row;
28 |
29 | private String ref;
30 |
31 | private String status;
32 |
33 | @OneToOne
34 | private User user;
35 |
36 | @OneToOne
37 | private Payment payment;
38 | }
39 |
--------------------------------------------------------------------------------
/payment/src/main/java/com/refactorizando/sample/saga/model/User.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.model;
2 |
3 | import java.time.LocalDate;
4 | import java.util.List;
5 |
6 | import lombok.Getter;
7 | import lombok.NoArgsConstructor;
8 | import lombok.Setter;
9 | import lombok.ToString;
10 |
11 | import javax.persistence.*;
12 | import javax.validation.constraints.NotNull;
13 |
14 | @Getter
15 | @Setter
16 | @NoArgsConstructor
17 | @Entity
18 | @ToString
19 | public class User {
20 |
21 | @Id
22 | @GeneratedValue(strategy = GenerationType.IDENTITY)
23 | private Long id;
24 |
25 | @NotNull
26 | private String name;
27 |
28 | private String surname;
29 |
30 | private LocalDate birth;
31 |
32 | private String email;
33 |
34 | @OneToOne
35 | private Seat seat;
36 |
37 | @OneToOne
38 | private Payment payment;
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/payment/src/main/java/com/refactorizando/sample/saga/repository/PaymentRepository.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.repository;
2 |
3 | import com.refactorizando.sample.saga.model.Payment;
4 | import io.quarkus.hibernate.orm.panache.PanacheRepository;
5 |
6 | import javax.enterprise.context.ApplicationScoped;
7 |
8 | @ApplicationScoped
9 | public class PaymentRepository implements PanacheRepository {
10 | }
11 |
--------------------------------------------------------------------------------
/payment/src/main/java/com/refactorizando/sample/saga/repository/SeatRepository.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.repository;
2 |
3 | import com.refactorizando.sample.saga.model.Seat;
4 | import io.quarkus.hibernate.orm.panache.PanacheRepository;
5 |
6 | import javax.enterprise.context.ApplicationScoped;
7 |
8 | @ApplicationScoped
9 | public class SeatRepository implements PanacheRepository {
10 |
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/payment/src/main/java/com/refactorizando/sample/saga/repository/UserRepository.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.repository;
2 |
3 | import com.refactorizando.sample.saga.model.Seat;
4 | import com.refactorizando.sample.saga.model.User;
5 | import io.quarkus.hibernate.orm.panache.PanacheRepository;
6 |
7 | import javax.enterprise.context.ApplicationScoped;
8 |
9 | @ApplicationScoped
10 | public class UserRepository implements PanacheRepository {
11 |
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/payment/src/main/java/com/refactorizando/sample/saga/service/PaymentService.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.service;
2 |
3 | import com.refactorizando.sample.saga.events.compensation.SeatEventProducer;
4 | import com.refactorizando.sample.saga.model.Payment;
5 | import com.refactorizando.sample.saga.repository.PaymentRepository;
6 | import com.refactorizando.sample.saga.repository.SeatRepository;
7 | import lombok.RequiredArgsConstructor;
8 | import lombok.extern.slf4j.Slf4j;
9 |
10 | import javax.enterprise.context.ApplicationScoped;
11 | import javax.transaction.Transactional;
12 | import java.util.List;
13 | import java.util.stream.Collectors;
14 |
15 | @ApplicationScoped
16 | @RequiredArgsConstructor
17 | @Slf4j
18 | public class PaymentService {
19 |
20 | private final PaymentRepository paymentRepository;
21 |
22 | private final SeatEventProducer seatEventProducer;
23 |
24 | private final SeatRepository seatRepository;
25 |
26 | @Transactional
27 | public Payment savePayment(Payment payment) {
28 |
29 | paymentRepository.persist(payment);
30 |
31 | return payment;
32 | }
33 |
34 | public void deletePayment(Long paymentId) {
35 |
36 | paymentRepository.deleteById(paymentId);
37 | //Refund money to user
38 | }
39 |
40 | public Payment findById(Long id) {
41 |
42 | return paymentRepository.findById(id);
43 | }
44 |
45 | public List findAll() {
46 |
47 | return paymentRepository.findAll().stream().collect(Collectors.toList());
48 |
49 | }
50 |
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/payment/src/main/java/com/refactorizando/sample/saga/service/SeatService.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.service;
2 |
3 | import com.refactorizando.sample.saga.events.PaymentProducer;
4 | import com.refactorizando.sample.saga.model.Seat;
5 | import com.refactorizando.sample.saga.repository.SeatRepository;
6 | import lombok.RequiredArgsConstructor;
7 | import lombok.extern.slf4j.Slf4j;
8 |
9 | import javax.enterprise.context.ApplicationScoped;
10 | import javax.inject.Inject;
11 | import javax.transaction.Transactional;
12 |
13 | import static javax.transaction.Transactional.TxType.REQUIRES_NEW;
14 |
15 | @ApplicationScoped
16 | @RequiredArgsConstructor
17 | @Slf4j
18 | public class SeatService {
19 |
20 | @Inject
21 | private final SeatRepository seatRepository;
22 |
23 | @Inject
24 | private final PaymentProducer paymentProducer;
25 |
26 | @Transactional(REQUIRES_NEW)
27 | public Seat blockSeat(Seat seat) {
28 |
29 | log.info("Block a seat ", seat.toString());
30 |
31 | seat.setStatus("BLOCKED");
32 |
33 | seatRepository.persistAndFlush(seat);
34 |
35 | return seat;
36 | }
37 |
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/payment/src/main/java/com/refactorizando/sample/saga/usecase/DeletePaymentUseCase.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.usecase;
2 |
3 | import com.refactorizando.sample.saga.events.compensation.SeatEventProducer;
4 | import com.refactorizando.sample.saga.service.PaymentService;
5 | import lombok.RequiredArgsConstructor;
6 |
7 | import javax.enterprise.context.ApplicationScoped;
8 |
9 | @RequiredArgsConstructor
10 | @ApplicationScoped
11 | public class DeletePaymentUseCase {
12 |
13 | private final PaymentService paymentService;
14 |
15 | private final SeatEventProducer seatEventProducer;
16 |
17 | public void deletePayment(Long paymentId) {
18 |
19 | try {
20 |
21 | paymentService.deletePayment(paymentId);
22 |
23 | } catch (Exception ex) {
24 |
25 | seatEventProducer.sendSeatEvent(paymentService.findById(paymentId).getSeat());
26 |
27 | }
28 | //Refund money to user
29 |
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/payment/src/main/java/com/refactorizando/sample/saga/usecase/MakePaymentUseCase.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga.usecase;
2 |
3 | import com.refactorizando.sample.saga.events.PaymentProducer;
4 | import com.refactorizando.sample.saga.events.compensation.SeatEventProducer;
5 | import com.refactorizando.sample.saga.model.Payment;
6 | import com.refactorizando.sample.saga.model.Seat;
7 | import com.refactorizando.sample.saga.service.PaymentService;
8 | import lombok.RequiredArgsConstructor;
9 | import lombok.extern.slf4j.Slf4j;
10 |
11 | import javax.enterprise.context.ApplicationScoped;
12 | import java.math.BigDecimal;
13 | import java.time.LocalDate;
14 |
15 | @ApplicationScoped
16 | @RequiredArgsConstructor
17 | @Slf4j
18 | public class MakePaymentUseCase {
19 |
20 | private final PaymentService paymentService;
21 |
22 | private final PaymentProducer paymentProducer;
23 |
24 | private final SeatEventProducer seatEventProducer;
25 |
26 | public Payment makeAPayment(Seat seat) {
27 |
28 | log.info("Create payment with seat {}", seat.getId());
29 |
30 | var payment = createPayment(seat);
31 |
32 | try {
33 |
34 | payment.setStatus("PAID");
35 | paymentService.savePayment(payment);
36 |
37 | }catch (Exception ex) {
38 |
39 | seatEventProducer.sendSeatEvent(payment.getSeat());
40 |
41 | return payment;
42 | }
43 |
44 | paymentProducer.sendPaymentEvent(payment);
45 |
46 | return payment;
47 | }
48 |
49 | private Payment createPayment(Seat seat) {
50 |
51 | Payment payment = new Payment();
52 |
53 | payment.setStatus("PAID");
54 | payment.setAmount(new BigDecimal(10));
55 | payment.setSeat(seat);
56 | payment.setUser(seat.getUser());
57 | payment.setDate(LocalDate.now());
58 |
59 | return payment;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/payment/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | #Configuring database
2 | quarkus.application.name=order-service
3 | quarkus.datasource.db-kind=h2
4 | quarkus.datasource.username=sa
5 | quarkus.datasource.password=password
6 | quarkus.datasource.jdbc.url=jdbc:h2:mem:testdb
7 | quarkus.hibernate-orm.database.generation=drop-and-create
8 | quarkus.hibernate-orm.log.sql=true
9 |
10 |
11 | # The Kafka broker location (defaults to localhost:9092)
12 | kafka.bootstrap.servers=localhost:9092
13 |
14 | # Configuring the incoming channel (reading from Kafka)
15 | mp.messaging.outgoing.payments-out.connector=smallrye-kafka
16 | mp.messaging.outgoing.payments-out.topic=payment
17 | mp.messaging.outgoing.payments-out.key.serializer=org.apache.kafka.common.serialization.LongSerializer
18 | mp.messaging.outgoing.payments-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer
19 |
20 | # Configuring the outgoing channel (writing to Kafka)
21 | mp.messaging.outgoing.seats-out.connector=smallrye-kafka
22 | mp.messaging.outgoing.seats-out.topic=status-update
23 | mp.messaging.outgoing.seats-out.key.serializer=org.apache.kafka.common.serialization.LongSerializer
24 | mp.messaging.outgoing.seats-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer
25 |
26 | # Configuring compensation
27 | mp.messaging.incoming.allocate-in.connector=smallrye-kafka
28 | mp.messaging.incoming.allocate-in.topic=payment-update
29 | mp.messaging.incoming.allocate-in.key.deserializer=org.apache.kafka.common.serialization.LongDeserializer
30 | mp.messaging.incoming.allocate-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
31 |
32 | # Configuring the incoming channel (reading from Kafka)
33 | mp.messaging.incoming.payments-in.connector=smallrye-kafka
34 | mp.messaging.incoming.payments-in.topic=seats
35 | mp.messaging.incoming.payments-in.key.deserializer=org.apache.kafka.common.serialization.LongDeserializer
36 | mp.messaging.incoming.payments-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
37 |
38 | quarkus.log.level=INFO
39 | quarkus.hibernate-orm.sql-load-script=load-data.sql
40 |
--------------------------------------------------------------------------------
/payment/src/main/resources/load-data.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO USER(id, name, surname, email) VALUES(1, 'noel', 'rodriguez', 'refactorizando.web@gmail.com');
2 | INSERT INTO SEAT(id, column, row, status, user_id) VALUES(1, 1, 1, 'FREE',1);
3 |
4 |
5 |
--------------------------------------------------------------------------------
/payment/src/test/java/com/refactorizando/sample/saga/NativeOrderSeatResourceIT.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga;
2 |
3 | import io.quarkus.test.junit.NativeImageTest;
4 |
5 | //@NativeImageTest
6 | public class NativeOrderSeatResourceIT extends OrderSeatResourceTest {
7 |
8 | // Execute the same tests but in native mode.
9 | }
--------------------------------------------------------------------------------
/payment/src/test/java/com/refactorizando/sample/saga/OrderSeatResourceTest.java:
--------------------------------------------------------------------------------
1 | package com.refactorizando.sample.saga;
2 |
3 | import io.quarkus.test.junit.QuarkusTest;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import static io.restassured.RestAssured.given;
7 | import static org.hamcrest.CoreMatchers.is;
8 |
9 | //@QuarkusTest
10 | public class OrderSeatResourceTest {
11 |
12 |
13 | }
--------------------------------------------------------------------------------