├── 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 | } --------------------------------------------------------------------------------