├── .github └── workflows │ ├── build.yml │ └── deploy.yml ├── .gitignore ├── README.md ├── amqp ├── pom.xml └── src │ └── main │ ├── java │ └── dev │ │ └── nano │ │ └── amqp │ │ ├── RabbitMQConfig.java │ │ └── RabbitMQProducer.java │ └── resources │ ├── amqp-default.properties │ ├── amqp-docker.properties │ ├── amqp-eks.properties │ └── amqp-kube.properties ├── apiKey-manager ├── pom.xml └── src │ └── main │ ├── java │ └── dev │ │ └── nano │ │ ├── ApiKeyManagerApplication.java │ │ ├── apikey │ │ ├── ApiKeyConstant.java │ │ ├── ApiKeyEntity.java │ │ ├── ApiKeyRepository.java │ │ ├── ApiKeyRequest.java │ │ ├── ApiKeyService.java │ │ ├── ApiKeyServiceImpl.java │ │ ├── KeyGenerator.java │ │ └── UUIDKeyGeneratorImpl.java │ │ ├── application │ │ ├── ApplicationConstant.java │ │ ├── ApplicationEntity.java │ │ ├── ApplicationName.java │ │ ├── ApplicationRepository.java │ │ ├── ApplicationService.java │ │ └── ApplicationServiceImpl.java │ │ └── controller │ │ ├── ApiKeyController.java │ │ └── ApplicationController.java │ └── resources │ ├── application.yml │ ├── banner.txt │ └── db │ └── data.sql ├── common ├── pom.xml └── src │ └── main │ ├── java │ ├── cache │ │ └── CacheConfig.java │ ├── exceptionhandler │ │ ├── business │ │ │ ├── CustomerException.java │ │ │ ├── NotificationException.java │ │ │ ├── OrderException.java │ │ │ ├── PaymentException.java │ │ │ └── ProductException.java │ │ ├── core │ │ │ ├── BadRequestException.java │ │ │ ├── BaseException.java │ │ │ ├── DuplicateResourceException.java │ │ │ ├── ResourceNotFoundException.java │ │ │ └── ValidationException.java │ │ ├── handler │ │ │ └── RestResponseEntityExceptionHandler.java │ │ └── payload │ │ │ ├── ErrorCode.java │ │ │ ├── ErrorDetails.java │ │ │ └── ValidationError.java │ └── swagger │ │ ├── BaseController.java │ │ ├── OpenAPIConfig.java │ │ └── OpenAPIProperties.java │ └── resources │ ├── application-openapi.yml │ ├── shared-application-default.yml │ ├── shared-application-docker.yml │ ├── shared-application-eks.yml │ └── shared-application-kube.yml ├── customer ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── pom.xml └── src │ └── main │ ├── java │ └── dev │ │ └── nano │ │ └── customer │ │ ├── CustomerApplication.java │ │ ├── CustomerConstant.java │ │ ├── CustomerController.java │ │ ├── CustomerDTO.java │ │ ├── CustomerEntity.java │ │ ├── CustomerMapper.java │ │ ├── CustomerRepository.java │ │ ├── CustomerService.java │ │ └── CustomerServiceImpl.java │ └── resources │ ├── application.yml │ ├── banner.txt │ └── db │ └── data.sql ├── docker ├── compose │ ├── docker-compose-local.yml │ └── docker-compose.yml └── config │ └── prometheus │ └── prometheus.yml ├── docs └── diagrams │ ├── architecture-diagram.png │ ├── deploy-workflow-diagram.png │ └── infrastructure-diagram.png ├── eureka-server ├── pom.xml └── src │ └── main │ ├── java │ └── dev │ │ └── nano │ │ └── eurekaserver │ │ └── EurekaServerApplication.java │ └── resources │ ├── application-docker.yml │ ├── application.yml │ └── banner.txt ├── feign-clients ├── pom.xml └── src │ └── main │ ├── java │ └── dev │ │ └── nano │ │ └── clients │ │ ├── apiKeyManager │ │ ├── ApplicationName.java │ │ ├── apiKey │ │ │ ├── ApiKeyManagerClient.java │ │ │ └── ApiKeyManagerResponse.java │ │ └── application │ │ │ └── ApplicationKeyManagerClient.java │ │ ├── notification │ │ ├── NotificationClient.java │ │ ├── NotificationRequest.java │ │ └── NotificationResponse.java │ │ ├── order │ │ ├── OrderClient.java │ │ ├── OrderRequest.java │ │ └── OrderResponse.java │ │ ├── payment │ │ ├── PaymentClient.java │ │ ├── PaymentRequest.java │ │ └── PaymentResponse.java │ │ └── product │ │ ├── ProductClient.java │ │ └── ProductResponse.java │ └── resources │ ├── clients-default.properties │ ├── clients-docker.properties │ ├── clients-eks.properties │ └── clients-kube.properties ├── gateway ├── pom.xml └── src │ └── main │ ├── java │ └── dev │ │ └── nano │ │ └── gateway │ │ ├── GatewayApplication.java │ │ ├── GatewayConfig.java │ │ └── security │ │ ├── ApiAuthorizationFilter.java │ │ ├── ApiKeyAuthorizationChecker.java │ │ └── ApiKeyManagerAuthorizationChecker.java │ └── resources │ ├── application.yml │ └── banner.txt ├── k8s ├── aws-eks │ ├── bootstrap │ │ ├── prometheus │ │ │ ├── prometheus.yml │ │ │ └── service.yml │ │ ├── rabbitmq │ │ │ ├── configmap.yml │ │ │ ├── rbac.yml │ │ │ ├── service.yml │ │ │ └── statefulset.yml │ │ └── zipkin │ │ │ ├── service.yml │ │ │ └── statefulset.yml │ └── services │ │ ├── customer │ │ ├── deployment.yml │ │ ├── service.yml │ │ └── servicemonitor.yml │ │ ├── notification │ │ ├── deployment.yml │ │ ├── service.yml │ │ └── servicemonitor.yml │ │ ├── order │ │ ├── deployment.yml │ │ ├── service.yml │ │ └── servicemonitor.yml │ │ ├── payment │ │ ├── deployment.yml │ │ ├── service.yml │ │ └── servicemonitor.yml │ │ └── product │ │ ├── deployment.yml │ │ ├── service.yml │ │ └── servicemonitor.yml └── minikube │ ├── bootstrap │ ├── ingress │ │ ├── ingress.yml │ │ └── readme.MD │ ├── postgres │ │ ├── configmap.yml │ │ ├── service.yml │ │ ├── statefulset.yml │ │ └── volume.yml │ ├── rabbitmq │ │ ├── configmap.yml │ │ ├── rbac.yml │ │ ├── service.yml │ │ └── statefulset.yml │ └── zipkin │ │ ├── service.yml │ │ └── statefulset.yml │ └── services │ ├── customer │ ├── deployment.yml │ └── service.yml │ ├── notification │ ├── deployment.yml │ └── service.yml │ ├── order │ ├── deployment.yml │ └── service.yml │ ├── payment │ ├── deployment.yml │ └── service.yml │ └── product │ ├── deployment.yml │ └── service.yml ├── notification ├── pom.xml └── src │ └── main │ ├── java │ └── dev │ │ └── nano │ │ └── notification │ │ ├── NotificationApplication.java │ │ ├── NotificationConstant.java │ │ ├── NotificationController.java │ │ ├── NotificationDTO.java │ │ ├── NotificationEntity.java │ │ ├── NotificationMapper.java │ │ ├── NotificationRepository.java │ │ ├── NotificationService.java │ │ ├── NotificationServiceImpl.java │ │ ├── aws │ │ ├── AWSConfig.java │ │ ├── AWSConstant.java │ │ └── AWSEmailService.java │ │ ├── email │ │ ├── DefaultEmailService.java │ │ └── EmailService.java │ │ └── rabbitmq │ │ └── NotificationConsumer.java │ └── resources │ ├── application.yml │ ├── banner.txt │ └── db │ └── data.sql ├── order ├── pom.xml └── src │ └── main │ ├── java │ └── dev │ │ └── nano │ │ └── order │ │ ├── OrderApplication.java │ │ ├── OrderConstant.java │ │ ├── OrderController.java │ │ ├── OrderDTO.java │ │ ├── OrderEntity.java │ │ ├── OrderMapper.java │ │ ├── OrderRepository.java │ │ ├── OrderService.java │ │ └── OrderServiceImpl.java │ └── resources │ ├── application.yml │ ├── banner.txt │ └── db │ └── data.sql ├── payment ├── pom.xml └── src │ └── main │ ├── java │ └── dev │ │ └── nano │ │ └── payment │ │ ├── PaymentApplication.java │ │ ├── PaymentConstant.java │ │ ├── PaymentController.java │ │ ├── PaymentDTO.java │ │ ├── PaymentEntity.java │ │ ├── PaymentMapper.java │ │ ├── PaymentRepository.java │ │ ├── PaymentService.java │ │ └── PaymentServiceImpl.java │ └── resources │ ├── application.yml │ ├── banner.txt │ └── db │ └── data.sql ├── pom.xml └── product ├── pom.xml └── src └── main ├── java └── dev │ └── nano │ └── product │ ├── ProductApplication.java │ ├── ProductConstant.java │ ├── ProductController.java │ ├── ProductDTO.java │ ├── ProductEntity.java │ ├── ProductMapper.java │ ├── ProductRepository.java │ ├── ProductService.java │ └── ProductServiceImpl.java └── resources ├── application.yml ├── banner.txt └── db └── data.sql /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Build java microservices with maven 2 | 3 | name: Build - CI 4 | 5 | #on: 6 | # pull_request: 7 | # branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | - uses: actions/checkout@v3 15 | - name: Set UP JDK 17 16 | uses: actions/setup-java@v3 17 | with: 18 | java-version: '17' 19 | distribution: 'temurin' 20 | cache: maven 21 | 22 | - name: Maven Clean Package 23 | run: mvn clean package 24 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to AWS EKS - CD 2 | 3 | #on: 4 | # push: 5 | # branches: [ "main" ] 6 | 7 | env: 8 | # AWS Bucket Keys 9 | ACCESS_KEY_BUCKET: ${{ secrets.ACCESS_KEY_BUCKET }} 10 | SECRET_KEY_BUCKET: ${{ secrets.SECRET_KEY_BUCKET }} 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up JDK 17 19 | uses: actions/setup-java@v3 20 | with: 21 | java-version: '17' 22 | distribution: 'temurin' 23 | cache: maven 24 | 25 | - name: generate image tag 26 | id: image-tag 27 | run: echo "::set-output name=IMAGE_TAG::$(date '+%d.%m.%Y.%H.%M.%S')" 28 | 29 | - name: Login to Docker Hub 30 | uses: docker/login-action@v2 31 | with: 32 | registry: docker.io 33 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 34 | password: ${{ secrets.DOCKER_HUB_PASSWORD }} 35 | 36 | - name: maven clean package & push image to dockerhub 37 | run: mvn clean package 38 | -P jib-build-push-image-to-dockerhub 39 | -Dimage.custom.tag=${{ steps.image-tag.outputs.IMAGE_TAG }} 40 | -Dapp.env.var1=${{ env.ACCESS_KEY_BUCKET }} 41 | -Dapp.env.var2=${{ env.SECRET_KEY_BUCKET }} 42 | 43 | - name: update images version in k8s manifests resources and commit changes 44 | run: | 45 | IMAGE_TAG=${{ steps.image-tag.outputs.IMAGE_TAG }} 46 | echo -e "Current k8s/aws-eks/services/customer/deployment.yml\n$(cat k8s/aws-eks/services/customer/deployment.yml)" 47 | sed -i -E 's_(miliariadnane/customer:)([^"]*)_\1'${IMAGE_TAG}'_' k8s/aws-eks/services/customer/deployment.yml 48 | echo -e "Current k8s/aws-eks/services/customer/deployment.yml\n$(cat k8s/aws-eks/services/customer/deployment.yml)" 49 | 50 | echo -e "Current k8s/aws-eks/services/product/deployment.yml\n$(cat k8s/aws-eks/services/product/deployment.yml)" 51 | sed -i -E 's_(miliariadnane/product:)([^"]*)_\1'${IMAGE_TAG}'_' k8s/aws-eks/services/product/deployment.yml 52 | echo -e "Current k8s/aws-eks/services/product/deployment.yml\n$(cat k8s/aws-eks/services/product/deployment.yml)" 53 | 54 | echo -e "Current k8s/aws-eks/services/order/deployment.yml\n$(cat k8s/aws-eks/services/order/deployment.yml)" 55 | sed -i -E 's_(miliariadnane/order:)([^"]*)_\1'${IMAGE_TAG}'_' k8s/aws-eks/services/order/deployment.yml 56 | echo -e "Current k8s/aws-eks/services/order/deployment.yml\n$(cat k8s/aws-eks/services/order/deployment.yml)" 57 | 58 | echo -e "Current k8s/aws-eks/services/notification/deployment.yml\n$(cat k8s/aws-eks/services/notification/deployment.yml)" 59 | sed -i -E 's_(miliariadnane/notification:)([^"]*)_\1'${IMAGE_TAG}'_' k8s/aws-eks/services/notification/deployment.yml 60 | echo -e "Current k8s/aws-eks/services/notification/deployment.yml\n$(cat k8s/aws-eks/services/notification/deployment.yml)" 61 | 62 | echo -e "Current k8s/aws-eks/services/payment/deployment.yml\n$(cat k8s/aws-eks/services/payment/deployment.yml)" 63 | sed -i -E 's_(miliariadnane/payment:)([^"]*)_\1'${IMAGE_TAG}'_' k8s/aws-eks/services/payment/deployment.yml 64 | echo -e "Current k8s/aws-eks/services/payment/deployment.yml\n$(cat k8s/aws-eks/services/payment/deployment.yml)" 65 | 66 | git config user.name github-actions 67 | git config user.email github-actions@github.com 68 | git add k8s/aws-eks/services/customer/deployment.yml 69 | git add k8s/aws-eks/services/product/deployment.yml 70 | git add k8s/aws-eks/services/order/deployment.yml 71 | git add k8s/aws-eks/services/payment/deployment.yml 72 | git add k8s/aws-eks/services/notification/deployment.yml 73 | git commit -m "new app version: ${IMAGE_TAG}" 74 | git push 75 | 76 | - name: install kubectl 77 | uses: azure/setup-kubectl@v1 78 | with: 79 | version: 'v1.21.5' 80 | id: install-kubectl 81 | 82 | - name: setup aws credentials 83 | uses: aws-actions/configure-aws-credentials@v1 84 | with: 85 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 86 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 87 | aws-region: ca-central-1 88 | 89 | - name: update kube config 90 | run: aws eks update-kubeconfig --name demo-microservices --region us-east-1 91 | 92 | - name: deploy to AWS EKS 93 | run: | 94 | kubectl apply -f k8s/aws-eks/services/customer/deployment.yml 95 | kubectl apply -f k8s/aws-eks/services/product/deployment.yml 96 | kubectl apply -f k8s/aws-eks/services/order/deployment.yml 97 | kubectl apply -f k8s/aws-eks/services/notification/deployment.yml 98 | kubectl apply -f k8s/aws-eks/services/payment/deployment.yml 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /amqp/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | demo-microservices 7 | dev.nano 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | jar 12 | amqp 13 | 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-amqp 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /amqp/src/main/java/dev/nano/amqp/RabbitMQConfig.java: -------------------------------------------------------------------------------- 1 | package dev.nano.amqp; 2 | 3 | import org.springframework.amqp.core.*; 4 | import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; 5 | import org.springframework.amqp.rabbit.connection.ConnectionFactory; 6 | import org.springframework.amqp.rabbit.core.RabbitTemplate; 7 | import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; 8 | import org.springframework.amqp.support.converter.MessageConverter; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.context.annotation.Primary; 13 | 14 | @Configuration 15 | public class RabbitMQConfig { 16 | 17 | private ConnectionFactory connectionFactory; 18 | @Value("${rabbitmq.exchange.internal}") 19 | private String internalExchange; 20 | @Value("${rabbitmq.queue.notification}") 21 | private String notificationQueue; 22 | @Value("${rabbitmq.routing-key.internal-notification}") 23 | private String internalNotificationRoutingKey; 24 | 25 | public RabbitMQConfig(ConnectionFactory connectionFactory) { 26 | this.connectionFactory = connectionFactory; 27 | } 28 | 29 | @Bean 30 | Queue notificationQueue() { 31 | return new Queue(this.notificationQueue); 32 | } 33 | 34 | @Bean 35 | TopicExchange internalExchange() { 36 | return new TopicExchange(this.internalExchange); 37 | } 38 | 39 | // bind queue to exchange with routing key 40 | @Bean 41 | Binding binding() { 42 | return BindingBuilder 43 | .bind(notificationQueue()) 44 | .to(internalExchange()) 45 | .with(this.internalNotificationRoutingKey); 46 | } 47 | 48 | // Configure amqp template that allows to send messages 49 | @Primary 50 | @Bean 51 | AmqpTemplate amqpTemplate() { 52 | RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); 53 | rabbitTemplate.setMessageConverter(jacksonConverter()); 54 | return rabbitTemplate; 55 | } 56 | 57 | //Build the rabbit listener container & connect to RabbitMQ broker to listener message 58 | // that allows to consume messages from queues (listener) 59 | @Bean 60 | SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory() { 61 | SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); 62 | factory.setConnectionFactory(connectionFactory); 63 | factory.setMessageConverter(jacksonConverter()); 64 | return factory; 65 | } 66 | 67 | // Jackson2JsonMessageConverter is used to convert messages to JSON format 68 | @Bean 69 | MessageConverter jacksonConverter() { 70 | MessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter(); 71 | return jackson2JsonMessageConverter; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /amqp/src/main/java/dev/nano/amqp/RabbitMQProducer.java: -------------------------------------------------------------------------------- 1 | package dev.nano.amqp; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.amqp.core.AmqpTemplate; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | @AllArgsConstructor @Slf4j 10 | public class RabbitMQProducer { 11 | 12 | private final AmqpTemplate amqpTemplate; 13 | 14 | public void publish(String exchange, String routingKey, Object payload) { 15 | log.info("Publishing to {} using routingKey {} with payload {}", exchange, routingKey, payload); 16 | amqpTemplate.convertAndSend(exchange, routingKey, payload); 17 | log.info("Published to {} using routingKey {}. Payload: {}", exchange, routingKey, payload); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /amqp/src/main/resources/amqp-default.properties: -------------------------------------------------------------------------------- 1 | rabbitmq.exchange.internal=internal.exchange 2 | rabbitmq.queue.notification=notification.queue 3 | rabbitmq.routing-key.internal-notification=internal.notification.routing-key 4 | -------------------------------------------------------------------------------- /amqp/src/main/resources/amqp-docker.properties: -------------------------------------------------------------------------------- 1 | rabbitmq.exchange.internal=internal.exchange 2 | rabbitmq.queue.notification=notification.queue 3 | rabbitmq.routing-key.internal-notification=internal.notification.routing-key 4 | -------------------------------------------------------------------------------- /amqp/src/main/resources/amqp-eks.properties: -------------------------------------------------------------------------------- 1 | rabbitmq.exchange.internal=internal.exchange 2 | rabbitmq.queue.notification=notification.queue 3 | rabbitmq.routing-key.internal-notification=internal.notification.routing-key 4 | -------------------------------------------------------------------------------- /amqp/src/main/resources/amqp-kube.properties: -------------------------------------------------------------------------------- 1 | rabbitmq.exchange.internal=internal.exchange 2 | rabbitmq.queue.notification=notification.queue 3 | rabbitmq.routing-key.internal-notification=internal.notification.routing-key 4 | -------------------------------------------------------------------------------- /apiKey-manager/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | dev.nano 6 | demo-microservices 7 | 1.0-SNAPSHOT 8 | 9 | 10 | apiKey-manager 11 | 12 | 13 | 17 14 | 17 15 | UTF-8 16 | 17 | 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-starter-web 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-data-jpa 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-validation 30 | 31 | 32 | org.postgresql 33 | postgresql 34 | runtime 35 | 36 | 37 | dev.nano 38 | common 39 | 1.0-SNAPSHOT 40 | 41 | 42 | 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-maven-plugin 48 | 49 | 50 | org.apache.maven.plugins 51 | maven-compiler-plugin 52 | ${maven.compiler.version} 53 | 54 | ${maven.compiler.source} 55 | ${maven.compiler.target} 56 | 57 | 58 | org.projectlombok 59 | lombok 60 | ${lombok.version} 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/ApiKeyManagerApplication.java: -------------------------------------------------------------------------------- 1 | package dev.nano; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ApiKeyManagerApplication { 8 | public static void main(String[] args) { 9 | SpringApplication.run(ApiKeyManagerApplication.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/apikey/ApiKeyConstant.java: -------------------------------------------------------------------------------- 1 | package dev.nano.apikey; 2 | 3 | public class ApiKeyConstant { 4 | public static final String API_KEY_URI_REST_API = "/api/v1/apiKey-manager/api-keys"; 5 | public static final String API_KEY_NOT_FOUND = "Api key not found"; 6 | } 7 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/apikey/ApiKeyEntity.java: -------------------------------------------------------------------------------- 1 | package dev.nano.apikey; 2 | 3 | import dev.nano.application.ApplicationEntity; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import lombok.experimental.SuperBuilder; 8 | 9 | import jakarta.persistence.*; 10 | import java.time.LocalDateTime; 11 | import java.util.List; 12 | 13 | @Entity 14 | @Table(name = "api_keys") 15 | @Data @SuperBuilder 16 | @NoArgsConstructor @AllArgsConstructor 17 | public class ApiKeyEntity { 18 | @Id 19 | @SequenceGenerator( 20 | name = "customer_sequence", 21 | sequenceName = "customer_sequence", 22 | allocationSize = 1 23 | ) 24 | @GeneratedValue( 25 | strategy = GenerationType.SEQUENCE, 26 | generator = "customer_sequence" 27 | ) 28 | @Column( 29 | name = "id", 30 | updatable = false 31 | ) 32 | private Long id; 33 | 34 | @Column(unique = true, nullable = false) 35 | private String key; 36 | 37 | @Column(nullable = false, unique = true) 38 | private String client; 39 | 40 | private String description; 41 | 42 | private LocalDateTime createdDate; 43 | 44 | private LocalDateTime expirationDate; 45 | 46 | private boolean enabled; 47 | 48 | private boolean neverExpires; 49 | 50 | private boolean approved; 51 | 52 | private boolean revoked; 53 | 54 | @OneToMany(mappedBy = "apiKey") 55 | private List applications; 56 | } 57 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/apikey/ApiKeyRepository.java: -------------------------------------------------------------------------------- 1 | package dev.nano.apikey; 2 | 3 | import dev.nano.application.ApplicationName; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.Query; 6 | 7 | import java.util.Optional; 8 | 9 | public interface ApiKeyRepository extends JpaRepository { 10 | @Query(""" 11 | SELECT ak FROM ApiKeyEntity ak 12 | INNER JOIN ApplicationEntity ap 13 | ON ak.id = ap.apiKey.id 14 | WHERE ak.key = :key 15 | AND ap.applicationName = :appName 16 | """) 17 | Optional findByKeyAndApplicationName(String key, ApplicationName applicationName); 18 | 19 | @Query(""" 20 | SELECT 21 | CASE WHEN COUNT(ak) > 0 22 | THEN TRUE 23 | ELSE FALSE 24 | END 25 | FROM ApiKeyEntity ak 26 | WHERE ak.key = :key 27 | """) 28 | boolean doesKeyExists(String key); 29 | 30 | @Query("SELECT ak FROM ApiKeyEntity ak WHERE ak.key = :key") 31 | Optional findByKey(String key); 32 | } 33 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/apikey/ApiKeyRequest.java: -------------------------------------------------------------------------------- 1 | package dev.nano.apikey; 2 | 3 | import dev.nano.application.ApplicationName; 4 | 5 | import java.util.List; 6 | 7 | public record ApiKeyRequest( 8 | String client, 9 | String description, 10 | List applications 11 | ) { 12 | } 13 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/apikey/ApiKeyService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.apikey; 2 | 3 | import dev.nano.application.ApplicationName; 4 | 5 | public interface ApiKeyService { 6 | String save(ApiKeyRequest apiKeyRequest); 7 | void revokeApi(String key); 8 | boolean isAuthorized(String apiKey, ApplicationName applicationName); 9 | } 10 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/apikey/ApiKeyServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.nano.apikey; 2 | 3 | import dev.nano.application.ApplicationEntity; 4 | import dev.nano.application.ApplicationName; 5 | import dev.nano.application.ApplicationRepository; 6 | import exceptionhandler.core.ResourceNotFoundException; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.time.LocalDateTime; 11 | import java.util.List; 12 | import java.util.Optional; 13 | import java.util.Set; 14 | import java.util.stream.Collectors; 15 | 16 | import static dev.nano.apikey.ApiKeyConstant.API_KEY_NOT_FOUND; 17 | 18 | 19 | @Service 20 | @RequiredArgsConstructor 21 | public class ApiKeyServiceImpl implements ApiKeyService { 22 | private static final Integer EXPIRATION_DAYS = 30; 23 | private final ApiKeyRepository apiKeyRepository; 24 | private final ApplicationRepository applicationRepository; 25 | private final KeyGenerator keyGenerator; 26 | 27 | @Override 28 | public String save(ApiKeyRequest apiKeyRequest) { 29 | ApiKeyEntity apiKey = new ApiKeyEntity(); 30 | 31 | apiKey.setClient(apiKeyRequest.client()); 32 | apiKey.setDescription(apiKeyRequest.description()); 33 | 34 | String apiKeyValue = keyGenerator.generateKey(); 35 | apiKey.setKey(apiKeyValue); 36 | 37 | apiKey.setApproved(true); 38 | apiKey.setEnabled(true); 39 | apiKey.setNeverExpires(false); 40 | apiKey.setCreatedDate(LocalDateTime.now()); 41 | apiKey.setExpirationDate(LocalDateTime.now().plusDays(EXPIRATION_DAYS)); 42 | 43 | ApiKeyEntity savedApiKeyEntity = apiKeyRepository.save(apiKey); 44 | 45 | Set applications = Optional.ofNullable(apiKeyRequest.applications()) 46 | .orElse(List.of()) 47 | .stream().map(app -> ApplicationEntity.builder() 48 | .applicationName(app) 49 | .apiKey(savedApiKeyEntity) 50 | .revoked(false) 51 | .enabled(true) 52 | .build()) 53 | .collect(Collectors.toUnmodifiableSet()); 54 | 55 | applicationRepository.saveAll(applications); 56 | 57 | return apiKeyValue; 58 | } 59 | 60 | @Override 61 | public void revokeApi(String key) { 62 | ApiKeyEntity apiKey = apiKeyRepository.findByKey(key).orElseThrow( 63 | () -> new ResourceNotFoundException(API_KEY_NOT_FOUND)); 64 | 65 | apiKey.setRevoked(true); 66 | apiKey.setEnabled(false); 67 | apiKey.setApproved(false); 68 | apiKeyRepository.save(apiKey); 69 | 70 | // revoke all applications associated with this api key 71 | apiKey.getApplications().forEach(app -> { 72 | app.setRevoked(true); 73 | app.setEnabled(false); 74 | app.setApproved(false); 75 | applicationRepository.save(app); 76 | }); 77 | } 78 | 79 | @Override 80 | public boolean isAuthorized(String apiKey, ApplicationName applicationName) { 81 | Optional optionalApiKey = apiKeyRepository.findByKeyAndApplicationName(apiKey, applicationName); 82 | 83 | if(optionalApiKey.isEmpty()) { 84 | return false; 85 | } 86 | 87 | ApiKeyEntity apiKeyEntity = optionalApiKey.get(); 88 | 89 | return apiKeyEntity.getApplications() 90 | .stream() 91 | .filter(app -> app.getApplicationName().equals(applicationName)) 92 | .findFirst() 93 | .map(app -> app.isEnabled() && 94 | app.isApproved() && 95 | !app.isRevoked() && 96 | apiKeyEntity.isEnabled() && 97 | apiKeyEntity.isApproved() && 98 | !apiKeyEntity.isRevoked() && 99 | (apiKeyEntity.isNeverExpires() || LocalDateTime.now().isBefore(apiKeyEntity.getExpirationDate())) // isAfter used to check if the expiration date is in the future 100 | ) 101 | .orElse(false); 102 | 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/apikey/KeyGenerator.java: -------------------------------------------------------------------------------- 1 | package dev.nano.apikey; 2 | 3 | public interface KeyGenerator { 4 | String generateKey(); 5 | } 6 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/apikey/UUIDKeyGeneratorImpl.java: -------------------------------------------------------------------------------- 1 | package dev.nano.apikey; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.util.UUID; 6 | 7 | @Component 8 | public class UUIDKeyGeneratorImpl implements KeyGenerator { 9 | @Override 10 | public String generateKey() { 11 | return UUID.randomUUID().toString(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/application/ApplicationConstant.java: -------------------------------------------------------------------------------- 1 | package dev.nano.application; 2 | 3 | public class ApplicationConstant { 4 | public static final String APPLICATION_URI_REST_API = "/api/v1/apiKey-manager/applications"; 5 | public static final String APPLICATION_NOT_FOUND = "Application not found"; 6 | } 7 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/application/ApplicationEntity.java: -------------------------------------------------------------------------------- 1 | package dev.nano.application; 2 | 3 | import dev.nano.apikey.ApiKeyEntity; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import lombok.experimental.SuperBuilder; 8 | 9 | import jakarta.persistence.*; 10 | 11 | @Entity 12 | @Table(name = "applications") 13 | @Data @SuperBuilder 14 | @NoArgsConstructor @AllArgsConstructor 15 | public class ApplicationEntity { 16 | @Id 17 | @SequenceGenerator( 18 | name = "customer_sequence", 19 | sequenceName = "customer_sequence", 20 | allocationSize = 1 21 | ) 22 | @GeneratedValue( 23 | strategy = GenerationType.SEQUENCE, 24 | generator = "customer_sequence" 25 | ) 26 | @Column( 27 | name = "id", 28 | updatable = false 29 | ) 30 | private Integer id; 31 | @Enumerated(EnumType.STRING) 32 | @Column(nullable = false) 33 | private ApplicationName applicationName; 34 | private boolean enabled; 35 | private boolean approved; 36 | private boolean revoked; 37 | @ManyToOne(fetch = FetchType.LAZY) 38 | @JoinColumn( 39 | name = "api_key_id", 40 | referencedColumnName = "id" 41 | ) 42 | private ApiKeyEntity apiKey; 43 | } 44 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/application/ApplicationName.java: -------------------------------------------------------------------------------- 1 | package dev.nano.application; 2 | 3 | public enum ApplicationName { 4 | CUSTOMER, 5 | PRODUCT, 6 | ORDER, 7 | PAYMENT, 8 | NOTIFICATION, 9 | APIKEY_MANAGER 10 | } 11 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/application/ApplicationRepository.java: -------------------------------------------------------------------------------- 1 | package dev.nano.application; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.Optional; 6 | 7 | public interface ApplicationRepository extends JpaRepository { 8 | Optional findByApplicationName(String applicationName); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/application/ApplicationService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.application; 2 | 3 | public interface ApplicationService { 4 | void assignApplicationToApiKey(ApplicationName applicationName, String apiKey); 5 | void revokeApplicationFromApiKey(String applicationName, String apiKey); 6 | } 7 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/application/ApplicationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.nano.application; 2 | 3 | import dev.nano.apikey.ApiKeyEntity; 4 | import dev.nano.apikey.ApiKeyRepository; 5 | import exceptionhandler.core.ResourceNotFoundException; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | import static dev.nano.apikey.ApiKeyConstant.API_KEY_NOT_FOUND; 10 | import static dev.nano.application.ApplicationConstant.APPLICATION_NOT_FOUND; 11 | 12 | @Service 13 | @RequiredArgsConstructor 14 | public class ApplicationServiceImpl implements ApplicationService{ 15 | private final ApplicationRepository applicationRepository; 16 | private final ApiKeyRepository apiKeyRepository; 17 | 18 | @Override 19 | public void assignApplicationToApiKey(ApplicationName applicationName, String apiKey) { 20 | ApiKeyEntity key = apiKeyRepository.findByKey(apiKey) 21 | .orElseThrow(() -> new ResourceNotFoundException(API_KEY_NOT_FOUND)); 22 | 23 | ApplicationEntity application = ApplicationEntity.builder() 24 | .applicationName(applicationName) 25 | .apiKey(key) 26 | .revoked(false) 27 | .enabled(true) 28 | .approved(true) 29 | .build(); 30 | 31 | applicationRepository.save(application); 32 | } 33 | 34 | @Override 35 | public void revokeApplicationFromApiKey(String applicationName, String apiKey) { 36 | if(!apiKeyRepository.doesKeyExists(apiKey)) 37 | throw new ResourceNotFoundException(API_KEY_NOT_FOUND); 38 | 39 | ApplicationEntity application = applicationRepository.findByApplicationName(applicationName) 40 | .orElseThrow(() -> new ResourceNotFoundException(APPLICATION_NOT_FOUND)); 41 | 42 | application.setRevoked(true); 43 | application.setEnabled(false); 44 | application.setApproved(false); 45 | 46 | applicationRepository.save(application); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/controller/ApiKeyController.java: -------------------------------------------------------------------------------- 1 | package dev.nano.controller; 2 | 3 | import dev.nano.apikey.ApiKeyConstant; 4 | import dev.nano.apikey.ApiKeyRequest; 5 | import dev.nano.apikey.ApiKeyService; 6 | import dev.nano.application.ApplicationName; 7 | import io.swagger.v3.oas.annotations.Operation; 8 | import io.swagger.v3.oas.annotations.media.Content; 9 | import io.swagger.v3.oas.annotations.media.Schema; 10 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 11 | import io.swagger.v3.oas.annotations.responses.ApiResponses; 12 | import io.swagger.v3.oas.annotations.tags.Tag; 13 | import lombok.AllArgsConstructor; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.web.bind.annotation.GetMapping; 16 | import org.springframework.web.bind.annotation.PathVariable; 17 | import org.springframework.web.bind.annotation.PostMapping; 18 | import org.springframework.web.bind.annotation.PutMapping; 19 | import org.springframework.web.bind.annotation.RequestBody; 20 | import org.springframework.web.bind.annotation.RequestMapping; 21 | import org.springframework.web.bind.annotation.RestController; 22 | import swagger.BaseController; 23 | 24 | @RestController 25 | @RequestMapping(path = ApiKeyConstant.API_KEY_URI_REST_API) 26 | @Tag(name = BaseController.API_KEY_TAG, description = BaseController.API_KEY_DESCRIPTION) 27 | @AllArgsConstructor 28 | public class ApiKeyController { 29 | private final ApiKeyService apiKeyService; 30 | 31 | @Operation( 32 | summary = "Generate new API key", 33 | description = "Create a new API key with specified client details and permissions" 34 | ) 35 | @ApiResponses(value = { 36 | @ApiResponse( 37 | responseCode = "200", 38 | description = "API key generated successfully", 39 | content = @Content(mediaType = "application/json", schema = @Schema(type = "string")) 40 | ), 41 | @ApiResponse(responseCode = "400", description = "Invalid request data"), 42 | @ApiResponse(responseCode = "500", description = "Internal server error") 43 | }) 44 | @PostMapping 45 | public ResponseEntity generateNewApiKey( 46 | @RequestBody ApiKeyRequest apiKeyRequest) { 47 | return ResponseEntity.ok(apiKeyService.save(apiKeyRequest)); 48 | } 49 | 50 | @Operation( 51 | summary = "Revoke API key", 52 | description = "Disable and revoke an existing API key" 53 | ) 54 | @ApiResponses(value = { 55 | @ApiResponse(responseCode = "200", description = "API key revoked successfully"), 56 | @ApiResponse(responseCode = "404", description = "API key not found"), 57 | @ApiResponse(responseCode = "500", description = "Internal server error") 58 | }) 59 | @PutMapping("{apiKey}/revoke") 60 | public void revokeKey(@PathVariable("apiKey") String apiKey) { 61 | apiKeyService.revokeApi(apiKey); 62 | } 63 | 64 | @Operation( 65 | summary = "Check API key authorization", 66 | description = "Verify if an API key is authorized for a specific application" 67 | ) 68 | @ApiResponses(value = { 69 | @ApiResponse( 70 | responseCode = "200", 71 | description = "Authorization check completed", 72 | content = @Content(mediaType = "application/json", schema = @Schema(type = "boolean")) 73 | ), 74 | @ApiResponse(responseCode = "400", description = "Invalid input parameters"), 75 | @ApiResponse(responseCode = "500", description = "Internal server error") 76 | }) 77 | @GetMapping("{apiKey}/applications/{applicationName}/authorization") 78 | public ResponseEntity isKeyAuthorizedForApplication( 79 | @PathVariable("apiKey") String apiKey, 80 | @PathVariable("applicationName") ApplicationName applicationName) { 81 | return ResponseEntity.ok(apiKeyService.isAuthorized(apiKey, applicationName)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/java/dev/nano/controller/ApplicationController.java: -------------------------------------------------------------------------------- 1 | package dev.nano.controller; 2 | 3 | import dev.nano.application.ApplicationConstant; 4 | import dev.nano.application.ApplicationName; 5 | import dev.nano.application.ApplicationService; 6 | import io.swagger.v3.oas.annotations.Operation; 7 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 8 | import io.swagger.v3.oas.annotations.responses.ApiResponses; 9 | import io.swagger.v3.oas.annotations.tags.Tag; 10 | import lombok.AllArgsConstructor; 11 | import org.springframework.web.bind.annotation.DeleteMapping; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.PutMapping; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | import org.springframework.web.bind.annotation.RestController; 16 | import swagger.BaseController; 17 | 18 | @RestController 19 | @RequestMapping(path = ApplicationConstant.APPLICATION_URI_REST_API) 20 | @Tag(name = BaseController.APPLICATION_TAG, description = BaseController.APPLICATION_DESCRIPTION) 21 | @AllArgsConstructor 22 | public class ApplicationController { 23 | private final ApplicationService applicationService; 24 | 25 | @Operation( 26 | summary = "Revoke application access", 27 | description = "Remove application access from a specific API key" 28 | ) 29 | @ApiResponses(value = { 30 | @ApiResponse(responseCode = "200", description = "Application access revoked successfully"), 31 | @ApiResponse(responseCode = "404", description = "Application or API key not found"), 32 | @ApiResponse(responseCode = "500", description = "Internal server error") 33 | }) 34 | @DeleteMapping("{applicationName}/revoke/{apiKey}") 35 | public void revokeApplicationFromApiKey( 36 | @PathVariable("applicationName") String applicationName, 37 | @PathVariable("apiKey") String apiKey) { 38 | applicationService.revokeApplicationFromApiKey(applicationName, apiKey); 39 | } 40 | 41 | @Operation( 42 | summary = "Assign application to API key", 43 | description = "Grant application access to a specific API key" 44 | ) 45 | @ApiResponses(value = { 46 | @ApiResponse(responseCode = "200", description = "Application assigned successfully"), 47 | @ApiResponse(responseCode = "404", description = "API key not found"), 48 | @ApiResponse(responseCode = "500", description = "Internal server error") 49 | }) 50 | @PutMapping("{applicationName}/revoke/{apiKey}") 51 | public void assignApplicationToApiKey( 52 | @PathVariable("applicationName") ApplicationName applicationName, 53 | @PathVariable("apiKey") String apiKey) { 54 | applicationService.assignApplicationToApiKey(applicationName, apiKey); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8006 3 | error: 4 | include-message: always 5 | 6 | spring: 7 | application: 8 | name: apikey-manager 9 | config: 10 | import: classpath:shared-application-${spring.profiles.active}.yml 11 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ,--.,--. ,--. 2 | ,--,--. ,---. `--'| .' /,---. ,--. ,--.,-----.,--,--,--. ,--,--.,--,--, ,--,--. ,---. ,---. ,--.--. 3 | ' ,-. || .-. |,--.| . '| .-. : \ ' / '-----'| |' ,-. || \' ,-. || .-. || .-. :| .--' 4 | \ '-' || '-' '| || |\ \ --. \ ' | | | |\ '-' || || |\ '-' |' '-' '\ --.| | 5 | `--`--'| |-' `--'`--' '--'`----'.-' / `--`--`--' `--`--'`--''--' `--`--'.`- / `----'`--' 6 | `--' `---' `---' 7 | -------------------------------------------------------------------------------- /apiKey-manager/src/main/resources/db/data.sql: -------------------------------------------------------------------------------- 1 | -- First insert API Keys and store their IDs 2 | INSERT INTO api_keys (id, key, client, description, created_date, expiration_date, enabled, never_expires, approved, 3 | revoked) 4 | SELECT nextval('api_key_sequence'), key, client, description, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + INTERVAL '365 days', enabled, never_expires, approved, revoked 5 | FROM ( 6 | VALUES 7 | ('ecom-frontend-key-2024', 'E-commerce Frontend', 'API Key for frontend application', true, false, true, false), 8 | ('mobile-app-key-2024', 'Mobile Application', 'API Key for mobile app', true, false, true, false), 9 | ('admin-dashboard-key-2024', 'Admin Dashboard', 'API Key for admin dashboard', true, true, true, false) 10 | ) AS t(key, client, description, enabled, never_expires, approved, revoked); 11 | 12 | -- Then insert Applications using the actual API key IDs 13 | INSERT INTO applications (id, application_name, enabled, approved, revoked, api_key_id) 14 | SELECT nextval('application_sequence'), 15 | app_name, 16 | true, 17 | true, 18 | false, 19 | ak.id 20 | FROM (VALUES ('CUSTOMER', 1), ('CUSTOMER', 2), ('CUSTOMER', 3), 21 | ('PRODUCT', 1), ('PRODUCT', 2), ('PRODUCT', 3), 22 | ('ORDER', 1), ('ORDER', 2), ('ORDER', 3), 23 | ('PAYMENT', 1), ('PAYMENT', 3), 24 | ('NOTIFICATION', 3) 25 | ) AS t(app_name, key_order) 26 | JOIN api_keys ak ON ak.id = (SELECT id FROM api_keys ORDER BY id LIMIT 1 OFFSET (t.key_order - 1)); 27 | -------------------------------------------------------------------------------- /common/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | demo-microservices 5 | dev.nano 6 | 1.0-SNAPSHOT 7 | 8 | 9 | 4.0.0 10 | common 11 | 12 | 13 | 14 | org.springframework.boot 15 | spring-boot-starter-web 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /common/src/main/java/cache/CacheConfig.java: -------------------------------------------------------------------------------- 1 | package cache; 2 | 3 | import org.springframework.cache.CacheManager; 4 | import org.springframework.cache.annotation.EnableCaching; 5 | import org.springframework.cache.concurrent.ConcurrentMapCacheManager; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | @EnableCaching 11 | public class CacheConfig { 12 | @Bean 13 | public CacheManager cacheManager() { 14 | return new ConcurrentMapCacheManager("apikey-authorizations"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /common/src/main/java/exceptionhandler/business/CustomerException.java: -------------------------------------------------------------------------------- 1 | package exceptionhandler.business; 2 | 3 | import exceptionhandler.core.BaseException; 4 | 5 | public class CustomerException extends BaseException { 6 | public CustomerException(String message) { 7 | super(message); 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /common/src/main/java/exceptionhandler/business/NotificationException.java: -------------------------------------------------------------------------------- 1 | package exceptionhandler.business; 2 | 3 | import exceptionhandler.core.BaseException; 4 | 5 | public class NotificationException extends BaseException { 6 | public NotificationException(String message) { 7 | super(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /common/src/main/java/exceptionhandler/business/OrderException.java: -------------------------------------------------------------------------------- 1 | package exceptionhandler.business; 2 | 3 | import exceptionhandler.core.BaseException; 4 | 5 | public class OrderException extends BaseException { 6 | public OrderException(String message) { 7 | super(message); 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /common/src/main/java/exceptionhandler/business/PaymentException.java: -------------------------------------------------------------------------------- 1 | package exceptionhandler.business; 2 | 3 | import exceptionhandler.core.BaseException; 4 | 5 | public class PaymentException extends BaseException { 6 | public PaymentException(String message) { 7 | super(message); 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /common/src/main/java/exceptionhandler/business/ProductException.java: -------------------------------------------------------------------------------- 1 | package exceptionhandler.business; 2 | 3 | import exceptionhandler.core.BaseException; 4 | 5 | public class ProductException extends BaseException { 6 | public ProductException(String message) { 7 | super(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /common/src/main/java/exceptionhandler/core/BadRequestException.java: -------------------------------------------------------------------------------- 1 | package exceptionhandler.core; 2 | 3 | public class BadRequestException extends BaseException { 4 | public BadRequestException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /common/src/main/java/exceptionhandler/core/BaseException.java: -------------------------------------------------------------------------------- 1 | package exceptionhandler.core; 2 | 3 | public abstract class BaseException extends RuntimeException { 4 | public BaseException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /common/src/main/java/exceptionhandler/core/DuplicateResourceException.java: -------------------------------------------------------------------------------- 1 | package exceptionhandler.core; 2 | 3 | public class DuplicateResourceException extends BaseException { 4 | public DuplicateResourceException(String message) { 5 | super(message); 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /common/src/main/java/exceptionhandler/core/ResourceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package exceptionhandler.core; 2 | 3 | public class ResourceNotFoundException extends BaseException { 4 | public ResourceNotFoundException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /common/src/main/java/exceptionhandler/core/ValidationException.java: -------------------------------------------------------------------------------- 1 | package exceptionhandler.core; 2 | 3 | import exceptionhandler.payload.ValidationError; 4 | import lombok.Getter; 5 | 6 | import java.util.List; 7 | 8 | @Getter 9 | public class ValidationException extends BaseException { 10 | private final List errors; 11 | 12 | public ValidationException(String message, List errors) { 13 | super(message); 14 | this.errors = errors; 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /common/src/main/java/exceptionhandler/handler/RestResponseEntityExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package exceptionhandler.handler; 2 | 3 | import exceptionhandler.business.CustomerException; 4 | import exceptionhandler.business.NotificationException; 5 | import exceptionhandler.business.OrderException; 6 | import exceptionhandler.business.PaymentException; 7 | import exceptionhandler.business.ProductException; 8 | import exceptionhandler.core.BaseException; 9 | import exceptionhandler.core.DuplicateResourceException; 10 | import exceptionhandler.core.ResourceNotFoundException; 11 | import exceptionhandler.core.ValidationException; 12 | import exceptionhandler.payload.ErrorCode; 13 | import exceptionhandler.payload.ErrorDetails; 14 | import exceptionhandler.payload.ValidationError; 15 | import lombok.extern.slf4j.Slf4j; 16 | import org.springframework.http.HttpStatus; 17 | import org.springframework.http.ResponseEntity; 18 | import org.springframework.web.bind.annotation.ExceptionHandler; 19 | import org.springframework.web.bind.annotation.ResponseStatus; 20 | import org.springframework.web.bind.annotation.RestControllerAdvice; 21 | import org.springframework.web.context.request.ServletWebRequest; 22 | import org.springframework.web.context.request.WebRequest; 23 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 24 | 25 | import java.time.LocalDateTime; 26 | import java.util.List; 27 | 28 | @RestControllerAdvice 29 | @Slf4j 30 | public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { 31 | 32 | @ExceptionHandler(ResourceNotFoundException.class) 33 | @ResponseStatus(HttpStatus.NOT_FOUND) 34 | public ResponseEntity handleResourceNotFoundException( 35 | ResourceNotFoundException ex, WebRequest request) { 36 | return buildErrorResponse(ex.getMessage(), 37 | ErrorCode.RESOURCE_NOT_FOUND, 38 | HttpStatus.NOT_FOUND, 39 | request); 40 | } 41 | 42 | @ExceptionHandler(ValidationException.class) 43 | @ResponseStatus(HttpStatus.BAD_REQUEST) 44 | public ResponseEntity handleValidationException( 45 | ValidationException ex, WebRequest request) { 46 | return buildErrorResponse(ex.getMessage(), 47 | ErrorCode.VALIDATION_FAILED, 48 | HttpStatus.BAD_REQUEST, 49 | request, 50 | ex.getErrors()); 51 | } 52 | 53 | @ExceptionHandler(DuplicateResourceException.class) 54 | @ResponseStatus(HttpStatus.CONFLICT) 55 | public ResponseEntity handleDuplicateResourceException( 56 | DuplicateResourceException ex, WebRequest request) { 57 | return buildErrorResponse(ex.getMessage(), 58 | ErrorCode.DUPLICATE_RESOURCE, 59 | HttpStatus.CONFLICT, 60 | request); 61 | } 62 | 63 | @ExceptionHandler({ 64 | CustomerException.class, 65 | OrderException.class, 66 | PaymentException.class, 67 | ProductException.class, 68 | NotificationException.class 69 | }) 70 | @ResponseStatus(HttpStatus.BAD_REQUEST) 71 | public ResponseEntity handleBusinessExceptions( 72 | BaseException ex, WebRequest request) { 73 | return buildErrorResponse(ex.getMessage(), 74 | ErrorCode.BAD_REQUEST, 75 | HttpStatus.BAD_REQUEST, 76 | request); 77 | } 78 | 79 | @ExceptionHandler(Exception.class) 80 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 81 | public ResponseEntity handleAllUncaughtException( 82 | Exception ex, WebRequest request) { 83 | return buildErrorResponse(ex.getMessage(), 84 | ErrorCode.INTERNAL_ERROR, 85 | HttpStatus.INTERNAL_SERVER_ERROR, 86 | request); 87 | } 88 | 89 | private ResponseEntity buildErrorResponse( 90 | String message, 91 | ErrorCode errorCode, 92 | HttpStatus status, 93 | WebRequest request) { 94 | return buildErrorResponse(message, errorCode, status, request, null); 95 | } 96 | 97 | private ResponseEntity buildErrorResponse( 98 | String message, 99 | ErrorCode errorCode, 100 | HttpStatus status, 101 | WebRequest request, 102 | List errors) { 103 | 104 | ErrorDetails errorDetails = ErrorDetails.builder() 105 | .timestamp(LocalDateTime.now()) 106 | .code(errorCode.getCode()) 107 | .message(message) 108 | .path(((ServletWebRequest) request).getRequest().getRequestURI()) 109 | .errors(errors) 110 | .build(); 111 | 112 | return new ResponseEntity<>(errorDetails, status); 113 | } 114 | } 115 | 116 | -------------------------------------------------------------------------------- /common/src/main/java/exceptionhandler/payload/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package exceptionhandler.payload; 2 | 3 | public enum ErrorCode { 4 | RESOURCE_NOT_FOUND("ERR_001"), 5 | VALIDATION_FAILED("ERR_002"), 6 | DUPLICATE_RESOURCE("ERR_003"), 7 | BAD_REQUEST("ERR_004"), 8 | INTERNAL_ERROR("ERR_005"); 9 | 10 | private final String code; 11 | 12 | ErrorCode(String code) { 13 | this.code = code; 14 | } 15 | 16 | public String getCode() { 17 | return code; 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /common/src/main/java/exceptionhandler/payload/ErrorDetails.java: -------------------------------------------------------------------------------- 1 | package exceptionhandler.payload; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.List; 8 | 9 | @Getter 10 | @Builder 11 | public class ErrorDetails { 12 | private LocalDateTime timestamp; 13 | private String code; 14 | private String message; 15 | private String path; 16 | private List errors; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /common/src/main/java/exceptionhandler/payload/ValidationError.java: -------------------------------------------------------------------------------- 1 | package exceptionhandler.payload; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @Builder 8 | public class ValidationError { 9 | private String field; 10 | private String message; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /common/src/main/java/swagger/BaseController.java: -------------------------------------------------------------------------------- 1 | package swagger; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class BaseController { 7 | public static final String CUSTOMER_TAG = "Customer Management"; 8 | public static final String CUSTOMER_DESCRIPTION = "Operations about customers including orders and payments"; 9 | public static final String PRODUCT_TAG = "Product Management"; 10 | public static final String PRODUCT_DESCRIPTION = "Operations for managing products catalog and inventory"; 11 | public static final String ORDER_TAG = "Order Management"; 12 | public static final String ORDER_DESCRIPTION = "Operations for managing customer orders and order processing"; 13 | public static final String PAYMENT_TAG = "Payment Management"; 14 | public static final String PAYMENT_DESCRIPTION = "Operations for managing payments and transactions"; 15 | public static final String NOTIFICATION_TAG = "Notification Management"; 16 | public static final String NOTIFICATION_DESCRIPTION = "Operations for managing notifications and communication with customers"; 17 | public static final String API_KEY_TAG = "API Key Management"; 18 | public static final String API_KEY_DESCRIPTION = "Operations for managing API keys and their authorizations"; 19 | public static final String APPLICATION_TAG = "Application Management"; 20 | public static final String APPLICATION_DESCRIPTION = "Operations for managing application access and permissions"; 21 | } 22 | -------------------------------------------------------------------------------- /common/src/main/java/swagger/OpenAPIConfig.java: -------------------------------------------------------------------------------- 1 | package swagger; 2 | 3 | import io.swagger.v3.oas.models.ExternalDocumentation; 4 | import io.swagger.v3.oas.models.OpenAPI; 5 | import io.swagger.v3.oas.models.info.Contact; 6 | import io.swagger.v3.oas.models.info.Info; 7 | import io.swagger.v3.oas.models.info.License; 8 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | @Configuration 13 | @EnableConfigurationProperties(OpenAPIProperties.class) 14 | public class OpenAPIConfig { 15 | 16 | private final OpenAPIProperties properties; 17 | 18 | public OpenAPIConfig(OpenAPIProperties properties) { 19 | this.properties = properties; 20 | } 21 | 22 | @Bean 23 | public OpenAPI customOpenAPI() { 24 | return new OpenAPI() 25 | .info(new Info() 26 | .title(properties.getTitle()) 27 | .version(properties.getVersion()) 28 | .description(properties.getDescription()) 29 | .contact(new Contact() 30 | .name(properties.getContact().getName()) 31 | .email(properties.getContact().getEmail()) 32 | .url(properties.getContact().getUrl())) 33 | .license(new License() 34 | .name(properties.getLicense().getName()) 35 | .url(properties.getLicense().getUrl()))) 36 | .externalDocs(new ExternalDocumentation() 37 | .description(properties.getExternalDocs().getDescription()) 38 | .url(properties.getExternalDocs().getUrl())); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /common/src/main/java/swagger/OpenAPIProperties.java: -------------------------------------------------------------------------------- 1 | package swagger; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | 7 | @ConfigurationProperties(prefix = "openapi") 8 | @Getter @Setter 9 | public class OpenAPIProperties { 10 | private String title; 11 | private String version; 12 | private String description; 13 | private Contact contact = new Contact(); 14 | private License license = new License(); 15 | private ExternalDocs externalDocs = new ExternalDocs(); 16 | 17 | @Getter 18 | @Setter 19 | public static class Contact { 20 | private String name; 21 | private String email; 22 | private String url; 23 | } 24 | 25 | @Getter 26 | @Setter 27 | public static class License { 28 | private String name; 29 | private String url; 30 | } 31 | 32 | @Getter 33 | @Setter 34 | public static class ExternalDocs { 35 | private String description; 36 | private String url; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /common/src/main/resources/application-openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 2 | title: Microservices API Documentation 3 | version: 1.0 4 | description: Documentation for all microservices endpoints 5 | contact: 6 | name: Adnane MILIARI 7 | email: miliari.adnane@gmail.com 8 | url: https://github.com/miliariadnane 9 | license: 10 | name: Apache 2.0 11 | url: http://www.apache.org/licenses/LICENSE-2.0.html 12 | external-docs: 13 | description: Project Documentation 14 | url: https://github.com/miliariadnane/demo-microservices 15 | -------------------------------------------------------------------------------- /common/src/main/resources/shared-application-default.yml: -------------------------------------------------------------------------------- 1 | management: 2 | observations: 3 | key-values: 4 | application: ${spring.application.name} 5 | tracing: 6 | sampling: 7 | probability: 1.0 8 | enabled: true 9 | zipkin: 10 | tracing: 11 | endpoint: http://localhost:9411/api/v2/spans 12 | endpoints: 13 | web: 14 | exposure: 15 | include: "*" 16 | endpoint: 17 | health: 18 | show-details: always 19 | 20 | spring: 21 | datasource: 22 | url: jdbc:postgresql://localhost:5432/${spring.application.name} 23 | username: postgres 24 | password: password 25 | jpa: 26 | hibernate: 27 | ddl-auto: create-drop 28 | properties: 29 | hibernate: 30 | dialect: org.hibernate.dialect.PostgreSQLDialect 31 | format_sql: true 32 | show-sql: true 33 | open-in-view: false 34 | defer-datasource-initialization: true 35 | sql: 36 | init: 37 | mode: always 38 | data-locations: classpath*:db/data.sql 39 | platform: postgresql 40 | rabbitmq: 41 | addresses: localhost:5672 42 | 43 | eureka: 44 | client: 45 | service-url: 46 | defaultZone: http://localhost:8761/eureka 47 | fetch-registry: true 48 | register-with-eureka: true 49 | 50 | logging: 51 | pattern: 52 | correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}]" 53 | level: "%5p ${logging.pattern.correlation} %c{1.} : %m%n" 54 | -------------------------------------------------------------------------------- /common/src/main/resources/shared-application-docker.yml: -------------------------------------------------------------------------------- 1 | management: 2 | zipkin: 3 | tracing: 4 | endpoint: http://zipkin:9411/api/v2/spans 5 | 6 | spring: 7 | datasource: 8 | url: jdbc:postgresql://postgres:5432/${spring.application.name} 9 | username: miliariadnane 10 | password: password 11 | rabbitmq: 12 | addresses: rabbitmq:5672 13 | sql: 14 | init: 15 | mode: always 16 | data-locations: classpath*:db/data.sql 17 | platform: postgresql 18 | 19 | eureka: 20 | client: 21 | service-url: 22 | defaultZone: http://eureka-server:8761/eureka 23 | -------------------------------------------------------------------------------- /common/src/main/resources/shared-application-eks.yml: -------------------------------------------------------------------------------- 1 | management: 2 | zipkin: 3 | tracing: 4 | endpoint: http://zipkin:9411/api/v2/spans 5 | 6 | spring: 7 | datasource: 8 | url: jdbc:postgresql://postgres:5432/${spring.application.name} 9 | username: miliariadnane 10 | password: password 11 | rabbitmq: 12 | addresses: rabbitmq:5672 13 | sql: 14 | init: 15 | mode: always 16 | data-locations: classpath*:db/data.sql 17 | platform: postgresql 18 | 19 | eureka: 20 | client: 21 | service-url: 22 | defaultZone: http://eureka-server:8761/eureka 23 | enabled: false 24 | -------------------------------------------------------------------------------- /common/src/main/resources/shared-application-kube.yml: -------------------------------------------------------------------------------- 1 | management: 2 | zipkin: 3 | tracing: 4 | endpoint: http://zipkin:9411/api/v2/spans 5 | 6 | spring: 7 | datasource: 8 | url: jdbc:postgresql://demo-microservices.csetxdk14qax.us-east-1.rds.amazonaws.com:5432/${spring.application.name} 9 | username: ${DB_USERNAME} 10 | password: ${DB_PASSWORD} 11 | rabbitmq: 12 | addresses: rabbitmq:5672 13 | jpa: 14 | hibernate: 15 | ddl-auto: update 16 | sql: 17 | init: 18 | mode: never 19 | -------------------------------------------------------------------------------- /customer/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miliariadnane/demo-microservices/dd46bfb87360bbe1813852cc0316c156fdb80ffb/customer/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /customer/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /customer/src/main/java/dev/nano/customer/CustomerApplication.java: -------------------------------------------------------------------------------- 1 | package dev.nano.customer; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | import org.springframework.cloud.openfeign.EnableFeignClients; 7 | import org.springframework.context.annotation.PropertySource; 8 | import org.springframework.context.annotation.PropertySources; 9 | 10 | @SpringBootApplication( 11 | scanBasePackages = { 12 | "dev.nano.customer", 13 | "dev.nano.amqp" 14 | } 15 | ) 16 | @EnableFeignClients( 17 | basePackages = "dev.nano.clients" 18 | ) 19 | @EnableDiscoveryClient 20 | @PropertySources({ 21 | @PropertySource("classpath:amqp-${spring.profiles.active}.properties"), 22 | @PropertySource("classpath:clients-${spring.profiles.active}.properties") 23 | }) 24 | public class CustomerApplication { 25 | public static void main(String[] args) { 26 | SpringApplication.run(CustomerApplication.class, args); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /customer/src/main/java/dev/nano/customer/CustomerConstant.java: -------------------------------------------------------------------------------- 1 | package dev.nano.customer; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class CustomerConstant { 7 | public static final String CUSTOMER_URI_REST_API = "/api/v1/customers"; 8 | public static final String CUSTOMER_NOT_FOUND = "Customer with ID %d not found"; 9 | public static final String CUSTOMER_EMAIL_EXISTS = "Customer with email %s already exists"; 10 | public static final String NO_CUSTOMERS_FOUND = "No customers found"; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /customer/src/main/java/dev/nano/customer/CustomerDTO.java: -------------------------------------------------------------------------------- 1 | package dev.nano.customer; 2 | 3 | 4 | import jakarta.validation.constraints.Email; 5 | import jakarta.validation.constraints.NotBlank; 6 | import jakarta.validation.constraints.Size; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | @Data 13 | @Builder 14 | @AllArgsConstructor 15 | @NoArgsConstructor 16 | public class CustomerDTO { 17 | private Long id; 18 | 19 | @NotBlank(message = "Name is required") 20 | @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters") 21 | private String name; 22 | 23 | @NotBlank(message = "Email is required") 24 | @Email(message = "Invalid email format") 25 | @Size(max = 255, message = "Email cannot exceed 255 characters") 26 | private String email; 27 | 28 | private String phone; 29 | 30 | @Size(max = 255, message = "Address cannot exceed 255 characters") 31 | private String address; 32 | } 33 | -------------------------------------------------------------------------------- /customer/src/main/java/dev/nano/customer/CustomerEntity.java: -------------------------------------------------------------------------------- 1 | package dev.nano.customer; 2 | 3 | import lombok.*; 4 | import lombok.experimental.SuperBuilder; 5 | 6 | import jakarta.persistence.*; 7 | 8 | @Getter @Setter 9 | @AllArgsConstructor @NoArgsConstructor 10 | @ToString @SuperBuilder 11 | @Entity(name = "Customer") 12 | @Table( 13 | name = "customer", 14 | uniqueConstraints = { 15 | /* customize unique constraint */ 16 | @UniqueConstraint(name = "customer_email_unique", columnNames = "email") 17 | } 18 | ) 19 | public class CustomerEntity { 20 | 21 | @Id 22 | @SequenceGenerator( 23 | name = "customer_sequence", 24 | sequenceName = "customer_sequence", 25 | allocationSize = 1 26 | ) 27 | @GeneratedValue( 28 | strategy = GenerationType.SEQUENCE, 29 | generator = "customer_sequence" 30 | ) 31 | @Column( 32 | name = "id", 33 | updatable = false 34 | ) 35 | private Long id; 36 | 37 | @Column( 38 | name = "name", 39 | nullable = false, 40 | columnDefinition = "TEXT" 41 | ) 42 | private String name; 43 | 44 | @Column( 45 | name = "email", 46 | nullable = false 47 | ) 48 | private String email; 49 | 50 | @Column( 51 | name = "phone", 52 | columnDefinition = "TEXT" 53 | ) 54 | private String phone; 55 | 56 | @Column( 57 | name = "address", 58 | columnDefinition = "TEXT" 59 | ) 60 | private String address; 61 | } 62 | -------------------------------------------------------------------------------- /customer/src/main/java/dev/nano/customer/CustomerMapper.java: -------------------------------------------------------------------------------- 1 | package dev.nano.customer; 2 | 3 | import org.mapstruct.Mapper; 4 | import org.mapstruct.MappingTarget; 5 | 6 | import java.util.List; 7 | 8 | @Mapper(componentModel = "spring") 9 | public interface CustomerMapper { 10 | CustomerDTO toDTO(CustomerEntity entity); 11 | CustomerEntity toEntity(CustomerDTO dto); 12 | List toListDTO(List listEntity); 13 | void updateCustomerFromDTO(CustomerDTO dto, @MappingTarget CustomerEntity entity); 14 | } 15 | -------------------------------------------------------------------------------- /customer/src/main/java/dev/nano/customer/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package dev.nano.customer; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | @Repository 7 | public interface CustomerRepository extends JpaRepository { 8 | boolean existsByEmail(String email); 9 | } 10 | -------------------------------------------------------------------------------- /customer/src/main/java/dev/nano/customer/CustomerService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.customer; 2 | 3 | import dev.nano.clients.order.OrderRequest; 4 | import dev.nano.clients.order.OrderResponse; 5 | import dev.nano.clients.payment.PaymentRequest; 6 | import dev.nano.clients.payment.PaymentResponse; 7 | 8 | import java.util.List; 9 | 10 | public interface CustomerService { 11 | CustomerDTO getCustomer(Long id); 12 | List getAllCustomers(); 13 | CustomerDTO createCustomer(CustomerDTO customer); 14 | CustomerDTO updateCustomer(Long id, CustomerDTO customer); 15 | void deleteCustomer(Long id); 16 | OrderResponse customerOrders(OrderRequest orderRequest); 17 | PaymentResponse customerPayment(PaymentRequest paymentRequest); 18 | } 19 | -------------------------------------------------------------------------------- /customer/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8001 3 | error: 4 | include-message: always 5 | 6 | spring: 7 | application: 8 | name: customer 9 | config: 10 | import: classpath:shared-application-${spring.profiles.active}.yml 11 | -------------------------------------------------------------------------------- /customer/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ,-----. ,--. 2 | ' .--./ ,--.,--. ,---. ,-' '-. ,---. ,--,--,--. ,---. ,--.--. 3 | | | | || | ( .-' '-. .-' | .-. | | | | .-. : | .--' 4 | ' '--'\ ' '' ' .-' `) | | ' '-' ' | | | | \ --. | | 5 | `-----' `----' `----' `--' `---' `--`--`--' `----' `--' 6 | -------------------------------------------------------------------------------- /customer/src/main/resources/db/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO customer (id, name, email, phone, address) 2 | VALUES (nextval('customer_sequence'), 'John Smith', 'john.smith@gmail.com', '+1234567890', '123 Main St, New York, NY'), 3 | (nextval('customer_sequence'), 'Emma Wilson', 'emma.wilson@outlook.com', '+1987654321', 4 | '456 Park Ave, London, UK'), 5 | (nextval('customer_sequence'), 'Mohammed Ali', 'mohammed.ali@yahoo.com', '+212661234567', 6 | 'Marina Street, Casablanca, Morocco'), 7 | (nextval('customer_sequence'), 'Sarah Chen', 'sarah.chen@gmail.com', '+8613912345678', 8 | '789 Nanjing Road, Shanghai, China'); 9 | -------------------------------------------------------------------------------- /docker/compose/docker-compose-local.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | container_name: postgres-local 4 | image: postgres:15.5-alpine 5 | environment: 6 | POSTGRES_USER: postgres 7 | POSTGRES_PASSWORD: password 8 | PGDATA: /data/postgres 9 | volumes: 10 | - postgres-local:/data/postgres 11 | ports: 12 | - "5432:5432" 13 | networks: 14 | - postgres-local 15 | restart: unless-stopped 16 | 17 | pgadmin: 18 | container_name: pgadmin-local 19 | image: dpage/pgadmin4 20 | environment: 21 | PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org} 22 | PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin} 23 | PGADMIN_CONFIG_SERVER_MODE: 'False' 24 | volumes: 25 | - pgadmin-local:/var/lib/pgadmin 26 | ports: 27 | - "5050:80" 28 | networks: 29 | - postgres-local 30 | restart: unless-stopped 31 | 32 | zipkin: 33 | image: openzipkin/zipkin:latest 34 | container_name: zipkin-local 35 | ports: 36 | - "9411:9411" 37 | networks: 38 | - spring-local 39 | 40 | rabbitmq: 41 | image: rabbitmq:3.12-management-alpine 42 | container_name: rabbitmq-local 43 | ports: 44 | - "5672:5672" 45 | - "15672:15672" 46 | networks: 47 | - spring-local 48 | 49 | networks: 50 | postgres-local: 51 | driver: bridge 52 | spring-local: 53 | driver: bridge 54 | 55 | volumes: 56 | postgres-local: 57 | pgadmin-local: 58 | -------------------------------------------------------------------------------- /docker/config/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. 4 | evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. 5 | # scrape_timeout is set to the global default (10s). 6 | 7 | # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. 8 | rule_files: 9 | # - "first_rules.yml" 10 | # - "second_rules.yml" 11 | 12 | # A scrape configuration containing exactly one endpoint to scrape: 13 | # Here it's Prometheus itself. 14 | scrape_configs: 15 | # The job name is added as a label `job=` to any timeseries scraped from this config. 16 | - job_name: 'prometheus' 17 | # metrics_path defaults to '/metrics' 18 | # scheme defaults to 'http'. 19 | static_configs: 20 | - targets: ['localhost:9090'] 21 | 22 | - job_name: 'customer' 23 | metrics_path: '/actuator/prometheus' 24 | scrape_interval: 5s 25 | static_configs: 26 | - targets: ['customer:8001'] 27 | 28 | - job_name: 'product' 29 | metrics_path: '/actuator/prometheus' 30 | scrape_interval: 5s 31 | static_configs: 32 | - targets: ['product:8002'] 33 | 34 | - job_name: 'order' 35 | metrics_path: '/actuator/prometheus' 36 | scrape_interval: 5s 37 | static_configs: 38 | - targets: ['order:8003'] 39 | 40 | - job_name: 'notification' 41 | metrics_path: '/actuator/prometheus' 42 | scrape_interval: 5s 43 | static_configs: 44 | - targets: ['notification:8004'] 45 | 46 | - job_name: 'payment' 47 | metrics_path: '/actuator/prometheus' 48 | scrape_interval: 5s 49 | static_configs: 50 | - targets: ['payment:8005'] 51 | 52 | - job_name: 'eureka-server' 53 | metrics_path: '/actuator/prometheus' 54 | scrape_interval: 5s 55 | static_configs: 56 | - targets: ['eureka-server:8761'] 57 | 58 | - job_name: 'gateway' 59 | metrics_path: '/actuator/prometheus' 60 | scrape_interval: 5s 61 | static_configs: 62 | - targets: ['gateway:8765'] 63 | -------------------------------------------------------------------------------- /docs/diagrams/architecture-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miliariadnane/demo-microservices/dd46bfb87360bbe1813852cc0316c156fdb80ffb/docs/diagrams/architecture-diagram.png -------------------------------------------------------------------------------- /docs/diagrams/deploy-workflow-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miliariadnane/demo-microservices/dd46bfb87360bbe1813852cc0316c156fdb80ffb/docs/diagrams/deploy-workflow-diagram.png -------------------------------------------------------------------------------- /docs/diagrams/infrastructure-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miliariadnane/demo-microservices/dd46bfb87360bbe1813852cc0316c156fdb80ffb/docs/diagrams/infrastructure-diagram.png -------------------------------------------------------------------------------- /eureka-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | demo-microservices 7 | dev.nano 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | jar 12 | eureka-server 13 | 14 | 15 | 16 | org.springframework.cloud 17 | spring-cloud-starter-netflix-eureka-server 18 | 19 | 20 | io.micrometer 21 | micrometer-tracing-bridge-brave 22 | 23 | 24 | io.zipkin.reporter2 25 | zipkin-reporter-brave 26 | 27 | 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-maven-plugin 33 | 34 | 35 | 36 | 37 | 38 | jib-build-push-image-to-local 39 | 40 | false 41 | 42 | 43 | 44 | 45 | com.google.cloud.tools 46 | jib-maven-plugin 47 | ${jib.maven.plugin.version} 48 | 49 | 50 | openjdk:17 51 | 52 | 53 | ${image} 54 | 55 | latest 56 | 57 | 58 | 59 | 60 | 61 | package 62 | 63 | dockerBuild 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /eureka-server/src/main/java/dev/nano/eurekaserver/EurekaServerApplication.java: -------------------------------------------------------------------------------- 1 | package dev.nano.eurekaserver; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; 6 | 7 | @SpringBootApplication 8 | @EnableEurekaServer 9 | public class EurekaServerApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(EurekaServerApplication.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /eureka-server/src/main/resources/application-docker.yml: -------------------------------------------------------------------------------- 1 | #Server 2 | server: 3 | port: 8761 4 | 5 | #Spring 6 | spring: 7 | application: 8 | name: eureka-server 9 | 10 | #Management 11 | management: 12 | observations: 13 | key-values: 14 | application: ${spring.application.name} 15 | tracing: 16 | sampling: 17 | probability: 1.0 18 | enabled: true 19 | zipkin: 20 | tracing: 21 | endpoint: http://zipkin:9411/api/v2/spans 22 | endpoints: 23 | web: 24 | exposure: 25 | include: "*" 26 | endpoint: 27 | health: 28 | show-details: always 29 | 30 | #Eureka-Server 31 | eureka: 32 | client: 33 | fetch-registry: false 34 | register-with-eureka: false 35 | -------------------------------------------------------------------------------- /eureka-server/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | #Server 2 | server: 3 | port: 8761 4 | 5 | #Spring 6 | spring: 7 | application: 8 | name: eureka-server 9 | 10 | #Management 11 | management: 12 | observations: 13 | key-values: 14 | application: ${spring.application.name} 15 | tracing: 16 | sampling: 17 | probability: 1.0 18 | enabled: true 19 | zipkin: 20 | tracing: 21 | endpoint: http://localhost:9411/api/v2/spans 22 | endpoints: 23 | web: 24 | exposure: 25 | include: "*" 26 | endpoint: 27 | health: 28 | show-details: always 29 | 30 | #Eureka-Server 31 | eureka: 32 | client: 33 | fetch-registry: false 34 | register-with-eureka: false 35 | 36 | #Logging 37 | logging: 38 | pattern: 39 | correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}]" 40 | level: "%5p ${logging.pattern.correlation} %c{1.} : %m%n" 41 | -------------------------------------------------------------------------------- /eureka-server/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ,------. ,--. ,---. 2 | | .---' ,--.,--. ,--.--. ,---. | |,-. ,--,--. ' .-' ,---. ,--.--. ,--. ,--. ,---. ,--.--. 3 | | `--, | || | | .--' | .-. : | / ' ,-. | `. `-. | .-. : | .--' \ `' / | .-. : | .--' 4 | | `---. ' '' ' | | \ --. | \ \ \ '-' | .-' | \ --. | | \ / \ --. | | 5 | `------' `----' `--' `----' `--'`--' `--`--' `-----' `----' `--' `--' `----' `--' -------------------------------------------------------------------------------- /feign-clients/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | demo-microservices 7 | dev.nano 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | jar 12 | feign-clients 13 | 14 | 15 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/apiKeyManager/ApplicationName.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.apiKeyManager; 2 | 3 | public enum ApplicationName { 4 | CUSTOMER, 5 | PRODUCT, 6 | ORDER, 7 | PAYMENT, 8 | NOTIFICATION, 9 | APIKEY_MANAGER 10 | } 11 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/apiKeyManager/apiKey/ApiKeyManagerClient.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.apiKeyManager.apiKey; 2 | 3 | import org.springframework.cloud.openfeign.FeignClient; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.PathVariable; 6 | import org.springframework.web.bind.annotation.PutMapping; 7 | 8 | @FeignClient(name = "apiKey-manager", url = "${clients.apiKey-manager.url}") 9 | public interface ApiKeyManagerClient { 10 | 11 | @GetMapping("{apiKey}/applications/{applicationName}/authorization") 12 | ApiKeyManagerResponse isKeyAuthorizedForApplication( 13 | @PathVariable("apiKey") String apiKey, 14 | @PathVariable("applicationName") String applicationName); 15 | 16 | @PutMapping("/api/v1/apiKey-manager/api-keys/{apiKey}/revoke") 17 | void revokeKey(@PathVariable("apiKey") String apiKey); 18 | } 19 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/apiKeyManager/apiKey/ApiKeyManagerResponse.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.apiKeyManager.apiKey; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ApiKeyManagerResponse { 7 | private boolean isAuthorized; 8 | } 9 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/apiKeyManager/application/ApplicationKeyManagerClient.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.apiKeyManager.application; 2 | 3 | public interface ApplicationKeyManagerClient { 4 | } 5 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/notification/NotificationClient.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.notification; 2 | 3 | import org.springframework.cloud.openfeign.FeignClient; 4 | import org.springframework.web.bind.annotation.PostMapping; 5 | import org.springframework.web.bind.annotation.RequestBody; 6 | 7 | @FeignClient(name = "notification", url = "${clients.notification.url}") 8 | public interface NotificationClient { 9 | 10 | @PostMapping(path = "/api/v1/notifications/send") 11 | NotificationResponse sendNotification(@RequestBody NotificationRequest notificationRequest); 12 | } 13 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/notification/NotificationRequest.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.notification; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.experimental.SuperBuilder; 7 | 8 | @Data 9 | @AllArgsConstructor 10 | @NoArgsConstructor 11 | @SuperBuilder 12 | public class NotificationRequest { 13 | private Long customerId; 14 | private String customerName; 15 | private String customerEmail; 16 | private String sender; 17 | private String message; 18 | } 19 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/notification/NotificationResponse.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.notification; 2 | 3 | import lombok.Data; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | @Data 8 | public class NotificationResponse { 9 | private Long customerId; 10 | private String customerName; 11 | private String customerEmail; 12 | private String sender; 13 | private String message; 14 | private LocalDateTime sentAt; 15 | } 16 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/order/OrderClient.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.order; 2 | 3 | import org.springframework.cloud.openfeign.FeignClient; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.PathVariable; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | 9 | @FeignClient(name = "order", url = "${clients.order.url}") 10 | public interface OrderClient { 11 | @GetMapping(path = "/api/v1/orders/{orderId}") 12 | OrderResponse getOrder(@PathVariable("orderId") Long orderId); 13 | 14 | @PostMapping(path = "/api/v1/orders/add") 15 | OrderResponse createOrder(@RequestBody OrderRequest orderRequest); 16 | } 17 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/order/OrderRequest.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.order; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class OrderRequest { 7 | private Long customerId; 8 | private String customerName; 9 | private String customerEmail; 10 | private Long productId; 11 | private Integer amount; 12 | } 13 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/order/OrderResponse.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.order; 2 | 3 | import lombok.Data; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | @Data 8 | public class OrderResponse { 9 | private Long id; 10 | private Long customerId; 11 | private Long productId; 12 | private Integer amount; 13 | private LocalDateTime createAt; 14 | } 15 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/payment/PaymentClient.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.payment; 2 | 3 | import org.springframework.cloud.openfeign.FeignClient; 4 | import org.springframework.web.bind.annotation.PostMapping; 5 | import org.springframework.web.bind.annotation.RequestBody; 6 | 7 | @FeignClient(name = "payment", url = "${clients.payment.url}") 8 | public interface PaymentClient { 9 | 10 | @PostMapping(path = "/api/v1/payments/make-new-payment") 11 | PaymentResponse createPayment(@RequestBody PaymentRequest payment); 12 | } 13 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/payment/PaymentRequest.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.payment; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class PaymentRequest { 7 | private Long customerId; 8 | private String customerName; 9 | private String customerEmail; 10 | private Long orderId; 11 | } 12 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/payment/PaymentResponse.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.payment; 2 | 3 | import lombok.Data; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | @Data 8 | public class PaymentResponse { 9 | private Long id; 10 | private Long customerId; 11 | private Long orderId; 12 | private LocalDateTime createAt; 13 | } 14 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/product/ProductClient.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.product; 2 | 3 | import org.springframework.cloud.openfeign.FeignClient; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.PathVariable; 6 | 7 | @FeignClient(name = "product", url = "${clients.product.url}") 8 | public interface ProductClient { 9 | 10 | @GetMapping(path = "/api/v1/products/{productId}") 11 | ProductResponse getProduct(@PathVariable("productId") Long productId); 12 | } 13 | -------------------------------------------------------------------------------- /feign-clients/src/main/java/dev/nano/clients/product/ProductResponse.java: -------------------------------------------------------------------------------- 1 | package dev.nano.clients.product; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ProductResponse { 7 | private String name; 8 | private String image; 9 | private Integer price; 10 | } 11 | -------------------------------------------------------------------------------- /feign-clients/src/main/resources/clients-default.properties: -------------------------------------------------------------------------------- 1 | clients.customer.url=http://localhost:8001 2 | clients.product.url=http://localhost:8002 3 | clients.order.url=http://localhost:8003 4 | clients.notification.url=http://localhost:8004 5 | clients.payment.url=http://localhost:8005 6 | clients.apiKey-manager.url=http://localhost:8006 7 | -------------------------------------------------------------------------------- /feign-clients/src/main/resources/clients-docker.properties: -------------------------------------------------------------------------------- 1 | clients.customer.url=http://customer:8001 2 | clients.product.url=http://product:8002 3 | clients.order.url=http://order:8003 4 | clients.notification.url=http://notification:8004 5 | clients.payment.url=http://payment:8005 6 | clients.apiKey-manager.url=http://localhost:8006 7 | -------------------------------------------------------------------------------- /feign-clients/src/main/resources/clients-eks.properties: -------------------------------------------------------------------------------- 1 | # we don't need to specify the port in k8s because it's listening on port 80 2 | clients.customer.url=http://customer 3 | clients.product.url=http://product 4 | clients.order.url=http://order 5 | clients.notification.url=http://notification 6 | clients.payment.url=http://payment 7 | clients.apiKey-manager.url=http://apiKey-manager 8 | -------------------------------------------------------------------------------- /feign-clients/src/main/resources/clients-kube.properties: -------------------------------------------------------------------------------- 1 | # we don't need to specify the port in k8s because it listening on port 80 2 | clients.customer.url=http://customer 3 | clients.product.url=http://product 4 | clients.order.url=http://order 5 | clients.notification.url=http://notification 6 | clients.payment.url=http://payment 7 | clients.apiKey-manager.url=http://apiKey-manager 8 | -------------------------------------------------------------------------------- /gateway/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | demo-microservices 7 | dev.nano 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | jar 12 | gateway 13 | 14 | 15 | 16 | org.springframework.cloud 17 | spring-cloud-starter-gateway 18 | 19 | 20 | org.springframework.cloud 21 | spring-cloud-starter-netflix-eureka-client 22 | 23 | 24 | org.springframework.cloud 25 | spring-cloud-starter-circuitbreaker-resilience4j 26 | 27 | 28 | io.micrometer 29 | micrometer-tracing-bridge-brave 30 | 31 | 32 | io.zipkin.reporter2 33 | zipkin-reporter-brave 34 | 35 | 36 | dev.nano 37 | common 38 | 1.0-SNAPSHOT 39 | 40 | 41 | dev.nano 42 | feign-clients 43 | 1.0-SNAPSHOT 44 | compile 45 | 46 | 47 | 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-maven-plugin 52 | 53 | 54 | 55 | 56 | 57 | jib-build-push-image-to-local 58 | 59 | false 60 | 61 | 62 | 63 | 64 | com.google.cloud.tools 65 | jib-maven-plugin 66 | ${jib.maven.plugin.version} 67 | 68 | 69 | openjdk:17 70 | 71 | 72 | ${image} 73 | 74 | latest 75 | 76 | 77 | 78 | 79 | 80 | package 81 | 82 | dockerBuild 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /gateway/src/main/java/dev/nano/gateway/GatewayApplication.java: -------------------------------------------------------------------------------- 1 | package dev.nano.gateway; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | import org.springframework.cloud.openfeign.EnableFeignClients; 7 | import org.springframework.context.annotation.PropertySource; 8 | import org.springframework.context.annotation.PropertySources; 9 | 10 | @SpringBootApplication 11 | @EnableDiscoveryClient 12 | @EnableFeignClients( 13 | basePackages = "dev.nano.clients" 14 | 15 | ) 16 | @PropertySources({ 17 | @PropertySource("classpath:clients-${spring.profiles.active}.properties") 18 | }) 19 | public class GatewayApplication { 20 | public static void main(String[] args) { 21 | SpringApplication.run(GatewayApplication.class, args); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gateway/src/main/java/dev/nano/gateway/GatewayConfig.java: -------------------------------------------------------------------------------- 1 | package dev.nano.gateway; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.http.codec.ServerCodecConfigurer; 6 | 7 | @Configuration 8 | public class GatewayConfig { 9 | 10 | @Bean 11 | public ServerCodecConfigurer serverCodecConfigurer() { 12 | return ServerCodecConfigurer.create(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /gateway/src/main/java/dev/nano/gateway/security/ApiAuthorizationFilter.java: -------------------------------------------------------------------------------- 1 | package dev.nano.gateway.security; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.cloud.gateway.filter.GatewayFilterChain; 7 | import org.springframework.cloud.gateway.filter.GlobalFilter; 8 | import org.springframework.cloud.gateway.route.Route; 9 | import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; 10 | import org.springframework.context.annotation.Lazy; 11 | import org.springframework.core.Ordered; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.web.server.ResponseStatusException; 15 | import org.springframework.web.server.ServerWebExchange; 16 | import reactor.core.publisher.Mono; 17 | 18 | import java.util.List; 19 | 20 | @Component 21 | public class ApiAuthorizationFilter implements GlobalFilter, Ordered { 22 | 23 | @Value("${spring.security.api-key.enabled:true}") 24 | private boolean apiKeyEnabled; 25 | 26 | final ApiKeyAuthorizationChecker apiKeyAuthorizationChecker; 27 | 28 | @Autowired 29 | public ApiAuthorizationFilter( 30 | @Qualifier("main-checker") @Lazy ApiKeyAuthorizationChecker apiKeyAuthorizationChecker 31 | ) { 32 | this.apiKeyAuthorizationChecker = apiKeyAuthorizationChecker; 33 | } 34 | 35 | @Override 36 | public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { 37 | if (!apiKeyEnabled) { 38 | return chain.filter(exchange); 39 | } 40 | 41 | Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR); 42 | String applicationName = route.getId(); 43 | List apiKey = exchange.getRequest().getHeaders().get("ApiKey"); 44 | 45 | if (applicationName == null || apiKey.isEmpty()) { 46 | throw new ResponseStatusException( 47 | HttpStatus.UNAUTHORIZED, 48 | "Application name is not defined, you are not authorized to access this resource" 49 | ); 50 | } 51 | 52 | return chain.filter(exchange); 53 | } 54 | 55 | @Override 56 | public int getOrder() { 57 | return Ordered.LOWEST_PRECEDENCE; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /gateway/src/main/java/dev/nano/gateway/security/ApiKeyAuthorizationChecker.java: -------------------------------------------------------------------------------- 1 | package dev.nano.gateway.security; 2 | 3 | public interface ApiKeyAuthorizationChecker { 4 | boolean isAuthorized(String apiKey, String applicationName); 5 | } 6 | -------------------------------------------------------------------------------- /gateway/src/main/java/dev/nano/gateway/security/ApiKeyManagerAuthorizationChecker.java: -------------------------------------------------------------------------------- 1 | package dev.nano.gateway.security; 2 | 3 | import dev.nano.clients.apiKeyManager.apiKey.ApiKeyManagerClient; 4 | import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; 5 | import lombok.AllArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.cache.annotation.Cacheable; 8 | import org.springframework.stereotype.Service; 9 | 10 | /** 11 | * Service responsible for checking API key authorization against the apiKey-manager service. 12 | * 13 | * Features: 14 | * - Caches authorization results to improve performance 15 | * - Uses circuit breaker pattern for resilience 16 | * - Integrates with apiKey-manager service via Feign client 17 | * - Provides fallback mechanism for service unavailability 18 | * 19 | * Authorization Flow: 20 | * 1. Check cache for existing authorization decision 21 | * 2. If not in cache, call apiKey-manager service 22 | * 3. Cache the result for future requests 23 | * 4. Handle service failures with circuit breaker 24 | */ 25 | 26 | @Service("main-checker") 27 | @AllArgsConstructor 28 | @Slf4j 29 | public class ApiKeyManagerAuthorizationChecker implements ApiKeyAuthorizationChecker { 30 | 31 | private final ApiKeyManagerClient apiKeyManagerClient; 32 | 33 | /** 34 | * Checks if an API key is authorized for a specific application 35 | * 36 | * @param apiKey the API key to validate 37 | * @param applicationName the target application/service name 38 | * @return true if authorized, false otherwise 39 | */ 40 | 41 | @Override 42 | @Cacheable(value = "apikey-authorizations", key = "#apiKey + '-' + #applicationName") 43 | @CircuitBreaker(name = "apiKeyAuthorization", fallbackMethod = "fallbackIsAuthorized") 44 | public boolean isAuthorized(String apiKey, String applicationName) { 45 | return apiKeyManagerClient.isKeyAuthorizedForApplication( 46 | apiKey, 47 | applicationName 48 | ).isAuthorized(); 49 | } 50 | 51 | /** 52 | * Fallback method when apiKey-manager service is unavailable 53 | * Returns false to maintain security in case of service failure 54 | * 55 | * @param apiKey the API key that was being checked 56 | * @param applicationName the target application 57 | * @param ex the exception that triggered the fallback 58 | * @return false to deny access when service is unavailable 59 | */ 60 | private boolean fallbackIsAuthorized(String apiKey, String applicationName, Exception ex) { 61 | log.error("Failed to check API key authorization for key: {} and application: {}. Error: {}", 62 | apiKey, applicationName, ex.getMessage()); 63 | return false; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /gateway/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | #Server 2 | server: 3 | port: 8765 4 | 5 | #Spring 6 | spring: 7 | application: 8 | name: gateway 9 | main: 10 | web-application-type: reactive 11 | allow-bean-definition-overriding: true 12 | security: 13 | api-key: 14 | enabled: false 15 | cloud: 16 | gateway: 17 | routes: 18 | - id: customer 19 | uri: lb://CUSTOMER # lb = load balancer 20 | predicates: 21 | - Path=/api/v1/customers/** 22 | - id: product 23 | uri: lb://PRODUCT 24 | predicates: 25 | - Path=/api/v1/products/** 26 | - id: order 27 | uri: lb://ORDER 28 | predicates: 29 | - Path=/api/v1/orders/** 30 | - id: payment 31 | uri: lb://PAYMENT 32 | predicates: 33 | - Path=/api/v1/payments/** 34 | - id: notification 35 | uri: lb://NOTIFICATION 36 | predicates: 37 | - Path=/api/v1/notifications/** 38 | - id: apiKey-manager 39 | uri: lb://APIKEY-MANAGER 40 | predicates: 41 | - Path=/api/v1/apiKey-manager/** 42 | config: 43 | import: 44 | - classpath:shared-application-${spring.profiles.active}.yml 45 | 46 | #Circuit Breaker 47 | resilience4j: 48 | circuitbreaker: 49 | instances: 50 | apiKeyAuthorization: 51 | slidingWindowSize: 10 52 | minimumNumberOfCalls: 5 53 | permittedNumberOfCallsInHalfOpenState: 3 54 | waitDurationInOpenState: 5s 55 | failureRateThreshold: 50 56 | -------------------------------------------------------------------------------- /gateway/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ,----. ,--. 2 | ' .-./ ,--,--. ,-' '-. ,---. ,--. ,--. ,--,--. ,--. ,--. 3 | | | .---. ' ,-. | '-. .-' | .-. : | |.'.| | ' ,-. | \ ' / 4 | ' '--' | \ '-' | | | \ --. | .'. | \ '-' | \ ' 5 | `------' `--`--' `--' `----' '--' '--' `--`--' .-' / 6 | `---' -------------------------------------------------------------------------------- /k8s/aws-eks/bootstrap/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: Prometheus 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: prometheus 6 | app.kubernetes.io/instance: k8s 7 | app.kubernetes.io/name: prometheus 8 | app.kubernetes.io/part-of: kube-prometheus 9 | app.kubernetes.io/version: 2.32.1 10 | name: applications 11 | namespace: monitoring 12 | spec: 13 | image: quay.io/prometheus/prometheus:v2.32.1 14 | nodeSelector: 15 | kubernetes.io/os: linux 16 | replicas: 1 17 | resources: 18 | requests: 19 | memory: 400Mi 20 | ruleSelector: {} 21 | securityContext: 22 | fsGroup: 2000 23 | runAsNonRoot: true 24 | runAsUser: 1000 25 | serviceAccountName: prometheus-k8s 26 | serviceMonitorNamespaceSelector: 27 | matchLabels: 28 | kubernetes.io/metadata.name: default 29 | serviceMonitorSelector: {} 30 | version: 2.32.1 31 | -------------------------------------------------------------------------------- /k8s/aws-eks/bootstrap/prometheus/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: prometheus-service 5 | namespace: monitoring 6 | spec: 7 | clusterIP: None 8 | ports: 9 | - name: web 10 | port: 9090 11 | protocol: TCP 12 | targetPort: 9090 13 | selector: 14 | prometheus: applications 15 | sessionAffinity: None 16 | type: ClusterIP 17 | -------------------------------------------------------------------------------- /k8s/aws-eks/bootstrap/rabbitmq/configmap.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: rabbitmq-config 5 | data: 6 | enabled_plugins: | 7 | [rabbitmq_management,rabbitmq_peer_discovery_k8s]. 8 | 9 | rabbitmq.conf: | 10 | ## Cluster formation. See https://www.rabbitmq.com/cluster-formation.html to learn more. 11 | cluster_formation.peer_discovery_backend = rabbit_peer_discovery_k8s 12 | cluster_formation.k8s.host = kubernetes.default.svc.cluster.local 13 | ## Should RabbitMQ node name be computed from the pod's hostname or IP address? 14 | ## IP addresses are not stable, so using [stable] hostnames is recommended when possible. 15 | ## Set to "hostname" to use pod hostnames. 16 | ## When this value is changed, so should the variable used to set the RABBITMQ_NODENAME 17 | ## environment variable. 18 | cluster_formation.k8s.address_type = hostname 19 | ## How often should node cleanup checks run? 20 | cluster_formation.node_cleanup.interval = 30 21 | ## Set to false if automatic removal of unknown/absent nodes 22 | ## is desired. This can be dangerous, see 23 | ## * https://www.rabbitmq.com/cluster-formation.html#node-health-checks-and-cleanup 24 | ## * https://groups.google.com/forum/#!msg/rabbitmq-users/wuOfzEywHXo/k8z_HWIkBgAJ 25 | cluster_formation.node_cleanup.only_log_warning = true 26 | cluster_partition_handling = autoheal 27 | ## See https://www.rabbitmq.com/ha.html#master-migration-data-locality 28 | queue_master_locator=min-masters 29 | ## This is just an example. 30 | ## This enables remote access for the default user with well known credentials. 31 | ## Consider deleting the default user and creating a separate user with a set of generated 32 | ## credentials instead. 33 | ## Learn more at https://www.rabbitmq.com/access-control.html#loopback-users 34 | loopback_users.guest = false 35 | -------------------------------------------------------------------------------- /k8s/aws-eks/bootstrap/rabbitmq/rbac.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: rabbitmq 6 | --- 7 | kind: Role 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | metadata: 10 | name: rabbitmq-peer-discovery-rbac 11 | rules: 12 | - apiGroups: [""] 13 | resources: ["endpoints"] 14 | verbs: ["get"] 15 | - apiGroups: [""] 16 | resources: ["events"] 17 | verbs: ["create"] 18 | --- 19 | kind: RoleBinding 20 | apiVersion: rbac.authorization.k8s.io/v1 21 | metadata: 22 | name: rabbitmq-peer-discovery-rbac 23 | subjects: 24 | - kind: ServiceAccount 25 | name: rabbitmq 26 | roleRef: 27 | apiGroup: rbac.authorization.k8s.io 28 | kind: Role 29 | name: rabbitmq-peer-discovery-rbac 30 | -------------------------------------------------------------------------------- /k8s/aws-eks/bootstrap/rabbitmq/service.yml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: rabbitmq 5 | labels: 6 | app: rabbitmq 7 | type: LoadBalancer 8 | spec: 9 | type: NodePort 10 | ports: 11 | - name: http 12 | protocol: TCP 13 | port: 15672 14 | targetPort: 15672 15 | nodePort: 31672 16 | - name: amqp 17 | protocol: TCP 18 | port: 5672 19 | targetPort: 5672 20 | nodePort: 30672 21 | selector: 22 | app: rabbitmq 23 | -------------------------------------------------------------------------------- /k8s/aws-eks/bootstrap/rabbitmq/statefulset.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | # See the Prerequisites section of https://www.rabbitmq.com/cluster-formation.html#peer-discovery-k8s. 3 | kind: StatefulSet 4 | metadata: 5 | name: rabbitmq 6 | spec: 7 | serviceName: rabbitmq 8 | # Three nodes is the recommended minimum. Some features may require a majority of nodes 9 | # to be available. 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: rabbitmq 14 | template: 15 | metadata: 16 | labels: 17 | app: rabbitmq 18 | spec: 19 | serviceAccountName: rabbitmq 20 | terminationGracePeriodSeconds: 10 21 | nodeSelector: 22 | # Use Linux nodes in a mixed OS kubernetes cluster. 23 | # Learn more at https://kubernetes.io/docs/reference/kubernetes-api/labels-annotations-taints/#kubernetes-io-os 24 | kubernetes.io/os: linux 25 | containers: 26 | - name: rabbitmq-k8s 27 | image: rabbitmq:3.9.20-management-alpine 28 | volumeMounts: 29 | - name: config-volume 30 | mountPath: /etc/rabbitmq 31 | # Learn more about what ports various protocols use 32 | # at https://www.rabbitmq.com/networking.html#ports 33 | ports: 34 | - name: http 35 | protocol: TCP 36 | containerPort: 15672 37 | - name: amqp 38 | protocol: TCP 39 | containerPort: 5672 40 | livenessProbe: 41 | exec: 42 | # This is just an example. There is no "one true health check" but rather 43 | # several rabbitmq-diagnostics commands that can be combined to form increasingly comprehensive 44 | # and intrusive health checks. 45 | # Learn more at https://www.rabbitmq.com/monitoring.html#health-checks. 46 | # 47 | # Stage 2 check: 48 | command: ["rabbitmq-diagnostics", "status"] 49 | initialDelaySeconds: 60 50 | # See https://www.rabbitmq.com/monitoring.html for monitoring frequency recommendations. 51 | periodSeconds: 60 52 | timeoutSeconds: 15 53 | readinessProbe: 54 | exec: 55 | # This is just an example. There is no "one true health check" but rather 56 | # several rabbitmq-diagnostics commands that can be combined to form increasingly comprehensive 57 | # and intrusive health checks. 58 | # Learn more at https://www.rabbitmq.com/monitoring.html#health-checks. 59 | # 60 | # Stage 1 check: 61 | command: ["rabbitmq-diagnostics", "ping"] 62 | initialDelaySeconds: 20 63 | periodSeconds: 60 64 | timeoutSeconds: 10 65 | imagePullPolicy: Always 66 | env: 67 | - name: MY_POD_NAME 68 | valueFrom: 69 | fieldRef: 70 | apiVersion: v1 71 | fieldPath: metadata.name 72 | - name: MY_POD_NAMESPACE 73 | valueFrom: 74 | fieldRef: 75 | fieldPath: metadata.namespace 76 | - name: RABBITMQ_USE_LONGNAME 77 | value: "true" 78 | # See a note on cluster_formation.k8s.address_type in the config file section 79 | - name: K8S_SERVICE_NAME 80 | value: rabbitmq 81 | - name: RABBITMQ_NODENAME 82 | value: rabbit@$(MY_POD_NAME).$(K8S_SERVICE_NAME).$(MY_POD_NAMESPACE).svc.cluster.local 83 | - name: K8S_HOSTNAME_SUFFIX 84 | value: .$(K8S_SERVICE_NAME).$(MY_POD_NAMESPACE).svc.cluster.local 85 | - name: RABBITMQ_ERLANG_COOKIE 86 | value: "mycookie" 87 | volumes: 88 | - name: config-volume 89 | configMap: 90 | name: rabbitmq-config 91 | items: 92 | - key: rabbitmq.conf 93 | path: rabbitmq.conf 94 | - key: enabled_plugins 95 | path: enabled_plugins 96 | -------------------------------------------------------------------------------- /k8s/aws-eks/bootstrap/zipkin/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: zipkin 5 | spec: 6 | selector: 7 | app: zipkin 8 | ports: 9 | - port: 9411 10 | targetPort: 9411 11 | protocol: TCP 12 | type: LoadBalancer 13 | -------------------------------------------------------------------------------- /k8s/aws-eks/bootstrap/zipkin/statefulset.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: zipkin 5 | labels: 6 | app: zipkin 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: zipkin 11 | serviceName: zipkin 12 | replicas: 1 13 | template: 14 | metadata: 15 | name: zipkin 16 | labels: 17 | app: zipkin 18 | spec: 19 | containers: 20 | - name: zipkin 21 | image: openzipkin/zipkin 22 | imagePullPolicy: Always 23 | ports: 24 | - containerPort: 9411 25 | protocol: TCP 26 | resources: 27 | requests: 28 | cpu: 100m 29 | memory: 256Mi 30 | limits: 31 | cpu: 200m 32 | memory: 256Mi 33 | restartPolicy: Always 34 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/customer/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: customer 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: customer 10 | template: 11 | metadata: 12 | labels: 13 | app: customer 14 | spec: 15 | containers: 16 | - name: customer 17 | image: miliariadnane/customer:12.09.2022.17.36.48 18 | imagePullPolicy: Always 19 | resources: 20 | requests: 21 | cpu: "200m" 22 | memory: "256Mi" 23 | limits: 24 | memory: "512Mi" 25 | cpu: "500m" 26 | ports: 27 | - containerPort: 8001 28 | env: 29 | - name: SPRING_PROFILES_ACTIVE 30 | value: eks 31 | - name: DB_USERNAME 32 | valueFrom: 33 | secretKeyRef: 34 | name: dbsecretd 35 | key: db_username 36 | - name: DB_PASSWORD 37 | valueFrom: 38 | secretKeyRef: 39 | name: dbsecret 40 | key: db_password 41 | restartPolicy: Always 42 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/customer/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: customer 5 | spec: 6 | selector: 7 | app: customer 8 | ports: 9 | - port: 80 10 | targetPort: 8001 11 | type: LoadBalancer 12 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/customer/servicemonitor.yml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | labels: 5 | name: customer 6 | namespace: default 7 | spec: 8 | endpoints: 9 | - interval: 30s 10 | port: http 11 | path: '/customer/actuator/prometheus' 12 | selector: 13 | matchLabels: 14 | app: customer -------------------------------------------------------------------------------- /k8s/aws-eks/services/notification/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: notification 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: notification 10 | template: 11 | metadata: 12 | labels: 13 | app: notification 14 | spec: 15 | containers: 16 | - name: notification 17 | image: miliariadnane/notification:12.09.2022.17.36.48 18 | imagePullPolicy: Always 19 | resources: 20 | requests: 21 | cpu: "200m" 22 | memory: "256Mi" 23 | limits: 24 | memory: "512Mi" 25 | cpu: "500m" 26 | ports: 27 | - containerPort: 8004 28 | env: 29 | - name: SPRING_PROFILES_ACTIVE 30 | value: eks 31 | - name: DB_USERNAME 32 | valueFrom: 33 | secretKeyRef: 34 | name: dbsecretd 35 | key: db_username 36 | - name: DB_PASSWORD 37 | valueFrom: 38 | secretKeyRef: 39 | name: dbsecret 40 | key: db_password 41 | restartPolicy: Always 42 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/notification/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: notification 5 | spec: 6 | selector: 7 | app: notification 8 | ports: 9 | - port: 80 10 | targetPort: 8004 11 | type: ClusterIP 12 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/notification/servicemonitor.yml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | labels: 5 | name: notification 6 | namespace: default 7 | spec: 8 | endpoints: 9 | - interval: 30s 10 | port: http 11 | path: '/notification/actuator/prometheus' 12 | selector: 13 | matchLabels: 14 | app: notification -------------------------------------------------------------------------------- /k8s/aws-eks/services/order/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: order 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: order 10 | template: 11 | metadata: 12 | labels: 13 | app: order 14 | spec: 15 | containers: 16 | - name: order 17 | image: miliariadnane/order:12.09.2022.17.36.48 18 | imagePullPolicy: Always 19 | resources: 20 | requests: 21 | cpu: "200m" 22 | memory: "256Mi" 23 | limits: 24 | memory: "512Mi" 25 | cpu: "500m" 26 | ports: 27 | - containerPort: 8003 28 | env: 29 | - name: SPRING_PROFILES_ACTIVE 30 | value: eks 31 | - name: DB_USERNAME 32 | valueFrom: 33 | secretKeyRef: 34 | name: dbsecretd 35 | key: db_username 36 | - name: DB_PASSWORD 37 | valueFrom: 38 | secretKeyRef: 39 | name: dbsecret 40 | key: db_password 41 | restartPolicy: Always 42 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/order/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: order 5 | spec: 6 | selector: 7 | app: order 8 | ports: 9 | - port: 80 10 | targetPort: 8003 11 | type: ClusterIP 12 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/order/servicemonitor.yml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | labels: 5 | name: orders 6 | namespace: default 7 | spec: 8 | endpoints: 9 | - interval: 30s 10 | port: http 11 | path: '/order/actuator/prometheus' 12 | selector: 13 | matchLabels: 14 | app: orders 15 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/payment/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: payment 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: payment 10 | template: 11 | metadata: 12 | labels: 13 | app: payment 14 | spec: 15 | containers: 16 | - name: orders 17 | image: miliariadnane/payment:12.09.2022.17.36.48 18 | imagePullPolicy: Always 19 | resources: 20 | requests: 21 | cpu: "200m" 22 | memory: "256Mi" 23 | limits: 24 | memory: "512Mi" 25 | cpu: "500m" 26 | ports: 27 | - containerPort: 8005 28 | env: 29 | - name: SPRING_PROFILES_ACTIVE 30 | value: eks 31 | - name: DB_USERNAME 32 | valueFrom: 33 | secretKeyRef: 34 | name: dbsecretd 35 | key: db_username 36 | - name: DB_PASSWORD 37 | valueFrom: 38 | secretKeyRef: 39 | name: dbsecret 40 | key: db_password 41 | restartPolicy: Always 42 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/payment/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: payment 5 | spec: 6 | selector: 7 | app: payment 8 | ports: 9 | - port: 80 10 | targetPort: 8005 11 | type: ClusterIP 12 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/payment/servicemonitor.yml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | labels: 5 | name: payment 6 | namespace: default 7 | spec: 8 | endpoints: 9 | - interval: 30s 10 | port: http 11 | path: '/payment/actuator/prometheus' 12 | selector: 13 | matchLabels: 14 | app: payment -------------------------------------------------------------------------------- /k8s/aws-eks/services/product/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: product 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: product 10 | template: 11 | metadata: 12 | labels: 13 | app: product 14 | spec: 15 | containers: 16 | - name: orders 17 | image: miliariadnane/product:12.09.2022.17.36.48 18 | imagePullPolicy: Always 19 | resources: 20 | requests: 21 | cpu: "200m" 22 | memory: "256Mi" 23 | limits: 24 | memory: "512Mi" 25 | cpu: "500m" 26 | ports: 27 | - containerPort: 8002 28 | env: 29 | - name: SPRING_PROFILES_ACTIVE 30 | value: eks 31 | - name: DB_USERNAME 32 | valueFrom: 33 | secretKeyRef: 34 | name: dbsecretd 35 | key: db_username 36 | - name: DB_PASSWORD 37 | valueFrom: 38 | secretKeyRef: 39 | name: dbsecret 40 | key: db_password 41 | restartPolicy: Always 42 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/product/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: product 5 | spec: 6 | selector: 7 | app: product 8 | ports: 9 | - port: 80 10 | targetPort: 8002 11 | type: ClusterIP 12 | -------------------------------------------------------------------------------- /k8s/aws-eks/services/product/servicemonitor.yml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | labels: 5 | name: product 6 | namespace: default 7 | spec: 8 | endpoints: 9 | - interval: 30s 10 | port: http 11 | path: '/product/actuator/prometheus' 12 | selector: 13 | matchLabels: 14 | app: product -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/ingress/ingress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: simple-ingress 5 | spec: 6 | rules: 7 | - host: simple-minikube.com 8 | http: 9 | paths: 10 | - path: /customers 11 | pathType: Prefix 12 | backend: 13 | service: 14 | name: customer 15 | port: 16 | number: 80 17 | - path: /products 18 | pathType: Prefix 19 | backend: 20 | service: 21 | name: product 22 | port: 23 | number: 80 24 | - path: /orders 25 | pathType: Prefix 26 | backend: 27 | service: 28 | name: order 29 | port: 30 | number: 80 31 | - path: /notifications 32 | pathType: Prefix 33 | backend: 34 | service: 35 | name: notification 36 | port: 37 | number: 80 38 | - path: /payments 39 | pathType: Prefix 40 | backend: 41 | service: 42 | name: payment 43 | port: 44 | number: 80 45 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/ingress/readme.MD: -------------------------------------------------------------------------------- 1 | ## Ingress 2 | 3 | * Kubernetes Ingress is an API object that provides routing rules to manage external users access to the services in a Kubernetes cluster. 4 | * Ingress can provide load balancing, SSL termination and name-based virtual hosting. 5 | * We must add ingress in our kubernetes minikube cluster to access our services from outside the cluster by specifying the customized path and host name for each microservice, also to manage load balancing. 6 | 7 | To read more about ingress, you can check k8s documentation : 8 | [Ingress k8s doc](https://kubernetes.io/docs/concepts/services-networking/ingress/) 9 | 10 | ![Ingress](../../../../docs/images/ingress.jpg) 11 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/postgres/configmap.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: postgres-config 5 | data: 6 | POSTGRES_DB: miliariadnane 7 | POSTGRES_USER: miliariadnane 8 | POSTGRES_PASSWORD: password 9 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/postgres/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: postgres 5 | spec: 6 | selector: 7 | app: postgres 8 | ports: 9 | - port: 5432 10 | targetPort: 5432 11 | type: ClusterIP 12 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/postgres/statefulset.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: postgres 5 | labels: 6 | app: postgres 7 | spec: 8 | serviceName: postgres 9 | replicas: 1 10 | template: 11 | metadata: 12 | name: postgres 13 | labels: 14 | app: postgres 15 | spec: 16 | volumes: 17 | - name: postgres 18 | persistentVolumeClaim: 19 | claimName: postgres-pc-volume-claim 20 | containers: 21 | - name: postgres 22 | image: postgres 23 | imagePullPolicy: IfNotPresent 24 | volumeMounts: 25 | - mountPath: "/var/lib/postgresql/data" 26 | name: postgres 27 | envFrom: 28 | - configMapRef: 29 | name: postgres-config 30 | resources: 31 | requests: 32 | cpu: 100m 33 | memory: 256Mi 34 | limits: 35 | cpu: 500m 36 | memory: 512Mi 37 | restartPolicy: Always 38 | selector: 39 | matchLabels: 40 | app: postgres 41 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/postgres/volume.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: postgres-pc-volume 5 | labels: 6 | type: local # local volume 7 | app: postgres 8 | spec: 9 | storageClassName: manual 10 | capacity: 11 | storage: 5Gi 12 | accessModes: 13 | - ReadWriteMany 14 | hostPath: 15 | path: /mnt/data # the mount path of the local volume 16 | --- 17 | apiVersion: v1 18 | kind: PersistentVolumeClaim 19 | metadata: 20 | name: postgres-pc-volume-claim 21 | labels: 22 | app: postgres 23 | spec: 24 | storageClassName: manual 25 | accessModes: 26 | - ReadWriteMany 27 | resources: 28 | requests: 29 | storage: 5Gi 30 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/rabbitmq/configmap.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: rabbitmq-config 5 | data: 6 | enabled_plugins: | 7 | [rabbitmq_management,rabbitmq_peer_discovery_k8s]. 8 | 9 | rabbitmq.conf: | 10 | ## Cluster formation. See https://www.rabbitmq.com/cluster-formation.html to learn more. 11 | cluster_formation.peer_discovery_backend = rabbit_peer_discovery_k8s 12 | cluster_formation.k8s.host = kubernetes.default.svc.cluster.local 13 | ## Should RabbitMQ node name be computed from the pod's hostname or IP address? 14 | ## IP addresses are not stable, so using [stable] hostnames is recommended when possible. 15 | ## Set to "hostname" to use pod hostnames. 16 | ## When this value is changed, so should the variable used to set the RABBITMQ_NODENAME 17 | ## environment variable. 18 | cluster_formation.k8s.address_type = hostname 19 | ## How often should node cleanup checks run? 20 | cluster_formation.node_cleanup.interval = 30 21 | ## Set to false if automatic removal of unknown/absent nodes 22 | ## is desired. This can be dangerous, see 23 | ## * https://www.rabbitmq.com/cluster-formation.html#node-health-checks-and-cleanup 24 | ## * https://groups.google.com/forum/#!msg/rabbitmq-users/wuOfzEywHXo/k8z_HWIkBgAJ 25 | cluster_formation.node_cleanup.only_log_warning = true 26 | cluster_partition_handling = autoheal 27 | ## See https://www.rabbitmq.com/ha.html#master-migration-data-locality 28 | queue_master_locator=min-masters 29 | ## This is just an example. 30 | ## This enables remote access for the default user with well known credentials. 31 | ## Consider deleting the default user and creating a separate user with a set of generated 32 | ## credentials instead. 33 | ## Learn more at https://www.rabbitmq.com/access-control.html#loopback-users 34 | loopback_users.guest = false 35 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/rabbitmq/rbac.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: rabbitmq 6 | --- 7 | kind: Role 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | metadata: 10 | name: rabbitmq-peer-discovery-rbac 11 | rules: 12 | - apiGroups: [""] 13 | resources: ["endpoints"] 14 | verbs: ["get"] 15 | - apiGroups: [""] 16 | resources: ["events"] 17 | verbs: ["create"] 18 | --- 19 | kind: RoleBinding 20 | apiVersion: rbac.authorization.k8s.io/v1 21 | metadata: 22 | name: rabbitmq-peer-discovery-rbac 23 | subjects: 24 | - kind: ServiceAccount 25 | name: rabbitmq 26 | roleRef: 27 | apiGroup: rbac.authorization.k8s.io 28 | kind: Role 29 | name: rabbitmq-peer-discovery-rbac 30 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/rabbitmq/service.yml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: rabbitmq 5 | labels: 6 | app: rabbitmq 7 | type: LoadBalancer 8 | spec: 9 | type: NodePort 10 | ports: 11 | - name: http 12 | protocol: TCP 13 | port: 15672 14 | targetPort: 15672 15 | nodePort: 31672 16 | - name: amqp 17 | protocol: TCP 18 | port: 5672 19 | targetPort: 5672 20 | nodePort: 30672 21 | selector: 22 | app: rabbitmq 23 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/rabbitmq/statefulset.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | # See the Prerequisites section of https://www.rabbitmq.com/cluster-formation.html#peer-discovery-k8s. 3 | kind: StatefulSet 4 | metadata: 5 | name: rabbitmq 6 | spec: 7 | serviceName: rabbitmq 8 | # Three nodes is the recommended minimum. Some features may require a majority of nodes 9 | # to be available. 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: rabbitmq 14 | template: 15 | metadata: 16 | labels: 17 | app: rabbitmq 18 | spec: 19 | serviceAccountName: rabbitmq 20 | terminationGracePeriodSeconds: 10 21 | nodeSelector: 22 | # Use Linux nodes in a mixed OS kubernetes cluster. 23 | # Learn more at https://kubernetes.io/docs/reference/kubernetes-api/labels-annotations-taints/#kubernetes-io-os 24 | kubernetes.io/os: linux 25 | containers: 26 | - name: rabbitmq-k8s 27 | image: rabbitmq:3.9.20-management-alpine 28 | volumeMounts: 29 | - name: config-volume 30 | mountPath: /etc/rabbitmq 31 | # Learn more about what ports various protocols use 32 | # at https://www.rabbitmq.com/networking.html#ports 33 | ports: 34 | - name: http 35 | protocol: TCP 36 | containerPort: 15672 37 | - name: amqp 38 | protocol: TCP 39 | containerPort: 5672 40 | livenessProbe: 41 | exec: 42 | # This is just an example. There is no "one true health check" but rather 43 | # several rabbitmq-diagnostics commands that can be combined to form increasingly comprehensive 44 | # and intrusive health checks. 45 | # Learn more at https://www.rabbitmq.com/monitoring.html#health-checks. 46 | # 47 | # Stage 2 check: 48 | command: ["rabbitmq-diagnostics", "status"] 49 | initialDelaySeconds: 60 50 | # See https://www.rabbitmq.com/monitoring.html for monitoring frequency recommendations. 51 | periodSeconds: 60 52 | timeoutSeconds: 15 53 | readinessProbe: 54 | exec: 55 | # This is just an example. There is no "one true health check" but rather 56 | # several rabbitmq-diagnostics commands that can be combined to form increasingly comprehensive 57 | # and intrusive health checks. 58 | # Learn more at https://www.rabbitmq.com/monitoring.html#health-checks. 59 | # 60 | # Stage 1 check: 61 | command: ["rabbitmq-diagnostics", "ping"] 62 | initialDelaySeconds: 20 63 | periodSeconds: 60 64 | timeoutSeconds: 10 65 | imagePullPolicy: Always 66 | env: 67 | - name: MY_POD_NAME 68 | valueFrom: 69 | fieldRef: 70 | apiVersion: v1 71 | fieldPath: metadata.name 72 | - name: MY_POD_NAMESPACE 73 | valueFrom: 74 | fieldRef: 75 | fieldPath: metadata.namespace 76 | - name: RABBITMQ_USE_LONGNAME 77 | value: "true" 78 | # See a note on cluster_formation.k8s.address_type in the config file section 79 | - name: K8S_SERVICE_NAME 80 | value: rabbitmq 81 | - name: RABBITMQ_NODENAME 82 | value: rabbit@$(MY_POD_NAME).$(K8S_SERVICE_NAME).$(MY_POD_NAMESPACE).svc.cluster.local 83 | - name: K8S_HOSTNAME_SUFFIX 84 | value: .$(K8S_SERVICE_NAME).$(MY_POD_NAMESPACE).svc.cluster.local 85 | - name: RABBITMQ_ERLANG_COOKIE 86 | value: "mycookie" 87 | volumes: 88 | - name: config-volume 89 | configMap: 90 | name: rabbitmq-config 91 | items: 92 | - key: rabbitmq.conf 93 | path: rabbitmq.conf 94 | - key: enabled_plugins 95 | path: enabled_plugins 96 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/zipkin/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: zipkin 5 | spec: 6 | selector: 7 | app: zipkin 8 | ports: 9 | - port: 9411 10 | targetPort: 9411 11 | protocol: TCP 12 | type: LoadBalancer 13 | -------------------------------------------------------------------------------- /k8s/minikube/bootstrap/zipkin/statefulset.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: zipkin 5 | labels: 6 | app: zipkin 7 | spec: 8 | serviceName: zipkin 9 | replicas: 1 10 | template: 11 | metadata: 12 | name: zipkin 13 | labels: 14 | app: zipkin 15 | spec: 16 | containers: 17 | - name: zipkin 18 | image: openzipkin/zipkin 19 | imagePullPolicy: Always 20 | ports: 21 | - containerPort: 9411 22 | protocol: TCP 23 | resources: 24 | requests: 25 | cpu: 100m 26 | memory: 256Mi 27 | limits: 28 | cpu: 200m 29 | memory: 256Mi 30 | 31 | restartPolicy: Always 32 | selector: 33 | matchLabels: 34 | app: zipkin 35 | -------------------------------------------------------------------------------- /k8s/minikube/services/customer/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: customer 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: customer 10 | template: 11 | metadata: 12 | labels: 13 | app: customer 14 | spec: 15 | containers: 16 | - name: customer 17 | image: miliariadnane/customer:latest 18 | imagePullPolicy: Always 19 | resources: 20 | requests: 21 | cpu: "200m" 22 | memory: "256Mi" 23 | limits: 24 | memory: "512Mi" 25 | cpu: "500m" 26 | ports: 27 | - containerPort: 8001 28 | env: 29 | - name: SPRING_PROFILES_ACTIVE 30 | value: kube 31 | restartPolicy: Always 32 | -------------------------------------------------------------------------------- /k8s/minikube/services/customer/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: customer 5 | spec: 6 | selector: 7 | app: customer # has to match .metadata.labels.app && template.metadata.labels.app of deployment || stafulset 8 | ports: 9 | - port: 80 10 | targetPort: 8001 # has to match containerPort of deployment || stafulset 11 | type: LoadBalancer 12 | -------------------------------------------------------------------------------- /k8s/minikube/services/notification/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: notification 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: notification 10 | template: 11 | metadata: 12 | labels: 13 | app: notification 14 | spec: 15 | containers: 16 | - name: notification 17 | image: miliariadnane/notification:latest 18 | imagePullPolicy: Always 19 | resources: 20 | requests: 21 | cpu: "200m" 22 | memory: "256Mi" 23 | limits: 24 | memory: "512Mi" 25 | cpu: "500m" 26 | ports: 27 | - containerPort: 8004 28 | env: 29 | - name: SPRING_PROFILES_ACTIVE 30 | value: kube 31 | restartPolicy: Always 32 | -------------------------------------------------------------------------------- /k8s/minikube/services/notification/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: notification 5 | spec: 6 | selector: 7 | app: notification # has to match .metadata.labels.app && template.metadata.labels.app of deployment || stafulset 8 | ports: 9 | - port: 80 10 | targetPort: 8004 # has to match containerPort of deployment || stafulset 11 | type: ClusterIP 12 | -------------------------------------------------------------------------------- /k8s/minikube/services/order/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: order 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: order 10 | template: 11 | metadata: 12 | labels: 13 | app: order 14 | spec: 15 | containers: 16 | - name: order 17 | image: miliariadnane/order:latest 18 | imagePullPolicy: Always 19 | resources: 20 | requests: 21 | cpu: "200m" 22 | memory: "256Mi" 23 | limits: 24 | memory: "512Mi" 25 | cpu: "500m" 26 | ports: 27 | - containerPort: 8003 28 | env: 29 | - name: SPRING_PROFILES_ACTIVE 30 | value: kube 31 | restartPolicy: Always 32 | -------------------------------------------------------------------------------- /k8s/minikube/services/order/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: order 5 | spec: 6 | selector: 7 | app: order 8 | ports: 9 | - port: 80 10 | targetPort: 8003 11 | type: ClusterIP 12 | -------------------------------------------------------------------------------- /k8s/minikube/services/payment/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: payment 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: payment 10 | template: 11 | metadata: 12 | labels: 13 | app: payment 14 | spec: 15 | containers: 16 | - name: orders 17 | image: miliariadnane/payment:latest 18 | imagePullPolicy: Always 19 | resources: 20 | requests: 21 | cpu: "200m" 22 | memory: "256Mi" 23 | limits: 24 | memory: "512Mi" 25 | cpu: "500m" 26 | ports: 27 | - containerPort: 8005 28 | env: 29 | - name: SPRING_PROFILES_ACTIVE 30 | value: eks 31 | restartPolicy: Always 32 | -------------------------------------------------------------------------------- /k8s/minikube/services/payment/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: payment 5 | spec: 6 | selector: 7 | app: payment 8 | ports: 9 | - port: 80 10 | targetPort: 8005 11 | type: ClusterIP 12 | -------------------------------------------------------------------------------- /k8s/minikube/services/product/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: product 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: product 10 | template: 11 | metadata: 12 | labels: 13 | app: product 14 | spec: 15 | containers: 16 | - name: orders 17 | image: miliariadnane/product:latest 18 | imagePullPolicy: Always 19 | resources: 20 | requests: 21 | cpu: "200m" 22 | memory: "256Mi" 23 | limits: 24 | memory: "512Mi" 25 | cpu: "500m" 26 | ports: 27 | - containerPort: 8002 28 | env: 29 | - name: SPRING_PROFILES_ACTIVE 30 | value: kube 31 | restartPolicy: Always 32 | -------------------------------------------------------------------------------- /k8s/minikube/services/product/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: product 5 | spec: 6 | selector: 7 | app: product 8 | ports: 9 | - port: 80 10 | targetPort: 8002 11 | type: ClusterIP 12 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/NotificationApplication.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | import org.springframework.context.annotation.PropertySource; 7 | import org.springframework.context.annotation.PropertySources; 8 | 9 | @SpringBootApplication( 10 | scanBasePackages = { 11 | "dev.nano.notification", 12 | "dev.nano.amqp", 13 | } 14 | ) 15 | @EnableDiscoveryClient 16 | @PropertySources({ 17 | @PropertySource("classpath:amqp-${spring.profiles.active}.properties") 18 | }) 19 | public class NotificationApplication { 20 | public static void main(String[] args) { 21 | SpringApplication.run(NotificationApplication.class, args); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/NotificationConstant.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class NotificationConstant { 7 | public static final String NOTIFICATION_URI_REST_API = "/api/v1/notifications"; 8 | public static final String NOTIFICATION_NOT_FOUND = "Notification with ID %d not found"; 9 | public static final String NOTIFICATION_SEND_ERROR = "Failed to send notification: %s"; 10 | public static final String NO_NOTIFICATIONS_FOUND = "No notifications found"; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/NotificationController.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification; 2 | 3 | import dev.nano.clients.notification.NotificationRequest; 4 | import io.swagger.v3.oas.annotations.Operation; 5 | import io.swagger.v3.oas.annotations.media.ArraySchema; 6 | import io.swagger.v3.oas.annotations.media.Content; 7 | import io.swagger.v3.oas.annotations.media.Schema; 8 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 9 | import io.swagger.v3.oas.annotations.responses.ApiResponses; 10 | import io.swagger.v3.oas.annotations.tags.Tag; 11 | import jakarta.validation.Valid; 12 | import lombok.AllArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.web.bind.annotation.*; 16 | import swagger.BaseController; 17 | 18 | import java.util.List; 19 | 20 | import static dev.nano.notification.NotificationConstant.NOTIFICATION_URI_REST_API; 21 | 22 | @RestController 23 | @RequestMapping(path = NOTIFICATION_URI_REST_API) 24 | @Tag(name = BaseController.NOTIFICATION_TAG, description = BaseController.NOTIFICATION_DESCRIPTION) 25 | @AllArgsConstructor @Slf4j 26 | public class NotificationController { 27 | 28 | private final NotificationService notificationService; 29 | 30 | @Operation( 31 | summary = "Get notification by ID", 32 | description = "Retrieve detailed information about a specific notification using its unique identifier" 33 | ) 34 | @ApiResponses(value = { 35 | @ApiResponse( 36 | responseCode = "200", 37 | description = "Notification found successfully", 38 | content = @Content( 39 | mediaType = "application/json", 40 | schema = @Schema(implementation = NotificationDTO.class) 41 | ) 42 | ), 43 | @ApiResponse(responseCode = "404", description = "Notification not found"), 44 | @ApiResponse(responseCode = "500", description = "Internal server error") 45 | }) 46 | @GetMapping("/{notificationId}") 47 | public ResponseEntity getNotification(@PathVariable("notificationId") Long notificationId) { 48 | log.info("Retrieving notification with id {}", notificationId); 49 | return ResponseEntity.ok(notificationService.getNotification(notificationId)); 50 | } 51 | 52 | @Operation( 53 | summary = "Get all notifications", 54 | description = "Retrieve a list of all notifications with their details" 55 | ) 56 | @ApiResponses(value = { 57 | @ApiResponse( 58 | responseCode = "200", 59 | description = "Notifications retrieved successfully", 60 | content = @Content( 61 | mediaType = "application/json", 62 | array = @ArraySchema(schema = @Schema(implementation = NotificationDTO.class)) 63 | ) 64 | ), 65 | @ApiResponse(responseCode = "500", description = "Internal server error") 66 | }) 67 | @GetMapping("/all") 68 | public ResponseEntity> getAllNotification() { 69 | log.info("Retrieving all notifications"); 70 | return ResponseEntity.ok(notificationService.getAllNotification()); 71 | } 72 | 73 | @Operation( 74 | summary = "Send new notification", 75 | description = "Send a notification to a customer through configured channels (email, SMS, etc.)" 76 | ) 77 | @ApiResponses(value = { 78 | @ApiResponse( 79 | responseCode = "200", 80 | description = "Notification sent successfully" 81 | ), 82 | @ApiResponse(responseCode = "400", description = "Invalid notification data"), 83 | @ApiResponse(responseCode = "404", description = "Customer not found"), 84 | @ApiResponse(responseCode = "500", description = "Internal server error or notification delivery failure") 85 | }) 86 | @PostMapping("/send") 87 | public ResponseEntity sendNotification(@Valid @RequestBody NotificationRequest notificationRequest) { 88 | log.info("Sending new notification {}", notificationRequest); 89 | notificationService.sendNotification(notificationRequest); 90 | return ResponseEntity.ok().build(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/NotificationDTO.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification; 2 | 3 | 4 | import jakarta.validation.constraints.Email; 5 | import jakarta.validation.constraints.NotBlank; 6 | import jakarta.validation.constraints.NotNull; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | import java.time.LocalDateTime; 13 | 14 | @Data 15 | @Builder 16 | @AllArgsConstructor 17 | @NoArgsConstructor 18 | public class NotificationDTO { 19 | private Long id; 20 | 21 | @NotNull(message = "Customer ID is required") 22 | private Long customerId; 23 | 24 | @NotBlank(message = "Customer name is required") 25 | private String customerName; 26 | 27 | @NotBlank(message = "Customer email is required") 28 | @Email(message = "Invalid email format") 29 | private String customerEmail; 30 | 31 | @NotBlank(message = "Sender is required") 32 | private String sender; 33 | 34 | @NotBlank(message = "Message is required") 35 | private String message; 36 | 37 | private LocalDateTime sentAt; 38 | } 39 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/NotificationEntity.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification; 2 | 3 | import lombok.*; 4 | import lombok.experimental.SuperBuilder; 5 | 6 | import jakarta.persistence.*; 7 | import java.time.LocalDateTime; 8 | 9 | @Getter @Setter 10 | @AllArgsConstructor @NoArgsConstructor 11 | @ToString @SuperBuilder 12 | @Entity(name = "Notification") 13 | @Table(name = "notification") 14 | public class NotificationEntity { 15 | 16 | @Id 17 | @SequenceGenerator( 18 | name = "notification_sequence", 19 | sequenceName = "notification_sequence", 20 | allocationSize = 1 21 | ) 22 | @GeneratedValue( 23 | strategy = GenerationType.SEQUENCE, 24 | generator = "notification_sequence" 25 | ) 26 | @Column( 27 | name = "id", 28 | updatable = false 29 | ) 30 | private Long id; 31 | 32 | @Column( 33 | name = "customer_id", 34 | nullable = false 35 | ) 36 | private Long customerId; 37 | 38 | @Column( 39 | name = "customer_name", 40 | nullable = false, 41 | columnDefinition = "TEXT" 42 | ) 43 | private String customerName; 44 | 45 | @Column( 46 | name = "customer_email", 47 | nullable = false, 48 | columnDefinition = "TEXT" 49 | ) 50 | private String customerEmail; 51 | 52 | @Column( 53 | name = "sender", 54 | nullable = false, 55 | columnDefinition = "TEXT" 56 | ) 57 | private String sender; 58 | 59 | @Column( 60 | name = "message", 61 | nullable = false, 62 | columnDefinition = "TEXT" 63 | ) 64 | private String message; 65 | 66 | @Column( 67 | name = "sent_at", 68 | nullable = false, 69 | columnDefinition = "TIMESTAMP" 70 | ) 71 | private LocalDateTime sentAt; 72 | } 73 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/NotificationMapper.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification; 2 | 3 | import org.mapstruct.Mapper; 4 | 5 | import java.util.List; 6 | 7 | @Mapper(componentModel = "spring") 8 | public interface NotificationMapper { 9 | NotificationEntity toEntity(NotificationDTO dto); 10 | NotificationDTO toDTO(NotificationEntity entity); 11 | List toListDTO(List listEntity); 12 | } 13 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/NotificationRepository.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | @Repository 7 | public interface NotificationRepository extends JpaRepository { 8 | } 9 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/NotificationService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification; 2 | 3 | import dev.nano.clients.notification.NotificationRequest; 4 | 5 | import java.util.List; 6 | 7 | public interface NotificationService { 8 | NotificationDTO getNotification(Long notificationId); 9 | List getAllNotification(); 10 | void sendNotification(NotificationRequest notificationRequest); 11 | } 12 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/NotificationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification; 2 | 3 | import dev.nano.clients.notification.NotificationRequest; 4 | import dev.nano.notification.email.EmailService; 5 | import exceptionhandler.business.NotificationException; 6 | import exceptionhandler.core.ResourceNotFoundException; 7 | import lombok.AllArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.time.LocalDateTime; 12 | import java.util.List; 13 | 14 | import static dev.nano.notification.NotificationConstant.*; 15 | 16 | @Service 17 | @AllArgsConstructor 18 | @Slf4j 19 | public class NotificationServiceImpl implements NotificationService { 20 | 21 | private final NotificationRepository notificationRepository; 22 | private final NotificationMapper notificationMapper; 23 | private final EmailService emailService; 24 | 25 | @Override 26 | public NotificationDTO getNotification(Long notificationId) { 27 | return notificationRepository.findById(notificationId) 28 | .map(notificationMapper::toDTO) 29 | .orElseThrow(() -> new ResourceNotFoundException( 30 | String.format(NOTIFICATION_NOT_FOUND, notificationId) 31 | )); 32 | } 33 | 34 | @Override 35 | public List getAllNotification() { 36 | List notifications = notificationRepository.findAll(); 37 | if (notifications.isEmpty()) { 38 | throw new ResourceNotFoundException(NO_NOTIFICATIONS_FOUND); 39 | } 40 | return notificationMapper.toListDTO(notifications); 41 | } 42 | 43 | @Override 44 | public void sendNotification(NotificationRequest notificationRequest) { 45 | try { 46 | NotificationEntity notification = NotificationEntity.builder() 47 | .customerId(notificationRequest.getCustomerId()) 48 | .customerName(notificationRequest.getCustomerName()) 49 | .customerEmail(notificationRequest.getCustomerEmail()) 50 | .sender("nanodev") 51 | .message(notificationRequest.getMessage()) 52 | .sentAt(LocalDateTime.now()) 53 | .build(); 54 | 55 | notificationRepository.save(notification); 56 | 57 | emailService.send( 58 | notificationRequest.getCustomerEmail(), 59 | buildEmail(notificationRequest.getCustomerName(), notificationRequest.getMessage()), 60 | notificationRequest.getSender() 61 | ); 62 | } catch (Exception e) { 63 | log.error("Failed to send notification: {}", e.getMessage()); 64 | throw new NotificationException(String.format(NOTIFICATION_SEND_ERROR, e.getMessage())); 65 | } 66 | } 67 | 68 | private String buildEmail(String name, String message) { 69 | return String.format(""" 70 | Mail sent to: %s 71 |

