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