%s

72 | """, name, message); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/aws/AWSConfig.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification.aws; 2 | 3 | import com.amazonaws.auth.AWSCredentials; 4 | import com.amazonaws.auth.AWSStaticCredentialsProvider; 5 | import com.amazonaws.auth.BasicAWSCredentials; 6 | import com.amazonaws.regions.Regions; 7 | import com.amazonaws.services.simpleemail.AmazonSimpleEmailService; 8 | import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClientBuilder; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.annotation.Profile; 12 | import org.springframework.core.env.Environment; 13 | 14 | @Configuration 15 | @Profile("eks") 16 | public class AWSConfig { 17 | private final Environment environment; 18 | 19 | public AWSConfig(Environment environment) { 20 | this.environment = environment; 21 | } 22 | 23 | private static final String ACCESS_KEY_BUCKET = "ACCESS_KEY_BUCKET"; 24 | private static final String SECRET_KEY_BUCKET = "ACCESS_KEY_BUCKET"; 25 | 26 | @Bean 27 | AmazonSimpleEmailService emailService() { 28 | AWSCredentials awsCredentials = new BasicAWSCredentials( 29 | environment.getRequiredProperty(ACCESS_KEY_BUCKET), 30 | environment.getRequiredProperty(SECRET_KEY_BUCKET) 31 | ); 32 | return AmazonSimpleEmailServiceClientBuilder 33 | .standard() 34 | .withRegion(Regions.US_EAST_1) 35 | .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) 36 | .build(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/aws/AWSConstant.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification.aws; 2 | 3 | public class AWSConstant { 4 | 5 | public static final String UTF_ENCODING = "UTF-8"; 6 | public static final String SEND_EMAIL_FAILED_EXCEPTION = "Failed to send email"; 7 | public static final String SENDER = "miliari.adnane@gmail.com"; 8 | } 9 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/aws/AWSEmailService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification.aws; 2 | 3 | import com.amazonaws.services.simpleemail.AmazonSimpleEmailService; 4 | import com.amazonaws.services.simpleemail.model.*; 5 | import dev.nano.notification.email.EmailService; 6 | import lombok.AllArgsConstructor; 7 | import org.springframework.context.annotation.Profile; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Service 11 | @AllArgsConstructor 12 | @Profile("eks") 13 | public class AWSEmailService implements EmailService { 14 | 15 | private final AmazonSimpleEmailService emailService; 16 | 17 | public void send(String to, String content, String subject) { 18 | try { 19 | SendEmailRequest request = new SendEmailRequest() 20 | .withDestination(new Destination().withToAddresses(to)) 21 | .withMessage(new Message() 22 | .withBody(new Body() 23 | .withHtml(new Content() 24 | .withCharset(AWSConstant.UTF_ENCODING).withData(content))) 25 | .withSubject(new Content() 26 | .withCharset(AWSConstant.UTF_ENCODING).withData(subject))) 27 | .withSource(AWSConstant.SENDER); 28 | emailService.sendEmail(request); 29 | } catch (AmazonSimpleEmailServiceException e) { 30 | throw new IllegalStateException(AWSConstant.SEND_EMAIL_FAILED_EXCEPTION, e); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/email/DefaultEmailService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification.email; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.context.annotation.Profile; 5 | import org.springframework.stereotype.Service; 6 | 7 | @Service 8 | @Profile("!eks") 9 | @Slf4j 10 | public class DefaultEmailService implements EmailService { 11 | @Override 12 | public void send(String to, String content, String subject) { 13 | log.info("Simulated email sent to: {}, subject: {}, content: {}", to, subject, content); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/email/EmailService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification.email; 2 | 3 | public interface EmailService { 4 | void send(String to, String content, String subject); 5 | } 6 | -------------------------------------------------------------------------------- /notification/src/main/java/dev/nano/notification/rabbitmq/NotificationConsumer.java: -------------------------------------------------------------------------------- 1 | package dev.nano.notification.rabbitmq; 2 | 3 | import dev.nano.clients.notification.NotificationRequest; 4 | import dev.nano.notification.NotificationService; 5 | import lombok.AllArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | @AllArgsConstructor @Slf4j 12 | public class NotificationConsumer { 13 | 14 | private final NotificationService notificationService; 15 | 16 | @RabbitListener(queues = "${rabbitmq.queue.notification}") 17 | public void consumer(NotificationRequest notificationRequest) { 18 | log.info("Consumed {} from queue", notificationRequest); 19 | notificationService.sendNotification(notificationRequest); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /notification/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8004 3 | error: 4 | include-message: always 5 | 6 | spring: 7 | application: 8 | name: notification 9 | config: 10 | import: 11 | - classpath:shared-application-${spring.profiles.active}.yml 12 | 13 | #RabbitMQ 14 | rabbitmq: 15 | exchange: 16 | internal: internal.exchange 17 | queue: 18 | notification: notification.queue 19 | routing-key: 20 | internal-notification: internal.notification.routing-key 21 | -------------------------------------------------------------------------------- /notification/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ,--. ,--. ,--. ,--. ,---. ,--. ,--. ,--. 2 | | ,'.| | ,---. ,-' '-. `--' / .-' `--' ,---. ,--,--. ,-' '-. `--' ,---. ,--,--, 3 | | |' ' | | .-. | '-. .-' ,--. | `-, ,--. | .--' ' ,-. | '-. .-' ,--. | .-. | | \ 4 | | | ` | ' '-' ' | | | | | .-' | | \ `--. \ '-' | | | | | ' '-' ' | || | 5 | `--' `--' `---' `--' `--' `--' `--' `---' `--`--' `--' `--' `---' `--''--' 6 | -------------------------------------------------------------------------------- /notification/src/main/resources/db/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO notification (id, customer_id, customer_name, customer_email, sender, message, sent_at) 2 | VALUES (nextval('notification_sequence'), 1, 'John Smith', 'john.smith@gmail.com', 'NanoShop', 3 | 'Your order #1 has been confirmed', CURRENT_TIMESTAMP), 4 | (nextval('notification_sequence'), 1, 'John Smith', 'john.smith@gmail.com', 'NanoShop', 5 | 'Your payment for order #1 has been processed', CURRENT_TIMESTAMP), 6 | (nextval('notification_sequence'), 2, 'Emma Wilson', 'emma.wilson@outlook.com', 'NanoShop', 7 | 'Your order #3 has been shipped', CURRENT_TIMESTAMP), 8 | (nextval('notification_sequence'), 3, 'Mohammed Ali', 'mohammed.ali@yahoo.com', 'NanoShop', 9 | 'Payment reminder for order #4', CURRENT_TIMESTAMP), 10 | (nextval('notification_sequence'), 4, 'Sarah Chen', 'sarah.chen@gmail.com', 'NanoShop', 11 | 'Thank you for your purchase! Order #5 confirmed', CURRENT_TIMESTAMP); 12 | -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/OrderApplication.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | import org.springframework.cloud.openfeign.EnableFeignClients; 7 | import org.springframework.context.annotation.PropertySource; 8 | import org.springframework.context.annotation.PropertySources; 9 | 10 | @SpringBootApplication( 11 | scanBasePackages = { 12 | "dev.nano.order", 13 | "dev.nano.amqp" 14 | } 15 | ) 16 | @EnableDiscoveryClient 17 | @EnableFeignClients( 18 | basePackages = "dev.nano.clients" 19 | ) 20 | @PropertySources({ 21 | @PropertySource("classpath:amqp-${spring.profiles.active}.properties"), 22 | @PropertySource("classpath:clients-${spring.profiles.active}.properties") 23 | }) 24 | public class OrderApplication { 25 | public static void main(String[] args) { 26 | SpringApplication.run(OrderApplication.class, args); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/OrderConstant.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class OrderConstant { 7 | public static final String ORDER_URI_REST_API = "/api/v1/orders"; 8 | public static final String ORDER_NOT_FOUND = "Order with ID %d not found"; 9 | public static final String ORDER_CREATE_ERROR = "Failed to create order: %s"; 10 | public static final String NO_ORDERS_FOUND = "No orders found"; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/OrderController.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order; 2 | 3 | import dev.nano.clients.order.OrderRequest; 4 | import io.swagger.v3.oas.annotations.Operation; 5 | import io.swagger.v3.oas.annotations.media.ArraySchema; 6 | import io.swagger.v3.oas.annotations.media.Content; 7 | import io.swagger.v3.oas.annotations.media.Schema; 8 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 9 | import io.swagger.v3.oas.annotations.responses.ApiResponses; 10 | import io.swagger.v3.oas.annotations.tags.Tag; 11 | import lombok.AllArgsConstructor; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | import jakarta.validation.Valid; 18 | import swagger.BaseController; 19 | 20 | import java.util.List; 21 | 22 | import static dev.nano.order.OrderConstant.ORDER_URI_REST_API; 23 | 24 | @RestController 25 | @RequestMapping(path = ORDER_URI_REST_API) 26 | @Tag(name = BaseController.ORDER_TAG, description = BaseController.ORDER_DESCRIPTION) 27 | @AllArgsConstructor 28 | @Slf4j 29 | public class OrderController { 30 | 31 | private final OrderService orderService; 32 | 33 | @Operation( 34 | summary = "Get order by ID", 35 | description = "Retrieve detailed information about a specific order using its unique identifier" 36 | ) 37 | @ApiResponses(value = { 38 | @ApiResponse( 39 | responseCode = "200", 40 | description = "Order found successfully", 41 | content = @Content( 42 | mediaType = "application/json", 43 | schema = @Schema(implementation = OrderDTO.class) 44 | ) 45 | ), 46 | @ApiResponse(responseCode = "404", description = "Order not found"), 47 | @ApiResponse(responseCode = "500", description = "Internal server error") 48 | }) 49 | @GetMapping("/{orderId}") 50 | public ResponseEntity getOrder(@PathVariable("orderId") Long orderId) { 51 | log.info("Retrieving order with id {}", orderId); 52 | return ResponseEntity.ok(orderService.getOrder(orderId)); 53 | } 54 | 55 | @Operation( 56 | summary = "Get all orders", 57 | description = "Retrieve a list of all orders with their details" 58 | ) 59 | @ApiResponses(value = { 60 | @ApiResponse( 61 | responseCode = "200", 62 | description = "Orders retrieved successfully", 63 | content = @Content( 64 | mediaType = "application/json", 65 | array = @ArraySchema(schema = @Schema(implementation = OrderDTO.class)) 66 | ) 67 | ), 68 | @ApiResponse(responseCode = "500", description = "Internal server error") 69 | }) 70 | @GetMapping 71 | public ResponseEntity> getAllOrders() { 72 | log.info("Retrieving all orders"); 73 | return ResponseEntity.ok(orderService.getAllOrders()); 74 | } 75 | 76 | @Operation( 77 | summary = "Create new order", 78 | description = "Place a new order in the system with product and customer details" 79 | ) 80 | @ApiResponses(value = { 81 | @ApiResponse( 82 | responseCode = "201", 83 | description = "Order created successfully", 84 | content = @Content( 85 | mediaType = "application/json", 86 | schema = @Schema(implementation = OrderDTO.class) 87 | ) 88 | ), 89 | @ApiResponse(responseCode = "400", description = "Invalid order data"), 90 | @ApiResponse(responseCode = "404", description = "Product or customer not found"), 91 | @ApiResponse(responseCode = "500", description = "Internal server error") 92 | }) 93 | @PostMapping("/add") 94 | public ResponseEntity createOrder(@Valid @RequestBody OrderRequest orderRequest) { 95 | log.info("Creating new order: {}", orderRequest); 96 | return new ResponseEntity<>( 97 | orderService.createOrder(orderRequest), 98 | HttpStatus.CREATED 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/OrderDTO.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order; 2 | 3 | import jakarta.validation.constraints.Min; 4 | import jakarta.validation.constraints.NotNull; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.time.LocalDateTime; 11 | 12 | @Data 13 | @Builder 14 | @AllArgsConstructor 15 | @NoArgsConstructor 16 | public class OrderDTO { 17 | private Long id; 18 | 19 | @NotNull(message = "Customer ID is required") 20 | private Long customerId; 21 | 22 | @NotNull(message = "Product ID is required") 23 | private Long productId; 24 | 25 | @NotNull(message = "Amount is required") 26 | @Min(value = 1, message = "Amount must be greater than 0") 27 | private double amount; 28 | 29 | private LocalDateTime createAt; 30 | } 31 | -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/OrderEntity.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order; 2 | 3 | import lombok.*; 4 | import lombok.experimental.SuperBuilder; 5 | 6 | import jakarta.persistence.*; 7 | import java.time.LocalDateTime; 8 | 9 | @Getter @Setter 10 | @AllArgsConstructor @NoArgsConstructor 11 | @ToString @SuperBuilder 12 | @Entity(name = "Order") 13 | @Table( 14 | name = "\"order\"", 15 | schema = "public" 16 | ) 17 | public class OrderEntity { 18 | 19 | @Id 20 | @SequenceGenerator( 21 | name = "order_sequence", 22 | sequenceName = "order_sequence", 23 | allocationSize = 1 24 | ) 25 | @GeneratedValue( 26 | strategy = GenerationType.SEQUENCE, 27 | generator = "order_sequence" 28 | ) 29 | @Column( 30 | name = "id", 31 | updatable = false 32 | ) 33 | private Long id; 34 | 35 | @Column( 36 | name = "customer_id", 37 | nullable = false 38 | ) 39 | private Long customerId; 40 | 41 | @Column( 42 | name = "product_id", 43 | nullable = false 44 | ) 45 | private Long productId; 46 | 47 | @Column( 48 | name = "amount", 49 | nullable = false 50 | ) 51 | private double amount; 52 | 53 | @Column( 54 | name = "create_at", 55 | nullable = false, 56 | columnDefinition = "TIMESTAMP" 57 | ) 58 | private LocalDateTime createAt; 59 | } 60 | -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/OrderMapper.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order; 2 | 3 | import org.mapstruct.Mapper; 4 | 5 | import java.util.List; 6 | 7 | @Mapper(componentModel = "spring") 8 | public interface OrderMapper { 9 | OrderEntity toEntity(OrderDTO dto); 10 | OrderDTO toDTO(OrderEntity entity); 11 | List toListDTO(List listEntity); 12 | } 13 | -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | @Repository 7 | public interface OrderRepository extends JpaRepository { 8 | } 9 | -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/OrderService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order; 2 | 3 | import dev.nano.clients.order.OrderRequest; 4 | 5 | import java.util.List; 6 | 7 | public interface OrderService { 8 | OrderDTO getOrder(Long id); 9 | List getAllOrders(); 10 | OrderDTO createOrder(OrderRequest order); 11 | } 12 | -------------------------------------------------------------------------------- /order/src/main/java/dev/nano/order/OrderServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.nano.order; 2 | 3 | import dev.nano.amqp.RabbitMQProducer; 4 | import dev.nano.clients.notification.NotificationRequest; 5 | import dev.nano.clients.order.OrderRequest; 6 | import dev.nano.clients.product.ProductClient; 7 | import exceptionhandler.business.NotificationException; 8 | import exceptionhandler.business.OrderException; 9 | import exceptionhandler.core.ResourceNotFoundException; 10 | import feign.FeignException; 11 | import lombok.AllArgsConstructor; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.stereotype.Service; 14 | 15 | import java.time.LocalDateTime; 16 | import java.util.List; 17 | 18 | import static dev.nano.order.OrderConstant.*; 19 | 20 | @Service 21 | @AllArgsConstructor 22 | @Slf4j 23 | public class OrderServiceImpl implements OrderService { 24 | private final OrderRepository orderRepository; 25 | private final OrderMapper orderMapper; 26 | private final ProductClient productClient; 27 | private final RabbitMQProducer rabbitMQProducer; 28 | 29 | @Override 30 | public OrderDTO getOrder(Long id) { 31 | return orderRepository.findById(id) 32 | .map(orderMapper::toDTO) 33 | .orElseThrow(() -> new ResourceNotFoundException( 34 | String.format(ORDER_NOT_FOUND, id) 35 | )); 36 | } 37 | 38 | @Override 39 | public List getAllOrders() { 40 | List orders = orderRepository.findAll(); 41 | if (orders.isEmpty()) { 42 | throw new ResourceNotFoundException(NO_ORDERS_FOUND); 43 | } 44 | return orderMapper.toListDTO(orders); 45 | } 46 | 47 | @Override 48 | public OrderDTO createOrder(OrderRequest orderRequest) { 49 | try { 50 | // Verify product exists 51 | productClient.getProduct(orderRequest.getProductId()); 52 | 53 | OrderEntity order = OrderEntity.builder() 54 | .customerId(orderRequest.getCustomerId()) 55 | .productId(orderRequest.getProductId()) 56 | .amount(orderRequest.getAmount()) 57 | .createAt(LocalDateTime.now()) 58 | .build(); 59 | 60 | OrderEntity savedOrder = orderRepository.save(order); 61 | sendOrderNotification(orderRequest); 62 | 63 | return orderMapper.toDTO(savedOrder); 64 | } catch (FeignException e) { 65 | throw new OrderException(String.format(ORDER_CREATE_ERROR, e.getMessage())); 66 | } 67 | } 68 | 69 | private void sendOrderNotification(OrderRequest order) { 70 | try { 71 | NotificationRequest notificationRequest = NotificationRequest.builder() 72 | .customerId(order.getCustomerId()) 73 | .customerName(order.getCustomerName()) 74 | .customerEmail(order.getCustomerEmail()) 75 | .sender("NanoDev") 76 | .message("Your order has been created successfully") 77 | .build(); 78 | 79 | rabbitMQProducer.publish( 80 | "internal.exchange", 81 | "internal.notification.routing-key", 82 | notificationRequest 83 | ); 84 | } catch (Exception e) { 85 | log.error("Failed to send order notification: {}", e.getMessage()); 86 | throw new NotificationException("Failed to send order notification"); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /order/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8003 3 | error: 4 | include-message: always 5 | 6 | spring: 7 | application: 8 | name: order 9 | config: 10 | import: 11 | - classpath:shared-application-${spring.profiles.active}.yml 12 | -------------------------------------------------------------------------------- /order/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ,-----. ,--. 2 | ' .-. ' ,--.--. ,-| | ,---. ,--.--. 3 | | | | | | .--' ' .-. | | .-. : | .--' 4 | ' '-' ' | | \ `-' | \ --. | | 5 | `-----' `--' `---' `----' `--' 6 | -------------------------------------------------------------------------------- /order/src/main/resources/db/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO "order" (id, customer_id, product_id, amount, create_at) 2 | VALUES (nextval('order_sequence'), 1, 1, 2, CURRENT_TIMESTAMP), 3 | (nextval('order_sequence'), 1, 3, 1, CURRENT_TIMESTAMP), 4 | (nextval('order_sequence'), 2, 2, 3, CURRENT_TIMESTAMP), 5 | (nextval('order_sequence'), 3, 4, 1, CURRENT_TIMESTAMP), 6 | (nextval('order_sequence'), 4, 1, 2, CURRENT_TIMESTAMP); 7 | -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/PaymentApplication.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | import org.springframework.cloud.openfeign.EnableFeignClients; 7 | import org.springframework.context.annotation.PropertySource; 8 | import org.springframework.context.annotation.PropertySources; 9 | 10 | 11 | 12 | @SpringBootApplication( 13 | scanBasePackages = { 14 | "dev.nano.payment", 15 | "dev.nano.amqp" 16 | } 17 | ) 18 | @EnableDiscoveryClient 19 | @EnableFeignClients( 20 | basePackages = "dev.nano.clients" 21 | ) 22 | @PropertySources({ 23 | @PropertySource("classpath:amqp-${spring.profiles.active}.properties"), 24 | @PropertySource("classpath:clients-${spring.profiles.active}.properties") 25 | }) 26 | public class PaymentApplication { 27 | public static void main(String[] args) { 28 | SpringApplication.run(PaymentApplication.class, args); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/PaymentConstant.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class PaymentConstant { 7 | public static final String PAYMENT_URI_REST_API = "/api/v1/payments"; 8 | public static final String PAYMENT_NOT_FOUND = "Payment with ID %d not found"; 9 | public static final String PAYMENT_CREATE_ERROR = "Failed to create payment: %s"; 10 | public static final String NO_PAYMENTS_FOUND = "No payments found"; 11 | } 12 | -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/PaymentController.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment; 2 | 3 | import dev.nano.clients.payment.PaymentRequest; 4 | import io.swagger.v3.oas.annotations.Operation; 5 | import io.swagger.v3.oas.annotations.media.ArraySchema; 6 | import io.swagger.v3.oas.annotations.media.Content; 7 | import io.swagger.v3.oas.annotations.media.Schema; 8 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 9 | import io.swagger.v3.oas.annotations.responses.ApiResponses; 10 | import io.swagger.v3.oas.annotations.tags.Tag; 11 | import jakarta.validation.Valid; 12 | import lombok.AllArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.http.ResponseEntity; 16 | import org.springframework.web.bind.annotation.*; 17 | import swagger.BaseController; 18 | 19 | import java.util.List; 20 | 21 | import static dev.nano.payment.PaymentConstant.PAYMENT_URI_REST_API; 22 | 23 | @RestController 24 | @RequestMapping(path = PAYMENT_URI_REST_API) 25 | @Tag(name = BaseController.PAYMENT_TAG, description = BaseController.PAYMENT_DESCRIPTION) 26 | @AllArgsConstructor 27 | @Slf4j 28 | public class PaymentController { 29 | 30 | private final PaymentService paymentService; 31 | 32 | @Operation( 33 | summary = "Get payment by ID", 34 | description = "Retrieve detailed information about a specific payment using its unique identifier" 35 | ) 36 | @ApiResponses(value = { 37 | @ApiResponse( 38 | responseCode = "200", 39 | description = "Payment found successfully", 40 | content = @Content( 41 | mediaType = "application/json", 42 | schema = @Schema(implementation = PaymentDTO.class) 43 | ) 44 | ), 45 | @ApiResponse(responseCode = "404", description = "Payment not found"), 46 | @ApiResponse(responseCode = "500", description = "Internal server error") 47 | }) 48 | @GetMapping("/{paymentId}") 49 | public ResponseEntity getPayment(@PathVariable("paymentId") Long paymentId) { 50 | log.info("Retrieving payment with id {}", paymentId); 51 | return ResponseEntity.ok(paymentService.getPayment(paymentId)); 52 | } 53 | 54 | @Operation( 55 | summary = "Get all payments", 56 | description = "Retrieve a list of all payments" 57 | ) 58 | @ApiResponses(value = { 59 | @ApiResponse( 60 | responseCode = "200", 61 | description = "Payments retrieved successfully", 62 | content = @Content( 63 | mediaType = "application/json", 64 | array = @ArraySchema(schema = @Schema(implementation = PaymentDTO.class)) 65 | ) 66 | ), 67 | @ApiResponse(responseCode = "500", description = "Internal server error") 68 | }) 69 | @GetMapping 70 | public ResponseEntity> getAllPayments() { 71 | log.info("Retrieving all payments"); 72 | return ResponseEntity.ok(paymentService.getAllPayments()); 73 | } 74 | 75 | @Operation( 76 | summary = "Process new payment", 77 | description = "Process a new payment transaction for an order" 78 | ) 79 | @ApiResponses(value = { 80 | @ApiResponse( 81 | responseCode = "201", 82 | description = "Payment processed successfully", 83 | content = @Content( 84 | mediaType = "application/json", 85 | schema = @Schema(implementation = PaymentDTO.class) 86 | ) 87 | ), 88 | @ApiResponse(responseCode = "400", description = "Invalid payment data"), 89 | @ApiResponse(responseCode = "404", description = "Order not found"), 90 | @ApiResponse(responseCode = "500", description = "Internal server error") 91 | }) 92 | @PostMapping("/make-new-payment") 93 | public ResponseEntity createPayment(@Valid @RequestBody PaymentRequest payment) { 94 | log.info("Processing new payment: {}", payment); 95 | return new ResponseEntity<>( 96 | paymentService.createPayment(payment), 97 | HttpStatus.CREATED 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/PaymentDTO.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.time.LocalDateTime; 10 | 11 | @Data 12 | @Builder 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | public class PaymentDTO { 16 | private Long id; 17 | 18 | @NotNull(message = "Customer ID is required") 19 | private Long customerId; 20 | 21 | @NotNull(message = "Order ID is required") 22 | private Long orderId; 23 | 24 | private LocalDateTime createAt; 25 | } 26 | -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/PaymentEntity.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment; 2 | 3 | import lombok.*; 4 | import lombok.experimental.SuperBuilder; 5 | 6 | import jakarta.persistence.*; 7 | import java.time.LocalDateTime; 8 | 9 | @Getter @Setter 10 | @AllArgsConstructor @NoArgsConstructor 11 | @ToString @SuperBuilder 12 | @Entity(name = "Payment") 13 | @Table(name = "payment") 14 | public class PaymentEntity { 15 | 16 | @Id 17 | @SequenceGenerator( 18 | name = "payment_sequence", 19 | sequenceName = "payment_sequence", 20 | allocationSize = 1 21 | ) 22 | @GeneratedValue( 23 | strategy = GenerationType.SEQUENCE, 24 | generator = "payment_sequence" 25 | ) 26 | @Column( 27 | name = "id", 28 | updatable = false 29 | ) 30 | private Long id; 31 | 32 | @Column( 33 | name = "customer_id", 34 | nullable = false 35 | ) 36 | private Long customerId; 37 | 38 | @Column( 39 | name = "order_id", 40 | nullable = false 41 | ) 42 | private Long orderId; 43 | 44 | @Column( 45 | name = "create_at", 46 | nullable = false, 47 | columnDefinition = "TIMESTAMP" 48 | ) 49 | private LocalDateTime createAt; 50 | } 51 | -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/PaymentMapper.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment; 2 | 3 | import org.mapstruct.Mapper; 4 | import java.util.List; 5 | 6 | @Mapper(componentModel = "spring") 7 | public interface PaymentMapper { 8 | PaymentDTO toDTO(PaymentEntity entity); 9 | PaymentEntity toEntity(PaymentDTO dto); 10 | List toListDTO(List entities); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/PaymentRepository.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | @Repository 7 | public interface PaymentRepository extends JpaRepository { 8 | } 9 | -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/PaymentService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment; 2 | 3 | import dev.nano.clients.payment.PaymentRequest; 4 | 5 | import java.util.List; 6 | 7 | public interface PaymentService { 8 | PaymentDTO getPayment(Long paymentId); 9 | List getAllPayments(); 10 | PaymentDTO createPayment(PaymentRequest payment); 11 | } 12 | -------------------------------------------------------------------------------- /payment/src/main/java/dev/nano/payment/PaymentServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.nano.payment; 2 | 3 | import dev.nano.amqp.RabbitMQProducer; 4 | import dev.nano.clients.notification.NotificationRequest; 5 | import dev.nano.clients.order.OrderClient; 6 | import dev.nano.clients.payment.PaymentRequest; 7 | import exceptionhandler.business.NotificationException; 8 | import exceptionhandler.business.PaymentException; 9 | import exceptionhandler.core.ResourceNotFoundException; 10 | import feign.FeignException; 11 | import lombok.AllArgsConstructor; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.stereotype.Service; 14 | 15 | import java.time.LocalDateTime; 16 | import java.util.List; 17 | 18 | import static dev.nano.payment.PaymentConstant.*; 19 | 20 | @Service 21 | @AllArgsConstructor 22 | @Slf4j 23 | public class PaymentServiceImpl implements PaymentService { 24 | 25 | private final PaymentRepository paymentRepository; 26 | private final PaymentMapper paymentMapper; 27 | private final OrderClient orderClient; 28 | private final RabbitMQProducer rabbitMQProducer; 29 | 30 | @Override 31 | public PaymentDTO getPayment(Long paymentId) { 32 | return paymentRepository.findById(paymentId) 33 | .map(paymentMapper::toDTO) 34 | .orElseThrow(() -> new ResourceNotFoundException( 35 | String.format(PAYMENT_NOT_FOUND, paymentId) 36 | )); 37 | } 38 | 39 | @Override 40 | public List getAllPayments() { 41 | List payments = paymentRepository.findAll(); 42 | if (payments.isEmpty()) { 43 | throw new ResourceNotFoundException(NO_PAYMENTS_FOUND); 44 | } 45 | return paymentMapper.toListDTO(payments); 46 | } 47 | 48 | @Override 49 | public PaymentDTO createPayment(PaymentRequest paymentRequest) { 50 | try { 51 | // Verify order exists 52 | orderClient.getOrder(paymentRequest.getOrderId()); 53 | 54 | PaymentEntity payment = PaymentEntity.builder() 55 | .customerId(paymentRequest.getCustomerId()) 56 | .orderId(paymentRequest.getOrderId()) 57 | .createAt(LocalDateTime.now()) 58 | .build(); 59 | 60 | PaymentEntity savedPayment = paymentRepository.save(payment); 61 | sendPaymentNotification(paymentRequest); 62 | 63 | return paymentMapper.toDTO(savedPayment); 64 | } catch (FeignException e) { 65 | throw new PaymentException(String.format(PAYMENT_CREATE_ERROR, e.getMessage())); 66 | } 67 | } 68 | 69 | private void sendPaymentNotification(PaymentRequest payment) { 70 | try { 71 | NotificationRequest notificationRequest = NotificationRequest.builder() 72 | .customerId(payment.getCustomerId()) 73 | .customerName(payment.getCustomerName()) 74 | .customerEmail(payment.getCustomerEmail()) 75 | .sender("nanodev") 76 | .message("Your payment has been processed successfully") 77 | .build(); 78 | 79 | rabbitMQProducer.publish( 80 | "internal.exchange", 81 | "internal.notification.routing-key", 82 | notificationRequest 83 | ); 84 | } catch (Exception e) { 85 | log.error("Failed to send payment notification: {}", e.getMessage()); 86 | throw new NotificationException("Failed to send payment notification"); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /payment/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8005 3 | error: 4 | include-message: always 5 | 6 | spring: 7 | application: 8 | name: payment 9 | config: 10 | import: 11 | - classpath:shared-application-${spring.profiles.active}.yml 12 | -------------------------------------------------------------------------------- /payment/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ,------. ,--. 2 | | .--. ' ,--,--. ,--. ,--. ,--,--,--. ,---. ,--,--, ,-' '-. 3 | | '--' | ' ,-. | \ ' / | | | .-. : | \ '-. .-' 4 | | | --' \ '-' | \ ' | | | | \ --. | || | | | 5 | `--' `--`--' .-' / `--`--`--' `----' `--''--' `--' -------------------------------------------------------------------------------- /payment/src/main/resources/db/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO payment (id, customer_id, order_id, create_at) 2 | VALUES (nextval('payment_sequence'), 1, 1, CURRENT_TIMESTAMP), 3 | (nextval('payment_sequence'), 1, 2, CURRENT_TIMESTAMP), 4 | (nextval('payment_sequence'), 2, 3, CURRENT_TIMESTAMP), 5 | (nextval('payment_sequence'), 3, 4, CURRENT_TIMESTAMP), 6 | (nextval('payment_sequence'), 4, 5, CURRENT_TIMESTAMP); 7 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | dev.nano 6 | demo-microservices 7 | pom 8 | 1.0-SNAPSHOT 9 | 10 | 11 | common 12 | eureka-server 13 | gateway 14 | customer 15 | product 16 | order 17 | notification 18 | payment 19 | amqp 20 | feign-clients 21 | apiKey-manager 22 | 23 | 24 | demo-microservices 25 | 26 | http://www.example.com 27 | 28 | 29 | UTF-8 30 | 17 31 | 17 32 | 3.11.0 33 | 1.18.36 34 | 1.5.5.Final 35 | 2.3.0 36 | 3.4.0 37 | 0.2.0 38 | 3.3.6 39 | 3.3.6 40 | 2023.0.4 41 | miliariadnane/${project.artifactId} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-dependencies 52 | ${spring.boot.dependencies.version} 53 | import 54 | pom 55 | 56 | 57 | org.springframework.cloud 58 | spring-cloud-dependencies 59 | ${spring.cloud.dependencies.version} 60 | import 61 | pom 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | org.projectlombok 70 | lombok 71 | 72 | 73 | org.springframework.cloud 74 | spring-cloud-starter-openfeign 75 | 76 | 77 | org.springdoc 78 | springdoc-openapi-starter-webmvc-ui 79 | ${springdoc.version} 80 | 81 | 82 | org.springframework.boot 83 | spring-boot-starter-test 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | org.springframework.boot 94 | spring-boot-maven-plugin 95 | 96 | ${spring.boot.maven.plugin.version} 97 | 98 | 99 | 100 | repackage 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | org.apache.maven.plugins 111 | maven-compiler-plugin 112 | 3.13.0 113 | 114 | 17 115 | 116 | 117 | 118 | org.openrewrite.maven 119 | rewrite-maven-plugin 120 | 5.45.1 121 | 122 | 123 | org.openrewrite.java.spring.boot3.SpringBoot3BestPractices 124 | org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_3 125 | 126 | 127 | 128 | 129 | org.openrewrite.recipe 130 | rewrite-spring 131 | 5.23.2 132 | 133 | 134 | org.openrewrite.recipe 135 | rewrite-migrate-java 136 | 2.29.1 137 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /product/src/main/java/dev/nano/product/ProductApplication.java: -------------------------------------------------------------------------------- 1 | package dev.nano.product; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | 7 | @SpringBootApplication 8 | @EnableDiscoveryClient 9 | public class ProductApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(ProductApplication.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /product/src/main/java/dev/nano/product/ProductConstant.java: -------------------------------------------------------------------------------- 1 | package dev.nano.product; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class ProductConstant { 7 | public static final String PRODUCT_URI_REST_API = "/api/v1/products"; 8 | public static final String PRODUCT_NOT_FOUND = "Product with ID %d not found"; 9 | public static final String PRODUCT_CREATE_ERROR = "Failed to create product: %s"; 10 | public static final String PRODUCT_DELETE_ERROR = "Failed to delete product: %s"; 11 | public static final String NO_PRODUCTS_FOUND = "No products found"; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /product/src/main/java/dev/nano/product/ProductController.java: -------------------------------------------------------------------------------- 1 | package dev.nano.product; 2 | 3 | import io.swagger.v3.oas.annotations.Operation; 4 | import io.swagger.v3.oas.annotations.media.ArraySchema; 5 | import io.swagger.v3.oas.annotations.media.Content; 6 | import io.swagger.v3.oas.annotations.media.Schema; 7 | import io.swagger.v3.oas.annotations.responses.ApiResponse; 8 | import io.swagger.v3.oas.annotations.responses.ApiResponses; 9 | import io.swagger.v3.oas.annotations.tags.Tag; 10 | import jakarta.validation.Valid; 11 | import lombok.AllArgsConstructor; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.web.bind.annotation.*; 16 | import swagger.BaseController; 17 | 18 | import java.util.List; 19 | 20 | import static dev.nano.product.ProductConstant.PRODUCT_URI_REST_API; 21 | 22 | @RestController 23 | @RequestMapping(path = PRODUCT_URI_REST_API) 24 | @Tag(name = BaseController.PRODUCT_TAG, description = BaseController.PRODUCT_DESCRIPTION) 25 | @AllArgsConstructor @Slf4j 26 | public class ProductController { 27 | 28 | private final ProductService productService; 29 | 30 | @Operation( 31 | summary = "Get product by ID", 32 | description = "Retrieve detailed information about a specific product using its unique identifier" 33 | ) 34 | @ApiResponses(value = { 35 | @ApiResponse( 36 | responseCode = "200", 37 | description = "Product found successfully", 38 | content = @Content( 39 | mediaType = "application/json", 40 | schema = @Schema(implementation = ProductDTO.class) 41 | ) 42 | ), 43 | @ApiResponse(responseCode = "404", description = "Product not found"), 44 | @ApiResponse(responseCode = "500", description = "Internal server error") 45 | }) 46 | @GetMapping("/{productId}") 47 | public ResponseEntity getProduct(@PathVariable("productId") Long productId) { 48 | log.info("Retrieving product with id {}", productId); 49 | return ResponseEntity.ok(productService.getProduct(productId)); 50 | } 51 | 52 | @Operation( 53 | summary = "Get all products with filtering and pagination", 54 | description = "Retrieve a paginated list of products with optional search criteria" 55 | ) 56 | @ApiResponses(value = { 57 | @ApiResponse( 58 | responseCode = "200", 59 | description = "Products retrieved successfully", 60 | content = @Content( 61 | mediaType = "application/json", 62 | array = @ArraySchema(schema = @Schema(implementation = ProductDTO.class)) 63 | ) 64 | ), 65 | @ApiResponse(responseCode = "500", description = "Internal server error") 66 | }) 67 | @GetMapping("/list") 68 | public ResponseEntity> getAllProducts( 69 | @RequestParam(value="page", defaultValue = "1") int page, 70 | @RequestParam(value="limit", defaultValue = "10") int limit, 71 | @RequestParam(value="search", defaultValue = "") String search) { 72 | log.info("Retrieving all products with page {}, limit {}, search {}", page, limit, search); 73 | return ResponseEntity.ok(productService.getAllProducts(page, limit, search)); 74 | } 75 | 76 | @Operation( 77 | summary = "Create new product", 78 | description = "Add a new product to the catalog" 79 | ) 80 | @ApiResponses(value = { 81 | @ApiResponse( 82 | responseCode = "201", 83 | description = "Product created successfully", 84 | content = @Content( 85 | mediaType = "application/json", 86 | schema = @Schema(implementation = ProductDTO.class) 87 | ) 88 | ), 89 | @ApiResponse(responseCode = "400", description = "Invalid product data"), 90 | @ApiResponse(responseCode = "500", description = "Internal server error") 91 | }) 92 | @PostMapping 93 | public ResponseEntity createProduct(@Valid @RequestBody ProductDTO productDTO) { 94 | log.info("Creating new product: {}", productDTO); 95 | return new ResponseEntity<>( 96 | productService.create(productDTO), 97 | HttpStatus.CREATED 98 | ); 99 | } 100 | 101 | @Operation( 102 | summary = "Update product", 103 | description = "Modify existing product information" 104 | ) 105 | @ApiResponses(value = { 106 | @ApiResponse( 107 | responseCode = "200", 108 | description = "Product updated successfully", 109 | content = @Content( 110 | mediaType = "application/json", 111 | schema = @Schema(implementation = ProductDTO.class) 112 | ) 113 | ), 114 | @ApiResponse(responseCode = "400", description = "Invalid product data"), 115 | @ApiResponse(responseCode = "404", description = "Product not found"), 116 | @ApiResponse(responseCode = "500", description = "Internal server error") 117 | }) 118 | @PutMapping("/{productId}") 119 | public ResponseEntity updateProduct( 120 | @PathVariable Long productId, 121 | @Valid @RequestBody ProductDTO productDTO) { 122 | log.info("Updating product with ID {}: {}", productId, productDTO); 123 | return ResponseEntity.ok(productService.update(productId, productDTO)); 124 | } 125 | 126 | @Operation( 127 | summary = "Delete product", 128 | description = "Remove a product from the catalog" 129 | ) 130 | @ApiResponses(value = { 131 | @ApiResponse(responseCode = "204", description = "Product deleted successfully"), 132 | @ApiResponse(responseCode = "404", description = "Product not found"), 133 | @ApiResponse(responseCode = "500", description = "Internal server error") 134 | }) 135 | @DeleteMapping("/{productId}") 136 | public ResponseEntity deleteProduct(@PathVariable Long productId) { 137 | log.info("Deleting product with ID: {}", productId); 138 | productService.delete(productId); 139 | return ResponseEntity.noContent().build(); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /product/src/main/java/dev/nano/product/ProductDTO.java: -------------------------------------------------------------------------------- 1 | package dev.nano.product; 2 | 3 | import jakarta.validation.constraints.Min; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | import jakarta.validation.constraints.Size; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | import org.hibernate.validator.constraints.URL; 12 | 13 | @Data 14 | @Builder 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | public class ProductDTO { 18 | private Long id; 19 | 20 | @NotBlank(message = "Product name is required") 21 | @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters") 22 | private String name; 23 | 24 | @NotNull(message = "Price is required") 25 | @Min(value = 0, message = "Price must be greater than or equal to 0") 26 | private Integer price; 27 | 28 | @URL(message = "Invalid image URL") 29 | private String image; 30 | } 31 | 32 | -------------------------------------------------------------------------------- /product/src/main/java/dev/nano/product/ProductEntity.java: -------------------------------------------------------------------------------- 1 | package dev.nano.product; 2 | 3 | import lombok.*; 4 | import lombok.experimental.SuperBuilder; 5 | 6 | import jakarta.persistence.*; 7 | 8 | @Getter @Setter 9 | @AllArgsConstructor @NoArgsConstructor 10 | @ToString @SuperBuilder 11 | @Entity(name = "Product") 12 | @Table(name = "product") 13 | public class ProductEntity { 14 | 15 | @Id 16 | @SequenceGenerator( 17 | name = "product_sequence", 18 | sequenceName = "product_sequence", 19 | allocationSize = 1 20 | ) 21 | @GeneratedValue( 22 | strategy = GenerationType.SEQUENCE, 23 | generator = "product_sequence" 24 | ) 25 | @Column( 26 | name = "id", 27 | updatable = false 28 | ) 29 | private Long id; 30 | 31 | @Column( 32 | name = "name", 33 | nullable = false, 34 | columnDefinition = "TEXT" 35 | ) 36 | private String name; 37 | 38 | @Column( 39 | name = "image", 40 | columnDefinition = "TEXT" 41 | ) 42 | private String image; 43 | 44 | @Column( 45 | name = "price", 46 | nullable = false, 47 | columnDefinition = "INTEGER" 48 | ) 49 | private Integer price; 50 | } 51 | -------------------------------------------------------------------------------- /product/src/main/java/dev/nano/product/ProductMapper.java: -------------------------------------------------------------------------------- 1 | package dev.nano.product; 2 | 3 | import org.mapstruct.Mapper; 4 | import org.mapstruct.MappingTarget; 5 | 6 | import java.util.List; 7 | 8 | @Mapper(componentModel = "spring") 9 | public interface ProductMapper { 10 | ProductDTO toDTO(ProductEntity entity); 11 | ProductEntity toEntity(ProductDTO dto); 12 | List toListDTO(List entities); 13 | void updateProductFromDTO(ProductDTO dto, @MappingTarget ProductEntity entity); 14 | } 15 | 16 | -------------------------------------------------------------------------------- /product/src/main/java/dev/nano/product/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package dev.nano.product; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.data.domain.Pageable; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.jpa.repository.Query; 7 | import org.springframework.data.repository.query.Param; 8 | import org.springframework.stereotype.Repository; 9 | 10 | @Repository 11 | public interface ProductRepository extends JpaRepository { 12 | 13 | @Query(value="SELECT * FROM products", nativeQuery=true) 14 | Page findAllProducts(Pageable pageableRequest); 15 | 16 | @Query(value="SELECT * FROM products p WHERE (p.name LIKE %:search%) ", nativeQuery=true) 17 | Page findAllProductsByCriteria(Pageable pageableRequest, @Param("search") String search); 18 | } 19 | -------------------------------------------------------------------------------- /product/src/main/java/dev/nano/product/ProductService.java: -------------------------------------------------------------------------------- 1 | package dev.nano.product; 2 | 3 | import java.util.List; 4 | 5 | public interface ProductService { 6 | ProductDTO create(ProductDTO product); 7 | ProductDTO getProduct(long productId); 8 | List getAllProducts(int page, int limit, String search); 9 | ProductDTO update(long id, ProductDTO request); 10 | void delete(long id); 11 | } 12 | -------------------------------------------------------------------------------- /product/src/main/java/dev/nano/product/ProductServiceImpl.java: -------------------------------------------------------------------------------- 1 | package dev.nano.product; 2 | 3 | import exceptionhandler.business.ProductException; 4 | import exceptionhandler.core.ResourceNotFoundException; 5 | import lombok.AllArgsConstructor; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.PageRequest; 8 | import org.springframework.data.domain.Pageable; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.List; 12 | 13 | import static dev.nano.product.ProductConstant.*; 14 | 15 | @Service 16 | @AllArgsConstructor 17 | public class ProductServiceImpl implements ProductService { 18 | 19 | private final ProductRepository productRepository; 20 | private final ProductMapper productMapper; 21 | 22 | @Override 23 | public ProductDTO getProduct(long productId) { 24 | return productRepository.findById(productId) 25 | .map(productMapper::toDTO) 26 | .orElseThrow(() -> new ResourceNotFoundException( 27 | String.format(PRODUCT_NOT_FOUND, productId) 28 | )); 29 | } 30 | 31 | @Override 32 | public List getAllProducts(int page, int limit, String search) { 33 | if(page > 0) page = page - 1; 34 | 35 | Pageable pageableRequest = PageRequest.of(page, limit); 36 | Page productPage; 37 | 38 | if(search == null || search.isEmpty()) { 39 | productPage = productRepository.findAllProducts(pageableRequest); 40 | } else { 41 | productPage = productRepository.findAllProductsByCriteria(pageableRequest, search); 42 | } 43 | 44 | List products = productPage.getContent(); 45 | if (products.isEmpty()) { 46 | throw new ResourceNotFoundException(NO_PRODUCTS_FOUND); 47 | } 48 | 49 | return productMapper.toListDTO(products); 50 | } 51 | 52 | @Override 53 | public ProductDTO create(ProductDTO productDTO) { 54 | try { 55 | ProductEntity product = productMapper.toEntity(productDTO); 56 | return productMapper.toDTO(productRepository.save(product)); 57 | } catch (Exception e) { 58 | throw new ProductException(String.format(PRODUCT_CREATE_ERROR, e.getMessage())); 59 | } 60 | } 61 | 62 | @Override 63 | public ProductDTO update(long id, ProductDTO productDTO) { 64 | ProductEntity existingProduct = productRepository.findById(id) 65 | .orElseThrow(() -> new ResourceNotFoundException( 66 | String.format(PRODUCT_NOT_FOUND, id) 67 | )); 68 | 69 | productMapper.updateProductFromDTO(productDTO, existingProduct); 70 | return productMapper.toDTO(productRepository.save(existingProduct)); 71 | } 72 | 73 | @Override 74 | public void delete(long id) { 75 | if (!productRepository.existsById(id)) { 76 | throw new ResourceNotFoundException(String.format(PRODUCT_NOT_FOUND, id)); 77 | } 78 | try { 79 | productRepository.deleteById(id); 80 | } catch (Exception e) { 81 | throw new ProductException(String.format(PRODUCT_DELETE_ERROR, e.getMessage())); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /product/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8002 3 | error: 4 | include-message: always 5 | 6 | spring: 7 | application: 8 | name: product 9 | config: 10 | import: 11 | - classpath:shared-application-${spring.profiles.active}.yml 12 | -------------------------------------------------------------------------------- /product/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ,------. ,--. ,--. 2 | | .--. ' ,--.--. ,---. ,-| | ,--.,--. ,---. ,-' '-. 3 | | '--' | | .--' | .-. | ' .-. | | || | | .--' '-. .-' 4 | | | --' | | ' '-' ' \ `-' | ' '' ' \ `--. | | 5 | `--' `--' `---' `---' `----' `---' `--' 6 | -------------------------------------------------------------------------------- /product/src/main/resources/db/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO product (id, name, image, price) 2 | VALUES 3 | (nextval('product_sequence'), 'MacBook Pro M3', 'https://picsum.photos/id/1/200/200', 1999), 4 | (nextval('product_sequence'), 'iPhone 15 Pro', 'https://picsum.photos/id/2/200/200', 1299), 5 | (nextval('product_sequence'), 'AirPods Pro', 'https://picsum.photos/id/3/200/200', 249), 6 | (nextval('product_sequence'), 'iPad Air', 'https://picsum.photos/id/4/200/200', 599), 7 | (nextval('product_sequence'), 'Apple Watch Series 9', 'https://picsum.photos/id/5/200/200', 499); 8 | --------------------------------------------------------------------------------