```
├── .gitignore (100 tokens)
├── .mvn/
├── wrapper/
├── maven-wrapper.properties (200 tokens)
├── Docker_guid.md (100 tokens)
├── README.md (2.7k tokens)
├── account-service/
├── .gitignore (100 tokens)
├── .mvn/
├── wrapper/
├── maven-wrapper.jar
├── maven-wrapper.properties (200 tokens)
├── Dockerfile (200 tokens)
├── mvnw (2.1k tokens)
├── mvnw.cmd (1300 tokens)
├── pom.xml (200 tokens)
├── src/
├── main/
├── java/
├── com/
├── demo/
├── AccountServiceApplication.java (200 tokens)
├── config/
├── DataInitializer.java (200 tokens)
├── OpenApiConfig.java (400 tokens)
├── controller/
├── AuthController.java (300 tokens)
├── HomeController.java (100 tokens)
├── ProductEventController.java (300 tokens)
├── TestController.java (300 tokens)
├── UserController.java (300 tokens)
├── dto/
├── ProductEvent.java (100 tokens)
├── User.java (100 tokens)
├── enums/
├── ERole.java
├── EventType.java
├── kafka/
├── KafkaConfig.java (400 tokens)
├── KafkaUserConsumer.java (400 tokens)
├── ProductEventConsumer.java (800 tokens)
├── model/
├── Role.java (100 tokens)
├── User.java (200 tokens)
├── payload/
├── request/
├── LoginRequest.java (100 tokens)
├── SignupRequest.java (200 tokens)
├── UpdateRequest.java (100 tokens)
├── response/
├── JwtResponse.java (100 tokens)
├── MessageResponse.java (100 tokens)
├── UserProfile.java (100 tokens)
├── repo/
├── RoleRepository.java (100 tokens)
├── UserRepository.java (200 tokens)
├── security/
├── WebSecurityConfig.java (700 tokens)
├── services/
├── UserDetailsImpl.java (300 tokens)
├── UserDetailsServiceImpl.java (200 tokens)
├── service/
├── AuthService.java (1100 tokens)
├── UserService.java (700 tokens)
├── resources/
├── application.yml (500 tokens)
├── logback-spring.xml (400 tokens)
├── templates/
├── home.html
├── commons/
├── pom.xml (1300 tokens)
├── src/
├── main/
├── java/
├── com/
├── demo/
├── config/
├── openfeign/
├── CommonOpenFeignConfig.java (400 tokens)
├── CommonOpenFeignErrorDecoder.java (400 tokens)
├── thread/
├── CommonContext.java (100 tokens)
├── CommonContextTaskDecorator.java (500 tokens)
├── DefaultCommonContext.java (100 tokens)
├── README.md (800 tokens)
├── ThreadAsyncConfig.java (600 tokens)
├── constants/
├── CorrelationConstants.java (100 tokens)
├── context/
├── CommonContextHolder.java (700 tokens)
├── exception/
├── ApplicationException.java (100 tokens)
├── FeignResponseException.java (100 tokens)
├── ForbiddenException.java (100 tokens)
├── FormValidateException.java (100 tokens)
├── HttpException.java (100 tokens)
├── NotFoundException.java (100 tokens)
├── UnauthorizedException.java (100 tokens)
├── UnprocessableEntityException.java (100 tokens)
├── handler/
├── CommonExceptionHandler.java (900 tokens)
├── model/
├── ApiError.java (100 tokens)
├── ErrorCode.java
├── kafka/
├── interceptor/
├── KafkaProducerInterceptor.java (200 tokens)
├── KafkaRecordInterceptor.java (300 tokens)
├── logging/
├── CommonRequestBodyLogger.java (500 tokens)
├── CommonResponseBodyLogger.java (500 tokens)
├── CorrelationLoggingFilter.java (700 tokens)
├── OpenFeignRequestInterceptor.java (100 tokens)
├── security/
├── CORSFilter.java (300 tokens)
├── WebConfiguration.java (100 tokens)
├── jwt/
├── AuthEntryPointJwt.java (300 tokens)
├── AuthTokenFilter.java (500 tokens)
├── JwtUtils.java (700 tokens)
├── service/
├── AsyncExampleService.java (1000 tokens)
├── util/
├── AsyncUtils.java (800 tokens)
├── AuthUtils.java (600 tokens)
├── CorrelationUtils.java (100 tokens)
├── StringToDateConverter.java (200 tokens)
├── docker-compose.yml (1100 tokens)
├── docs/
├── 01-Quick-Setup.md (1100 tokens)
├── 02-System-Architecture.md (4.8k tokens)
├── 03-API-Reference.md (2.6k tokens)
├── 04-Development-Guide.md (3.4k tokens)
├── 05-Testing-Guide.md (3.7k tokens)
├── 06-Debugging-Guide.md (2.3k tokens)
├── 07-Docker-Operations.md (2.2k tokens)
├── 08-Monitoring-Logging.md (2.9k tokens)
├── 09-Troubleshooting.md (3.4k tokens)
├── 10-Postman-Collection.md (1500 tokens)
├── 11-Configuration-Reference.md (2.6k tokens)
├── e2e.sh (700 tokens)
├── fluentd/
├── Dockerfile
├── conf/
├── fluent.conf (200 tokens)
├── mvnw (2.1k tokens)
├── mvnw.cmd (1300 tokens)
├── pom.xml (500 tokens)
├── product-service/
├── .gitignore (100 tokens)
├── .mvn/
├── wrapper/
├── maven-wrapper.jar
├── maven-wrapper.properties
├── Dockerfile (200 tokens)
├── mvnw (2.3k tokens)
├── mvnw.cmd (1500 tokens)
├── pom.xml (200 tokens)
├── src/
├── main/
├── java/
├── com/
├── demo/
├── ProductServiceApplication.java (300 tokens)
├── aop/
├── ProductAop.java (100 tokens)
├── config/
├── OpenApiConfig.java (400 tokens)
├── controller/
├── AsyncTestController.java (800 tokens)
├── HomeController.java (100 tokens)
├── ProductController.java (600 tokens)
├── UserController.java (300 tokens)
├── dto/
├── ProductEvent.java (400 tokens)
├── ProductRequest.java (200 tokens)
├── ProductSearchRequest.java (100 tokens)
├── ProductUpdateRequest.java (100 tokens)
├── User.java (100 tokens)
├── enums/
├── EventType.java
├── feign/
├── UserFeignClient.java (100 tokens)
├── kafka/
├── config/
├── KafkaConfig.java (400 tokens)
├── ProductKafkaProducer.java (300 tokens)
├── UserKafkaProducer.java (300 tokens)
├── model/
├── Product.java (200 tokens)
├── repo/
├── ProductRepo.java (300 tokens)
├── security/
├── CustomAccessDeniedHandler.java (300 tokens)
├── CustomAuthenticationEntryPoint.java (200 tokens)
├── WebSecurityConfig.java (700 tokens)
├── service/
├── ProductService.java (1000 tokens)
├── resources/
├── application.yml (600 tokens)
├── logback-spring.xml (400 tokens)
├── spring microservices.postman_collection.json (4.8k tokens)
```
## /.gitignore
```gitignore path="/.gitignore"
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
# IDE
.idea/
*.iws
*.iml
*.ipr
.vscode/
.settings/
.project
.classpath
# OS
.DS_Store
Thumbs.db
# Logs
*.log
logs/
# Docker
.docker/
# Temporary files
*.tmp
.tmp
*.swp
*.swo
# Environment
.env
.env.local
.env.*.local
# Backup files
*.backup
*.bak
```
## /.mvn/wrapper/maven-wrapper.properties
```properties path="/.mvn/wrapper/maven-wrapper.properties"
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.7/apache-maven-3.8.7-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar
```
## /Docker_guid.md
🧠 Tổng hợp cuối bảng
Tình huống Cần làm Lệnh
🔧 Sửa commons Build lib + rebuild 2 service mvn clean install -DskipTests; docker-compose build; docker-compose up -d --force-recreate
🔧 Sửa account-service Rebuild + restart account docker-compose up -d --build --force-recreate account-service-app
🔧 Sửa product-service Rebuild + restart product docker-compose up -d --build --force-recreate product-service-app
🔁 Làm sạch toàn hệ thống Stop + rebuild lại từ đầu docker-compose down -v && mvn clean install -DskipTests && docker-compose build && docker-compose up -d
## /README.md
# Spring Microservices Blueprint
🚀 Java Spring Boot microservices with complete ecosystem: PostgreSQL, Kafka+Zookeeper, Elasticsearch+Kibana+Fluentd, JWT auth, Swagger. Two simple commands launch 10+ pre-configured services!
This project demonstrates event-driven microservices with request tracing: each request carries a correlation_id across REST calls and Kafka messages, making logging and observability easy.
## 🏗️ Multi-Module Maven Architecture
This project demonstrates **Maven Multi-Module** best practices:
```
spring-microservices-blueprint/
├── pom.xml # Parent POM with dependency management
├── commons/ # Shared utilities and DTOs
│ ├── pom.xml
│ └── src/main/java/
├── account-service/ # User management microservice
│ ├── pom.xml
│ └── src/main/java/
└── product-service/ # Product management microservice
├── pom.xml
└── src/main/java/
```
**Benefits:**
- **Shared Dependencies**: Common libraries managed in parent POM
- **Code Reusability**: Shared DTOs and utilities in commons module
- **Consistent Versioning**: All modules use same version from parent
- **Easy Setup & Launch**: Only two commands needed to build and start all services
- **Simplified Development**: Debugging, logging, and monitoring pre-configured for convenience
## 🔧 Prerequisites
**Required:**
- **Java 17+**
- **Maven 3.9+**
- **Docker Desktop**
**Verify installation:**
```bash
java -version # Should show Java 17+
mvn -version # Should show Maven 3.9+
docker --version # Should show Docker 20.10+
```
## 🚀 Quick Start (5 Minutes)
**Build and start everything:**
```bash
# Build Maven artifacts first
mvn clean install -DskipTests
# Build Docker images and start all services
docker compose up -d --build
```
**Wait for services to be ready (30-60 seconds):**
```bash
docker compose ps
```
**Test the system:**
```bash
# Test Account Service
curl http://localhost:8088/api/test/all
# Test Product Service
curl http://localhost:8089/api/product/search
```
If both return data, you're ready! 🎉
## 📚 Complete Documentation
### 🏁 Getting Started
- **[01-Quick-Setup.md](docs/01-Quick-Setup.md)** - Get running in 5 minutes
- **[02-System-Architecture.md](docs/02-System-Architecture.md)** - Understand the design
- **[03-API-Reference.md](docs/03-API-Reference.md)** - Complete API documentation
### 🛠️ Development & Testing
- **[04-Development-Guide.md](docs/04-Development-Guide.md)** - Local development workflow
- **[05-Testing-Guide.md](docs/05-Testing-Guide.md)** - Testing strategies and scripts
- **[06-Debugging-Guide.md](docs/06-Debugging-Guide.md)** - Debug in containers
### 🐳 Operations & Deployment
- **[07-Docker-Operations.md](docs/07-Docker-Operations.md)** - Container management
- **[08-Monitoring-Logging.md](docs/08-Monitoring-Logging.md)** - Observability setup
- **[09-Troubleshooting.md](docs/09-Troubleshooting.md)** - Common issues & solutions
### 📋 Reference Materials
- **[10-Postman-Collection.md](docs/10-Postman-Collection.md)** - API testing with Postman
- **[11-Configuration-Reference.md](docs/11-Configuration-Reference.md)** - All configuration options
## 🎯 What You'll Learn
### Core Microservices Patterns
- **Service Decomposition** - Separate services for different business domains
- **Database Per Service** - Independent data storage for each service
- **API Gateway Pattern** - Centralized entry point (future enhancement)
- **Service Discovery** - Dynamic service location (future enhancement)
### Communication Patterns
- **Synchronous Communication** - REST APIs with Feign clients
- **Asynchronous Messaging** - Event-driven architecture with Kafka
- **Request/Response** - Direct service-to-service calls
- **Publish/Subscribe** - Event broadcasting for loose coupling
### Cross-Cutting Concerns
- **Authentication & Authorization** - JWT tokens with role-based access
- **Centralized Logging** - ELK Stack for log aggregation
- **Distributed Tracing** - Correlation IDs across service calls
- **Health Monitoring** - Service health checks and metrics
- **Distributed Tracing & Logging** - request body, response body, and Kafka messages carry correlation_id, making it easy to trace interactions across services.
### Infrastructure & DevOps
- **Multi-Module Architecture** - Maven parent-child module structure
- **Containerization** - Docker for consistent environments
- **Container Orchestration** - Docker Compose for multi-service deployment
- **Configuration Management** - Environment-based configuration
- **Database Management** - PostgreSQL with proper schema design
## 🏗️ System Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Client Layer │
│ Web Browser, Mobile App, Postman, curl, etc. │
└─────────────────────┬───────────────────────────────────────┘
│ HTTP/REST
┌─────────────────────▼───────────────────────────────────────┐
│ Service Layer │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Account Service │◄────────┤ Product Service │ │
│ │ Port 8088 │ Feign │ Port 8089 │ │
│ │ │ Client │ │ │
│ │ • Authentication│ │ • Product CRUD │ │
│ │ • User Management│ │ • Search & Filter│ │
│ │ • JWT Tokens │ │ • Authorization │ │
│ └─────────┬───────┘ └─────────┬───────┘ │
└────────────┼─────────────────────────────┼───────────────────┘
│ │
│ Kafka Events │ Kafka Events
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Message Layer │
│ │
│ ┌─────────────────┐ │
│ │ Kafka Cluster │ │
│ │ Port 9092 │ │
│ │ │ │
│ │ Topics: │ │
│ │ • user-events │ │
│ │ • product-events│ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ │
┌────────────▼───────────┐ ┌───────────▼───────────────────┐
│ Data Layer │ │ Observability Layer │
│ │ │ │
│ ┌─────────┐ ┌────────┐ │ │ ┌─────────────┐ ┌───────────┐ │
│ │Account │ │Product │ │ │ │Elasticsearch│ │ Kibana │ │
│ │ DB │ │ DB │ │ │ │Port 9200 │ │Port 5601 │ │
│ │Port 5432│ │Port 5434│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ Log Storage │ │Log Viewer │ │
│ └─────────┘ └────────┘ │ │ └─────────────┘ └───────────┘ │
└─────────────────────────┘ │ ┌─────────────┐ ┌───────────┐ │
│ │ Fluentd │ │ Kafdrop │ │
│ │Port 24224 │ │Port 8085 │ │
│ │ │ │ │ │
│ │Log Collector│ │Kafka UI │ │
│ └─────────────┘ └───────────┘ │
└─────────────────────────────────┘
```
## 🎮 Try It Out (Interactive Demo)
### 1. Register and Login
```bash
# Register a new user
curl -X POST http://localhost:8088/api/auth/signup \
-H "Content-Type: application/json" \
-d '{"username": "demo_user", "email": "demo@example.com", "password": "password123"}'
# Login to get JWT token
curl -X POST http://localhost:8088/api/auth/signin \
-H "Content-Type: application/json" \
-d '{"username": "demo_user", "password": "password123"}'
```
### 2. Explore Products
```bash
# Search all products (no authentication needed)
curl "http://localhost:8089/api/product/search"
# Search with filters
curl "http://localhost:8089/api/product/search?name=Sample&page=0&size=5"
```
### 3. Monitor the System
- **API Documentation:**
- http://localhost:8088/swagger-ui.html (Account Service)
- http://localhost:8089/swagger-ui.html (Product Service)
- **View Logs:** http://localhost:5601 (elastic/elastic)
- **Monitor Kafka:** http://localhost:8085
- **Check Health:**
- http://localhost:8088/actuator/health
- http://localhost:8089/actuator/health
## 🛠️ Development Commands
### First Time Setup
```bash
# Build everything from scratch
mvn clean install -DskipTests
docker compose build
docker compose up -d
```
### Daily Development
```bash
# After changing commons module
mvn clean install -DskipTests
docker compose up -d --build --force-recreate
# After changing account-service only
docker compose up -d --build --force-recreate account-service-app
# After changing product-service only
docker compose up -d --build --force-recreate product-service-app
```
### Debugging & Monitoring
```bash
# View logs
docker compose logs -f account-service-app product-service-app
# Check service status
docker compose ps
# Clean restart everything
docker compose down -v && mvn clean install -DskipTests && docker compose up -d --build
```
## 📊 Module & Service Overview
### Maven Modules
| Module | Purpose | Dependencies |
|--------|---------|-------------|
| **Parent POM** | Dependency management | Spring Boot BOM, Maven plugins |
| **Commons** | Shared utilities, DTOs | Spring Boot Starter |
| **Account Service** | User management, JWT auth | Commons, Spring Security, PostgreSQL |
| **Product Service** | Product CRUD, search | Commons, Spring JPA, Feign Client |
### Runtime Services
| Service | Port | Purpose | Technology Stack |
|---------|------|---------|------------------|
| **Account Service** | 8088 | User management, JWT auth | Spring Boot, Spring Security, PostgreSQL |
| **Product Service** | 8089 | Product CRUD, search | Spring Boot, JPA, PostgreSQL, Feign |
| **Account Database** | 5432 | User data storage | PostgreSQL 15 |
| **Product Database** | 5434 | Product data storage | PostgreSQL 15 |
| **Kafka** | 9092 | Event streaming | Apache Kafka |
| **Zookeeper** | 2181 | Kafka coordination | Apache Zookeeper |
| **Elasticsearch** | 9200 | Log storage & search | Elasticsearch 7.17 |
| **Kibana** | 5601 | Log visualization | Kibana 7.17 |
| **Fluentd** | 24224 | Log collection | Fluentd |
| **Kafdrop** | 8085 | Kafka monitoring | Kafdrop UI |
## 🎓 Learning Path
### **Beginner (Week 1-2)**
1. **Start Here:** [01-Quick-Setup.md](docs/01-Quick-Setup.md)
2. **Understand:** [02-System-Architecture.md](docs/02-System-Architecture.md)
3. **Try APIs:** [03-API-Reference.md](docs/03-API-Reference.md)
4. **Import:** [10-Postman-Collection.md](docs/10-Postman-Collection.md)
### **Intermediate (Week 3-4)**
1. **Develop:** [04-Development-Guide.md](docs/04-Development-Guide.md)
2. **Test:** [05-Testing-Guide.md](docs/05-Testing-Guide.md)
3. **Debug:** [06-Debugging-Guide.md](docs/06-Debugging-Guide.md)
4. **Monitor:** [08-Monitoring-Logging.md](docs/08-Monitoring-Logging.md)
### **Advanced (Week 5+)**
1. **Deploy:** [07-Docker-Operations.md](docs/07-Docker-Operations.md)
2. **Configure:** [11-Configuration-Reference.md](docs/11-Configuration-Reference.md)
3. **Troubleshoot:** [09-Troubleshooting.md](docs/09-Troubleshooting.md)
4. **Extend:** Add new services and features
## 🚨 Common Issues & Quick Fixes
### Services won't start?
```bash
# Check if ports are in use
netstat -an | findstr :8088
netstat -an | findstr :8089
# Clean restart
docker compose down -v
docker system prune -f
mvn clean install -DskipTests
docker compose up -d --build
```
### Build failures?
```bash
# Clear Maven cache
mvn dependency:purge-local-repository
# Rebuild everything
mvn clean install -DskipTests
```
### Out of memory?
```bash
# Clean Docker resources
docker system prune -af --volumes
# Increase Docker memory limit to 8GB+
```
## 🤝 Contributing
This is a learning project! Contributions welcome:
1. **Fork** the repository
2. **Create** a feature branch
3. **Make** your changes
4. **Test** thoroughly
5. **Submit** a pull request
### Ideas for Contributions
- Add new microservices (notification, payment, etc.)
- Implement API Gateway (Spring Cloud Gateway)
- Add caching layer (Redis)
- Implement circuit breakers (Resilience4j)
- Add metrics collection (Prometheus/Grafana)
- Create Kubernetes deployment manifests
## 📄 License
This project is for **educational purposes**. Feel free to use, modify, and learn from it!
---
## 🎉 Ready to Start?
1. **Quick Start:** Follow the 5-minute setup above
2. **Deep Dive:** Read [01-Quick-Setup.md](docs/01-Quick-Setup.md)
3. **Get Help:** Check [09-Troubleshooting.md](docs/09-Troubleshooting.md)
**Happy Learning! 🚀**
⭐ **Found this helpful?** Give it a star to support the project!
*Built with ❤️ for the microservices learning community*
## /account-service/.gitignore
```gitignore path="/account-service/.gitignore"
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
```
## /account-service/.mvn/wrapper/maven-wrapper.jar
Binary file available at https://raw.githubusercontent.com/cuongnh28/spring-microservices-blueprint/refs/heads/main/account-service/.mvn/wrapper/maven-wrapper.jar
## /account-service/.mvn/wrapper/maven-wrapper.properties
```properties path="/account-service/.mvn/wrapper/maven-wrapper.properties"
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.7/apache-maven-3.8.7-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar
```
## /account-service/Dockerfile
``` path="/account-service/Dockerfile"
FROM maven:3.9.6-eclipse-temurin-17-alpine AS build
WORKDIR /app
# Copy only pom files first to leverage Docker layer caching
COPY pom.xml ./
COPY commons/pom.xml commons/pom.xml
COPY account-service/pom.xml account-service/pom.xml
COPY product-service/pom.xml product-service/pom.xml
RUN mvn -q -DskipTests dependency:go-offline
# Copy source and build
COPY . .
RUN mvn -q -pl account-service -am -DskipTests package
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# Create non-root user
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup
# Install curl for health checks
RUN apk add --no-cache curl
COPY --from=build --chown=appuser:appgroup /app/account-service/target/*.jar app.jar
USER appuser
EXPOSE 8088 5005
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8088/actuator/health || exit 1
ENTRYPOINT ["java", \
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", \
"-XX:+UseContainerSupport", \
"-XX:MaxRAMPercentage=75.0", \
"-jar", "/app/app.jar"]
```
## /account-service/mvnw
``` path="/account-service/mvnw"
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Maven Start Up Batch script
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# M2_HOME - location of maven2's installed home dir
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /usr/local/etc/mavenrc ] ; then
. /usr/local/etc/mavenrc
fi
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "`uname`" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
export JAVA_HOME="`/usr/libexec/java_home`"
else
export JAVA_HOME="/Library/Java/Home"
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
fi
fi
if [ -z "$M2_HOME" ] ; then
## resolve links - $0 may be a link to maven's home
PRG="$0"
# need this for relative symlinks
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\){{contextString}}#39;`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG="`dirname "$PRG"`/$link"
fi
done
saveddir=`pwd`
M2_HOME=`dirname "$PRG"`/..
# make it fully qualified
M2_HOME=`cd "$M2_HOME" && pwd`
cd "$saveddir"
# echo Using m2 at $M2_HOME
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --unix "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="`which javac`"
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=`which readlink`
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
if $darwin ; then
javaHome="`dirname \"$javaExecutable\"`"
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
else
javaExecutable="`readlink -f \"$javaExecutable\"`"
fi
javaHome="`dirname \"$javaExecutable\"`"
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="`\\unset -f command; \\command -v java`"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=`cd "$wdir/.."; pwd`
fi
# end of workaround
done
echo "${basedir}"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
echo "$(tr -s '\n' ' ' < "$1")"
fi
}
BASE_DIR=`find_maven_basedir "$(pwd)"`
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found .mvn/wrapper/maven-wrapper.jar"
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
fi
if [ -n "$MVNW_REPOURL" ]; then
jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
else
jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
fi
while IFS="=" read key value; do
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
esac
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
if [ "$MVNW_VERBOSE" = true ]; then
echo "Downloading from: $jarUrl"
fi
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
if $cygwin; then
wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
fi
if command -v wget > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found wget ... using wget"
fi
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
else
wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
fi
elif command -v curl > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found curl ... using curl"
fi
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
curl -o "$wrapperJarPath" "$jarUrl" -f
else
curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Falling back to using Java to download"
fi
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
# For Cygwin, switch paths to Windows format before running javac
if $cygwin; then
javaClass=`cygpath --path --windows "$javaClass"`
fi
if [ -e "$javaClass" ]; then
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Compiling MavenWrapperDownloader.java ..."
fi
# Compiling the Java class
("$JAVA_HOME/bin/javac" "$javaClass")
fi
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
# Running the downloader
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Running MavenWrapperDownloader.java ..."
fi
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
fi
fi
fi
fi
##########################################################################################
# End of extension
##########################################################################################
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
if [ "$MVNW_VERBOSE" = true ]; then
echo $MAVEN_PROJECTBASEDIR
fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --path --windows "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
fi
# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
export MAVEN_CMD_LINE_ARGS
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
exec "$JAVACMD" \
$MAVEN_OPTS \
$MAVEN_DEBUG_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" \
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
```
## /account-service/mvnw.cmd
```cmd path="/account-service/mvnw.cmd"
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM https://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Maven Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
if "%MVNW_VERBOSE%" == "true" (
echo Found %WRAPPER_JAR%
)
) else (
if not "%MVNW_REPOURL%" == "" (
SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
)
if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %DOWNLOAD_URL%
)
powershell -Command "&{"^
"$webclient = new-object System.Net.WebClient;"^
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
"}"^
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
"}"
if "%MVNW_VERBOSE%" == "true" (
echo Finished downloading %WRAPPER_JAR%
)
)
@REM End of extension
@REM Provide a "standardized" way to retrieve the CLI args that will
@REM work with both Windows and non-Windows executions.
set MAVEN_CMD_LINE_ARGS=%*
%MAVEN_JAVA_EXE% ^
%JVM_CONFIG_MAVEN_PROPS% ^
%MAVEN_OPTS% ^
%MAVEN_DEBUG_OPTS% ^
-classpath %WRAPPER_JAR% ^
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%"=="on" pause
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
cmd /C exit /B %ERROR_CODE%
```
## /account-service/pom.xml
```xml path="/account-service/pom.xml"
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.microservices</groupId>
<artifactId>spring-microservices-blueprint</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>account-service</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.microservices</groupId>
<artifactId>commons</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
```
## /account-service/src/main/java/com/demo/AccountServiceApplication.java
```java path="/account-service/src/main/java/com/demo/AccountServiceApplication.java"
package com.demo;
import com.demo.logging.OpenFeignRequestInterceptor;
import com.demo.logging.CommonRequestBodyLogger;
import com.demo.logging.CommonResponseBodyLogger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Import;
import org.springframework.kafka.annotation.EnableKafka;
import com.demo.util.StringToDateConverter;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@SpringBootApplication
@EnableFeignClients(defaultConfiguration = {OpenFeignRequestInterceptor.class})
@Import({StringToDateConverter.class, CommonRequestBodyLogger.class, CommonResponseBodyLogger.class})
@EnableKafka
public class AccountServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AccountServiceApplication.class, args);
}
}
```
## /account-service/src/main/java/com/demo/config/DataInitializer.java
```java path="/account-service/src/main/java/com/demo/config/DataInitializer.java"
package com.demo.config;
import com.demo.enums.ERole;
import com.demo.model.Role;
import com.demo.repo.RoleRepository;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Configuration
public class DataInitializer {
@Bean
public ApplicationRunner seedRoles(RoleRepository roleRepository) {
return args -> {
if (roleRepository.findByName(ERole.ROLE_USER).isEmpty()) {
roleRepository.save(new Role(null, ERole.ROLE_USER));
}
if (roleRepository.findByName(ERole.ROLE_ADMIN).isEmpty()) {
roleRepository.save(new Role(null, ERole.ROLE_ADMIN));
}
};
}
}
```
## /account-service/src/main/java/com/demo/config/OpenApiConfig.java
```java path="/account-service/src/main/java/com/demo/config/OpenApiConfig.java"
package com.demo.config;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.tags.Tag;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI accountServiceOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Account Service API")
.description("User account management and authentication APIs.\n\n"
+ "This service provides endpoints for user registration, login, role-based \n"
+ "authorization, and profile retrieval. It also exposes test endpoints for \n"
+ "verifying security and infrastructure behaviors.")
.version("v1.0.0")
.termsOfService("https://example.com/terms")
.contact(new Contact()
.name("Vito Nguyen (cuongnh28)")
.url("https://github.com/cuongnh28"))
.license(new License().name("Apache 2.0").url("https://www.apache.org/licenses/LICENSE-2.0")))
.externalDocs(new ExternalDocumentation()
.description("Project README")
.url("https://example.com/docs"))
.addTagsItem(new Tag().name("auth-controller").description("Authentication endpoints: sign in, sign up"))
.addTagsItem(new Tag().name("user-controller").description("User profile and account information"))
.addTagsItem(new Tag().name("test-controller").description("Public/admin/user access test endpoints"))
.addTagsItem(new Tag().name("product-event-controller").description("Kafka product event test endpoints"));
}
}
```
## /account-service/src/main/java/com/demo/controller/AuthController.java
```java path="/account-service/src/main/java/com/demo/controller/AuthController.java"
package com.demo.controller;
import com.demo.payload.request.LoginRequest;
import com.demo.payload.request.SignupRequest;
import com.demo.service.AuthService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/auth")
@Tag(name = "auth-controller", description = "Authentication endpoints for sign-in and sign-up")
public class AuthController {
@Autowired
AuthService authService;
@PostMapping("/signin")
@Operation(summary = "Authenticate user", description = "Validate credentials and return JWT plus user info")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
return ResponseEntity.ok(authService.authenticateUser(loginRequest));
}
@PostMapping("/signup")
@Operation(summary = "Register new user", description = "Create a new account and assign roles")
public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRequest) {
return authService.registerUser(signUpRequest);
}
}
```
## /account-service/src/main/java/com/demo/controller/HomeController.java
```java path="/account-service/src/main/java/com/demo/controller/HomeController.java"
package com.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import io.swagger.v3.oas.annotations.Hidden;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Controller
@Hidden
public class HomeController {
@GetMapping("/")
public String home() {
return "home";
}
}
```
## /account-service/src/main/java/com/demo/controller/ProductEventController.java
```java path="/account-service/src/main/java/com/demo/controller/ProductEventController.java"
package com.demo.controller;
import com.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@RestController
@RequestMapping("/api/product-events")
@CrossOrigin(origins = "*", maxAge = 3600)
@Slf4j
@Tag(name = "product-event-controller", description = "Simulates product events handled by account-service")
public class ProductEventController {
@Autowired
private UserService userService;
/**
* Test endpoint to simulate product event processing
* This demonstrates how the account service would handle product events
*/
@PostMapping("/test/{userId}")
@Operation(summary = "Test product event handling", description = "Updates user stats based on a simulated product event action")
public String testProductEvent(@PathVariable Long userId, @RequestParam String action) {
log.info("Testing product event processing for user {} with action {}", userId, action);
userService.updateUserProductStats(userId, action);
return String.format("Product event processed for user %d with action: %s", userId, action);
}
}
```
## /account-service/src/main/java/com/demo/controller/TestController.java
```java path="/account-service/src/main/java/com/demo/controller/TestController.java"
package com.demo.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/test")
@Tag(name = "test-controller", description = "Public/user/admin access test endpoints and Kafka interceptor demo")
public class TestController {
@GetMapping("/all")
@Operation(summary = "Public access test", description = "Open endpoint to verify anonymous access works")
public String allAccess() {
return "Public Content.";
}
@GetMapping("/user")
@Operation(summary = "User access test", description = "Requires USER or ADMIN role")
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
public String userAccess() {
return "User Content.";
}
@GetMapping("/admin")
@Operation(summary = "Admin access test", description = "Requires ADMIN role")
@PreAuthorize("hasRole('ADMIN')")
public String adminAccess() {
return "Admin Board.";
}
@PostMapping("/kafka-test")
@Operation(summary = "Kafka interceptor demo", description = "Produces/consumes a message to trigger KafkaRecordInterceptor")
public String testKafkaInterceptor() {
return "Kafka test endpoint - this will trigger the interceptor when messages are consumed";
}
}
```
## /account-service/src/main/java/com/demo/controller/UserController.java
```java path="/account-service/src/main/java/com/demo/controller/UserController.java"
package com.demo.controller;
import com.demo.payload.response.UserProfile;
import com.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/user")
@Tag(name = "user-controller", description = "User profile retrieval and current user info")
public class UserController {
@Autowired
UserService userService;
@GetMapping("/{id}")
@Operation(summary = "Get user profile by ID", description = "Fetch user profile including roles by numeric identifier")
public UserProfile getUserBy(@PathVariable("id") long id) {
return userService.getUserById(id);
}
@GetMapping("/me")
@Operation(summary = "Get current user profile", description = "Return profile of the authenticated user")
@PreAuthorize("isAuthenticated()")
public UserProfile getMe() {
return userService.getCurrentUserProfile();
}
}
```
## /account-service/src/main/java/com/demo/dto/ProductEvent.java
```java path="/account-service/src/main/java/com/demo/dto/ProductEvent.java"
package com.demo.dto;
import com.demo.enums.EventType;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProductEvent {
private EventType eventType;
private Long productId;
private String productName;
private String description;
private BigDecimal price;
private Long creatorId;
private LocalDateTime timestamp;
private String username;
}
```
## /account-service/src/main/java/com/demo/dto/User.java
```java path="/account-service/src/main/java/com/demo/dto/User.java"
package com.demo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User {
private String username;
private String email;
private String password;
}
```
## /account-service/src/main/java/com/demo/enums/ERole.java
```java path="/account-service/src/main/java/com/demo/enums/ERole.java"
package com.demo.enums;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
public enum ERole {
ROLE_USER,
ROLE_ADMIN
}
```
## /account-service/src/main/java/com/demo/enums/EventType.java
```java path="/account-service/src/main/java/com/demo/enums/EventType.java"
package com.demo.enums;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
public enum EventType {
CREATED,
UPDATED,
DELETED
}
```
## /account-service/src/main/java/com/demo/kafka/KafkaConfig.java
```java path="/account-service/src/main/java/com/demo/kafka/KafkaConfig.java"
package com.demo.kafka;
import com.demo.kafka.interceptor.KafkaRecordInterceptor;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.listener.ContainerProperties;
import java.util.HashMap;
import java.util.Map;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Configuration
public class KafkaConfig {
@Value("${spring.kafka.bootstrap-servers}")
private String bootstrapAddress;
@Value("${spring.kafka.consumer.group-id}")
private String group;
@Bean
public ConsumerFactory<String, String> requestLogConsumerFactory() {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
props.put(ConsumerConfig.GROUP_ID_CONFIG, group);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
return new DefaultKafkaConsumerFactory<>(props);
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> consumerKafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(requestLogConsumerFactory());
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
factory.getContainerProperties().setSyncCommits(true);
factory.setRecordInterceptor(new KafkaRecordInterceptor<>());
return factory;
}
}
```
## /account-service/src/main/java/com/demo/kafka/KafkaUserConsumer.java
```java path="/account-service/src/main/java/com/demo/kafka/KafkaUserConsumer.java"
package com.demo.kafka;
import com.demo.dto.User;
import com.demo.service.UserService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Component
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class KafkaUserConsumer {
@Autowired
private UserService userService;
@Value("${spring.kafka.topic.name}")
private String topic;
@Value("${spring.kafka.replication.factor:1}")
private int replicationFactor;
@Value("${spring.kafka.partition.number:1}")
private int partitionNumber;
@Autowired
private ObjectMapper objectMapper;
@KafkaListener(
topics = "${spring.kafka.topic.name}",
groupId = "${spring.kafka.consumer.group-id}",
containerFactory = "consumerKafkaListenerContainerFactory",
autoStartup = "${application.kafka.payment-request-log.auto-startup}"
)
public void kafkaListener(ConsumerRecord<String, String> record, Acknowledgment acknowledgment) {
String value = record.value();
try {
User user = objectMapper.readValue(value, User.class);
userService.addUser(user);
acknowledgment.acknowledge();
} catch (JsonProcessingException e) {
log.error("Error deserializing user: {}", e.getMessage());
}
}
}
```
## /account-service/src/main/java/com/demo/kafka/ProductEventConsumer.java
```java path="/account-service/src/main/java/com/demo/kafka/ProductEventConsumer.java"
package com.demo.kafka;
import com.demo.dto.ProductEvent;
import com.demo.enums.EventType;
import com.demo.service.UserService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Component
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class ProductEventConsumer {
@Autowired
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
@KafkaListener(
topics = "${spring.kafka.topic.product.name:product-events}",
groupId = "${spring.kafka.consumer.group-id}",
containerFactory = "consumerKafkaListenerContainerFactory"
)
public void handleProductEvent(ConsumerRecord<String, String> record, Acknowledgment acknowledgment) {
String value = record.value();
try {
ProductEvent productEvent = objectMapper.readValue(value, ProductEvent.class);
log.info("Received product event: {} for product: {} by user: {}",
productEvent.getEventType(),
productEvent.getProductName(),
productEvent.getUsername());
// Process the product event
processProductEvent(productEvent);
acknowledgment.acknowledge();
} catch (JsonProcessingException e) {
log.error("Error deserializing product event: {}", e.getMessage());
} catch (Exception e) {
log.error("Error processing product event: {}", e.getMessage(), e);
}
}
private void processProductEvent(ProductEvent productEvent) {
try {
EventType type = productEvent.getEventType();
if (type == null) {
log.warn("Unknown product event type: null");
return;
}
switch (type) {
case CREATED:
handleProductCreated(productEvent);
break;
case UPDATED:
handleProductUpdated(productEvent);
break;
case DELETED:
handleProductDeleted(productEvent);
break;
default:
log.warn("Unknown product event type: {}", type);
}
} catch (Exception e) {
log.error("Error processing product event type {}: {}",
productEvent.getEventType(), e.getMessage(), e);
}
}
private void handleProductCreated(ProductEvent productEvent) {
log.info("User {} created a new product: {} (ID: {})",
productEvent.getUsername(),
productEvent.getProductName(),
productEvent.getProductId());
// Update user statistics or send notifications
userService.updateUserProductStats(productEvent.getCreatorId(), EventType.CREATED.name());
}
private void handleProductUpdated(ProductEvent productEvent) {
log.info("User {} updated product: {} (ID: {})",
productEvent.getUsername(),
productEvent.getProductName(),
productEvent.getProductId());
// Update user statistics
userService.updateUserProductStats(productEvent.getCreatorId(), EventType.UPDATED.name());
}
private void handleProductDeleted(ProductEvent productEvent) {
log.info("User {} deleted product: {} (ID: {})",
productEvent.getUsername(),
productEvent.getProductName(),
productEvent.getProductId());
// Update user statistics
userService.updateUserProductStats(productEvent.getCreatorId(), EventType.DELETED.name());
}
}
```
## /account-service/src/main/java/com/demo/model/Role.java
```java path="/account-service/src/main/java/com/demo/model/Role.java"
package com.demo.model;
import com.demo.enums.ERole;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Entity
@Table(name = "roles")
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Enumerated(EnumType.STRING)
@Column(length = 20)
private ERole name;
}
```
## /account-service/src/main/java/com/demo/model/User.java
```java path="/account-service/src/main/java/com/demo/model/User.java"
package com.demo.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.HashSet;
import java.util.Set;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 20)
private String username;
@Column(length = 50)
private String email;
@Column(length = 120)
private String password;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
@Builder.Default
private Set<Role> roles = new HashSet<>();
public User(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
}
}
```
## /account-service/src/main/java/com/demo/payload/request/LoginRequest.java
```java path="/account-service/src/main/java/com/demo/payload/request/LoginRequest.java"
package com.demo.payload.request;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.ToString;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Data
public class LoginRequest {
@NotBlank(message = "Username is required")
private String username;
@NotBlank(message = "Password is required")
@ToString.Exclude
private String password;
}
```
## /account-service/src/main/java/com/demo/payload/request/SignupRequest.java
```java path="/account-service/src/main/java/com/demo/payload/request/SignupRequest.java"
package com.demo.payload.request;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.util.Set;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SignupRequest {
@NotBlank(message = "Username is required")
@Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters")
private String username;
@NotBlank(message = "Email is required")
@Size(max = 50, message = "Email must not exceed 50 characters")
@Email(message = "Email must be valid")
private String email;
private Set<String> roles;
@NotBlank(message = "Password is required")
@Size(min = 6, max = 40, message = "Password must be between 6 and 40 characters")
@ToString.Exclude
private String password;
}
```
## /account-service/src/main/java/com/demo/payload/request/UpdateRequest.java
```java path="/account-service/src/main/java/com/demo/payload/request/UpdateRequest.java"
package com.demo.payload.request;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UpdateRequest {
@NotBlank(message = "Email is required")
@Size(max = 50, message = "Email must not exceed 50 characters")
@Email(message = "Email must be valid")
private String email;
}
```
## /account-service/src/main/java/com/demo/payload/response/JwtResponse.java
```java path="/account-service/src/main/java/com/demo/payload/response/JwtResponse.java"
package com.demo.payload.response;
import lombok.Data;
import java.util.List;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Data
public class JwtResponse {
private String token;
private String type = "Bearer";
private Long id;
private String username;
private String email;
private List<String> roles;
public JwtResponse(String accessToken, Long id, String username, String email, List<String> roles) {
this.token = accessToken;
this.id = id;
this.username = username;
this.email = email;
this.roles = roles;
}
}
```
## /account-service/src/main/java/com/demo/payload/response/MessageResponse.java
```java path="/account-service/src/main/java/com/demo/payload/response/MessageResponse.java"
package com.demo.payload.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MessageResponse {
private String message;
}
```
## /account-service/src/main/java/com/demo/payload/response/UserProfile.java
```java path="/account-service/src/main/java/com/demo/payload/response/UserProfile.java"
package com.demo.payload.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserProfile {
private Long id;
private String username;
private String email;
private List<String> roles;
}
```
## /account-service/src/main/java/com/demo/repo/RoleRepository.java
```java path="/account-service/src/main/java/com/demo/repo/RoleRepository.java"
package com.demo.repo;
import com.demo.enums.ERole;
import com.demo.model.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(ERole name);
}
```
## /account-service/src/main/java/com/demo/repo/UserRepository.java
```java path="/account-service/src/main/java/com/demo/repo/UserRepository.java"
package com.demo.repo;
import com.demo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
@Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.username = :username")
Optional<User> findWithRolesByUsername(@Param("username") String username);
Boolean existsByUsername(String username);
Boolean existsByEmail(String email);
}
```
## /account-service/src/main/java/com/demo/security/WebSecurityConfig.java
```java path="/account-service/src/main/java/com/demo/security/WebSecurityConfig.java"
package com.demo.security;
import com.demo.security.jwt.AuthEntryPointJwt;
import com.demo.security.jwt.AuthTokenFilter;
import com.demo.security.services.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfig {
@Autowired
UserDetailsServiceImpl userDetailsService;
@Autowired
private AuthEntryPointJwt unauthorizedHandler;
@Autowired
private AuthTokenFilter authenticationJwtTokenFilter;
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth ->
auth.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/test/**").permitAll()
.requestMatchers("/api/user/**").permitAll()
.requestMatchers("/").permitAll()
.requestMatchers("/swagger-ui/**").permitAll()
.requestMatchers("/v3/api-docs/**").permitAll()
.requestMatchers("/swagger-ui.html").permitAll()
.anyRequest().authenticated()
);
http.authenticationProvider(authenticationProvider());
http.addFilterBefore(authenticationJwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
```
## /account-service/src/main/java/com/demo/security/services/UserDetailsImpl.java
```java path="/account-service/src/main/java/com/demo/security/services/UserDetailsImpl.java"
package com.demo.security.services;
import com.demo.model.User;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Getter
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class UserDetailsImpl implements UserDetails {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String email;
@JsonIgnore
private String password;
private Collection<? extends GrantedAuthority> authorities;
public static UserDetailsImpl build(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName().name()))
.collect(Collectors.toList());
return new UserDetailsImpl(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getPassword(),
authorities);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
```
## /account-service/src/main/java/com/demo/security/services/UserDetailsServiceImpl.java
```java path="/account-service/src/main/java/com/demo/security/services/UserDetailsServiceImpl.java"
package com.demo.security.services;
import com.demo.model.User;
import com.demo.repo.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findWithRolesByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username));
return UserDetailsImpl.build(user);
}
}
```
## /account-service/src/main/java/com/demo/service/AuthService.java
```java path="/account-service/src/main/java/com/demo/service/AuthService.java"
package com.demo.service;
import com.demo.enums.ERole;
import com.demo.exception.UnprocessableEntityException;
import com.demo.model.Role;
import com.demo.model.User;
import com.demo.payload.request.LoginRequest;
import com.demo.payload.request.SignupRequest;
import com.demo.payload.response.JwtResponse;
import com.demo.payload.response.MessageResponse;
import com.demo.repo.RoleRepository;
import com.demo.repo.UserRepository;
import com.demo.security.jwt.JwtUtils;
import com.demo.security.services.UserDetailsImpl;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.security.Key;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Service
public class AuthService {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
UserRepository userRepository;
@Autowired
RoleRepository roleRepository;
@Autowired
PasswordEncoder encoder;
@Autowired
JwtUtils jwtUtils;
@Value("${auth.app.jwtSecret}")
private String jwtSecret;
@Value("${auth.app.jwtExpirationMs}")
private int jwtExpirationMs;
public JwtResponse authenticateUser(LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = generateJwtTokenWithUserDetails(authentication);
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
List<String> roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList();
return new JwtResponse(jwt,
userDetails.getId(),
userDetails.getUsername(),
userDetails.getEmail(),
roles);
}
public ResponseEntity<?> registerUser(SignupRequest signUpRequest) {
if (userRepository.existsByUsername(signUpRequest.getUsername())) {
throw new UnprocessableEntityException("Username is already taken");
}
if (userRepository.existsByEmail(signUpRequest.getEmail())) {
throw new UnprocessableEntityException("Email is already in use");
}
User user = new User(signUpRequest.getUsername(),
signUpRequest.getEmail(),
encoder.encode(signUpRequest.getPassword()));
Set<String> strRoles = signUpRequest.getRoles();
Set<Role> roles = new HashSet<>();
// Default to ROLE_USER when roles are not provided or empty
if (CollectionUtils.isEmpty(strRoles)) {
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(userRole);
} else {
// Map provided roles; fall back to ROLE_USER for unknown entries
strRoles.forEach(role -> {
String r = role == null ? "" : role.trim().toLowerCase();
switch (r) {
case "admin", "role_admin" -> {
Role adminRole = roleRepository.findByName(ERole.ROLE_ADMIN)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(adminRole);
}
default -> {
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(userRole);
}
}
});
}
user.setRoles(roles);
userRepository.save(user);
return ResponseEntity.ok(new MessageResponse("User registered successfully!"));
}
private String generateJwtTokenWithUserDetails(Authentication authentication) {
UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();
Date now = new Date();
Date expiry = new Date(now.getTime() + jwtExpirationMs);
return Jwts.builder()
.subject(userPrincipal.getUsername())
.claim("userId", userPrincipal.getId().toString())
.claim("roles", userPrincipal.getAuthorities().stream()
.map(authority -> authority.getAuthority())
.toArray())
.issuedAt(now)
.expiration(expiry)
.signWith(key())
.compact();
}
private Key key() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
}
}
```
## /account-service/src/main/java/com/demo/service/UserService.java
```java path="/account-service/src/main/java/com/demo/service/UserService.java"
package com.demo.service;
import com.demo.model.User;
import com.demo.payload.response.UserProfile;
import com.demo.repo.UserRepository;
import com.demo.exception.NotFoundException;
import com.demo.util.AuthUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Service
@Slf4j
public class UserService {
@Autowired
private UserRepository userRepository;
public UserProfile getUserById(long id) {
User user = userRepository.findById(id).orElseThrow(() -> new NotFoundException("User not found"));
return UserProfile.builder()
.id(user.getId())
.username(user.getUsername())
.email(user.getEmail())
.roles(user.getRoles().stream().map(r -> r.getName().name()).toList())
.build();
}
public UserProfile getCurrentUserProfile() {
String username = AuthUtils.getCurrentUsername();
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new NotFoundException("User not found"));
return UserProfile.builder()
.id(user.getId())
.username(user.getUsername())
.email(user.getEmail())
.roles(user.getRoles().stream().map(r -> r.getName().name()).toList())
.build();
}
public User addUser(com.demo.dto.User user) {
return userRepository.save(
User.builder().email(user.getEmail()).username(user.getUsername()).password(user.getPassword()).build()
);
}
/**
* Updates user product statistics based on product events.
* This method is called when the account service receives product events from Kafka.
*
* @param userId the ID of the user who performed the product action
* @param action the action performed (CREATED, UPDATED, DELETED)
*/
public void updateUserProductStats(Long userId, String action) {
try {
User user = userRepository.findById(userId).orElse(null);
if (user == null) {
log.warn("User with ID {} not found when updating product stats for action: {}", userId, action);
return;
}
log.info("Updating product stats for user: {} (action: {})", user.getUsername(), action);
// Here you could:
// 1. Update user statistics in database
// 2. Send notifications to the user
// 3. Update user activity logs
// 4. Trigger other business logic
// For now, we'll just log the activity
switch (action) {
case "CREATED":
log.info("User {} created a new product", user.getUsername());
// Could increment a products_created counter
break;
case "UPDATED":
log.info("User {} updated a product", user.getUsername());
// Could increment a products_updated counter
break;
case "DELETED":
log.info("User {} deleted a product", user.getUsername());
// Could increment a products_deleted counter
break;
default:
log.warn("Unknown product action: {}", action);
}
} catch (Exception e) {
log.error("Error updating product stats for user {} with action {}: {}",
userId, action, e.getMessage(), e);
}
}
}
```
## /account-service/src/main/resources/application.yml
```yml path="/account-service/src/main/resources/application.yml"
# Account Service Configuration
spring:
application:
name: account-service
datasource:
url: ${DB_URL:jdbc:postgresql://localhost:5432/account_service}
username: postgres
password: 123456
type: com.zaxxer.hikari.HikariDataSource
hikari:
maximum-pool-size: 20
driver-class-name: org.postgresql.Driver
jpa:
properties:
hibernate:
jdbc:
lob:
non_contextual_creation: true
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: false
show-sql: false
hibernate:
ddl-auto: update
kafka:
bootstrap-servers: ${SPRING_KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
topic:
name: account-service
replication:
factor: 1
partition:
number: 2
consumer:
group-id: kafka-user-listener
auto-offset-reset: earliest
level:
concurrency: 5
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
properties:
interceptor.classes: com.demo.kafka.interceptor.KafkaProducerInterceptor
# Application Kafka Configuration
application:
kafka:
payment-request-log:
auto-startup: true
# JWT Configuration
auth:
app:
jwtSecret: ======================Auth=Spring===========================
jwtExpirationMs: 86400000
# Server Configuration
server:
port: 8088
# Management/Actuator Configuration
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html
operationsSorter: method
# Application Info
info:
app:
name: Account Service
description: User account management service
version: 1.0.0
# Application Configuration
app:
async:
core-pool-size: 3
max-pool-size: 10
queue-capacity: 50
thread-name-prefix: account-async-
keep-alive-seconds: 60
await-termination-seconds: 30
logging:
request: true
response: true
trace: true
# Logging Configuration
logging:
level:
com.demo: DEBUG
org.springframework.kafka: INFO
org.springframework.kafka.listener: ERROR
org.springframework.kafka.listener.KafkaMessageListenerContainer: ERROR
org.springframework.kafka.listener.ConcurrentMessageListenerContainer: ERROR
org.apache.kafka: WARN
org.apache.kafka.clients.consumer.KafkaConsumer: WARN
org.springframework.scheduling: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
```
## /account-service/src/main/resources/logback-spring.xml
```xml path="/account-service/src/main/resources/logback-spring.xml"
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<!-- ISO8601 timestamp -->
<timestamp>
<timeZone>UTC</timeZone>
<pattern>yyyy-MM-dd'T'HH:mm:ss.SSSZ</pattern>
</timestamp>
<logLevel />
<loggerName />
<threadName />
<mdc />
<arguments />
<message />
<!-- Stack trace -->
<stackTrace>
<fieldName>stackTrace</fieldName>
<throwableConverter
class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
<maxDepthPerThrowable>30</maxDepthPerThrowable>
<rootCauseFirst>true</rootCauseFirst>
</throwableConverter>
</stackTrace>
<!-- Custom fixed fields -->
<globalCustomFields>{"service":"${spring.application.name}"}</globalCustomFields>
</providers>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
<!-- Tinh chỉnh noise log -->
<logger name="org.springframework" level="INFO" />
<logger name="org.hibernate.SQL" level="WARN" />
<!-- Reduce Kafka listener noise -->
<logger name="org.springframework.kafka" level="ERROR" />
<logger name="org.springframework.kafka.listener" level="ERROR" />
<logger name="org.springframework.kafka.listener.KafkaMessageListenerContainer" level="ERROR" />
<logger name="org.springframework.kafka.listener.ConcurrentMessageListenerContainer" level="ERROR" />
<logger name="org.apache.kafka" level="WARN" />
<logger name="org.apache.kafka.clients.consumer.KafkaConsumer" level="WARN" />
</configuration>
```
## /account-service/src/main/resources/templates/home.html
```html path="/account-service/src/main/resources/templates/home.html"
Hello world
```
## /commons/pom.xml
```xml path="/commons/pom.xml"
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.microservices</groupId>
<artifactId>spring-microservices-blueprint</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>commons</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<lombok.version>1.18.34</lombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.17.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>kafka</artifactId>
<version>1.20.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>8.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
```
## /commons/src/main/java/com/demo/config/openfeign/CommonOpenFeignConfig.java
```java path="/commons/src/main/java/com/demo/config/openfeign/CommonOpenFeignConfig.java"
package com.demo.config.openfeign;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import feign.codec.Decoder;
import feign.codec.ErrorDecoder;
import feign.okhttp.OkHttpClient;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Configuration
public class CommonOpenFeignConfig {
@Bean
public Decoder feignDecoder() {
HttpMessageConverter<Object> objectHttpMessageConverter = new MappingJackson2HttpMessageConverter(customObjectMapper());
ObjectFactory<HttpMessageConverters> objectFactory = () -> new HttpMessageConverters(objectHttpMessageConverter);
return new ResponseEntityDecoder(new SpringDecoder(objectFactory));
}
public ObjectMapper customObjectMapper() {
JavaTimeModule module = new JavaTimeModule();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.registerModule(module);
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
objectMapper.registerModule(new JavaTimeModule());
return objectMapper;
}
@Bean
public OkHttpClient client() {
return new OkHttpClient();
}
@Bean
public ErrorDecoder errorDecoder() {
return new CommonOpenFeignErrorDecoder();
}
}
```
## /commons/src/main/java/com/demo/config/openfeign/CommonOpenFeignErrorDecoder.java
```java path="/commons/src/main/java/com/demo/config/openfeign/CommonOpenFeignErrorDecoder.java"
package com.demo.config.openfeign;
import com.demo.exception.FeignResponseException;
import com.demo.exception.HttpException;
import com.demo.exception.model.ApiError;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.Response;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.http.HttpStatus;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Slf4j
public class CommonOpenFeignErrorDecoder implements ErrorDecoder {
private final ObjectMapper mapper = new ObjectMapper();
@Override
public Exception decode(String methodKey, Response response) {
ApiError apiError;
InputStream inputStream;
String responseBody;
HttpStatus responseStatus = HttpStatus.valueOf(response.status());
if (response.body() == null) {
return new HttpException(responseStatus, List.of(responseStatus.getReasonPhrase()));
}
try {
inputStream = response.body().asInputStream();
responseBody = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
log.info(response.request().url());
log.info(responseBody);
} catch (IOException e) {
return new HttpException(responseStatus, List.of(responseStatus.getReasonPhrase()));
}
try {
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
apiError = mapper.readValue(responseBody, ApiError.class);
} catch (JsonProcessingException e) {
return new HttpException(responseStatus, List.of(responseBody));
}
return new FeignResponseException(apiError);
}
}
```
## /commons/src/main/java/com/demo/config/thread/CommonContext.java
```java path="/commons/src/main/java/com/demo/config/thread/CommonContext.java"
package com.demo.config.thread;
import java.io.Serializable;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
public interface CommonContext extends Serializable {
String getCorrelationId();
String getUserId();
String getUsername();
void setCorrelationId(String correlationId);
void setUserId(String userId);
void setUsername(String username);
}
```
## /commons/src/main/java/com/demo/config/thread/CommonContextTaskDecorator.java
```java path="/commons/src/main/java/com/demo/config/thread/CommonContextTaskDecorator.java"
package com.demo.config.thread;
import com.demo.constants.CorrelationConstants;
import com.demo.context.CommonContextHolder;
import com.demo.util.CorrelationUtils;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.core.task.TaskDecorator;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Map;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Slf4j
public class CommonContextTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
CommonContext commonContext = CommonContextHolder.getContext();
Map<String, String> mdcContext = MDC.getCopyOfContextMap();
SecurityContext securityContext = SecurityContextHolder.getContext();
log.debug("Capturing context for async operation - correlationId: {}",
commonContext != null ? commonContext.getCorrelationId() : "none");
return () -> {
try {
if (commonContext != null) {
CommonContextHolder.setContext(commonContext, true);
} else {
CommonContext newContext = CommonContextHolder.createFromSecurityContext();
CommonContextHolder.setContext(newContext, true);
}
if (mdcContext != null) {
MDC.setContextMap(mdcContext);
} else {
String correlationId = CorrelationUtils.currentCorrelationId();
if (correlationId != null) {
MDC.put(CorrelationConstants.CONTEXT_CORRELATION_ID.getValue(), correlationId);
}
}
SecurityContextHolder.setContext(securityContext);
log.debug("Context restored for async operation - correlationId: {}",
CorrelationUtils.currentCorrelationId());
runnable.run();
} finally {
CommonContextHolder.resetContext();
MDC.clear();
SecurityContextHolder.clearContext();
log.debug("Context cleaned up after async operation");
}
};
}
}
```
## /commons/src/main/java/com/demo/config/thread/DefaultCommonContext.java
```java path="/commons/src/main/java/com/demo/config/thread/DefaultCommonContext.java"
package com.demo.config.thread;
import lombok.Data;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Data
public class DefaultCommonContext implements CommonContext {
private String correlationId;
private String userId;
private String username;
public DefaultCommonContext() {
}
public DefaultCommonContext(String correlationId, String userId, String username) {
this.correlationId = correlationId;
this.userId = userId;
this.username = username;
}
}
```
## /commons/src/main/java/com/demo/config/thread/README.md
# Thread Context Configuration
This package provides thread context management for microservices, ensuring that correlation IDs and user context are preserved across async operations.
## Components
### 1. CommonContext Interface
Defines the contract for thread-local context information:
- `correlationId`: Request tracing identifier
- `userId`: Current user ID
- `username`: Current username
### 2. DefaultCommonContext
Simple implementation of CommonContext interface.
### 3. CommonContextHolder
Thread-local storage for context information with support for:
- Regular thread-local storage
- Inheritable thread-local storage (for child threads)
- Integration with Spring Security context
### 4. CommonContextTaskDecorator
Task decorator that preserves context across async operations:
- Captures current context before async execution
- Restores context in the new thread
- Cleans up context after execution
### 5. AsyncConfig
Configuration class that enables async operations with context preservation:
- `@EnableAsync` annotation enables async processing
- Custom `TaskExecutor` with configurable thread pool
- Automatic context preservation via `CommonContextTaskDecorator`
### 6. AsyncUtils
Utility class for easy async operations with context preservation:
- `runAsync()` - Execute Runnable asynchronously
- `supplyAsync()` - Execute Supplier asynchronously
- Automatic context preservation
## Usage
### Using @Async Annotation
```java
@Service
public class MyService {
@Async
public CompletableFuture<String> processAsync(String data) {
// Context is automatically preserved by CommonContextTaskDecorator
String correlationId = CommonContextHolder.getCorrelationId();
String userId = CommonContextHolder.getUserId();
// Your async logic here
return CompletableFuture.completedFuture("Processed: " + data);
}
}
```
### Using AsyncUtils
```java
@Service
public class MyService {
public CompletableFuture<String> processWithAsyncUtils(String data) {
return AsyncUtils.supplyAsync(() -> {
// Context is automatically preserved
String correlationId = CommonContextHolder.getCorrelationId();
String userId = CommonContextHolder.getUserId();
// Your async logic here
return "Processed: " + data;
});
}
}
```
### Manual Context Management
```java
// Set context manually
CommonContext context = CommonContextHolder.createFromSecurityContext();
CommonContextHolder.setContext(context, true); // true = inheritable
// Get context information
String correlationId = CommonContextHolder.getCorrelationId();
String userId = CommonContextHolder.getUserId();
String username = CommonContextHolder.getUsername();
// Clear context
CommonContextHolder.resetContext();
```
### Configuration Properties
Add these to your `application.yml`:
```yaml
app:
async:
core-pool-size: 5
max-pool-size: 20
queue-capacity: 100
thread-name-prefix: microservice-async-
keep-alive-seconds: 60
await-termination-seconds: 30
```
## Benefits
1. **Request Tracing**: Correlation IDs are preserved across all async operations
2. **User Context**: User information is available in background threads
3. **Logging**: Structured logging with consistent correlation IDs
4. **Debugging**: Easier to trace requests through async operations
5. **Security**: User context is preserved for authorization checks
6. **Performance**: Configurable thread pools for optimal performance
7. **Reliability**: Graceful shutdown and error handling
## Current Status
✅ **Fully Configured and Ready to Use**
The async infrastructure is now properly configured and actively used in the application:
- `@Async` annotations work with context preservation
- `AsyncUtils` provides convenient async operations
- Thread pools are configurable via application properties
- Context is automatically preserved across all async operations
## Example Service
See `AsyncExampleService` for complete examples of how to use async operations with context preservation.
## /commons/src/main/java/com/demo/config/thread/ThreadAsyncConfig.java
```java path="/commons/src/main/java/com/demo/config/thread/ThreadAsyncConfig.java"
package com.demo.config.thread;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Slf4j
@Configuration
@EnableAsync
public class ThreadAsyncConfig implements AsyncConfigurer {
@Value("${app.async.core-pool-size:5}")
private int corePoolSize;
@Value("${app.async.max-pool-size:20}")
private int maxPoolSize;
@Value("${app.async.queue-capacity:100}")
private int queueCapacity;
@Value("${app.async.thread-name-prefix:microservice-async-}")
private String threadNamePrefix;
@Value("${app.async.keep-alive-seconds:60}")
private int keepAliveSeconds;
@Value("${app.async.await-termination-seconds:30}")
private int awaitTerminationSeconds;
/**
* Creates a custom TaskExecutor with context preservation.
*
* @return configured TaskExecutor
*/
@Bean("asyncTaskExecutor")
public TaskExecutor asyncTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// Basic thread pool configuration
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setThreadNamePrefix(threadNamePrefix);
executor.setKeepAliveSeconds(keepAliveSeconds);
// Context preservation
executor.setTaskDecorator(new CommonContextTaskDecorator());
// Rejection policy - caller runs when queue is full
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// Allow core threads to timeout
executor.setAllowCoreThreadTimeOut(true);
// Graceful shutdown configuration
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(awaitTerminationSeconds);
// Initialize the executor
executor.initialize();
log.info("Async TaskExecutor configured - core: {}, max: {}, queue: {}, prefix: {}",
corePoolSize, maxPoolSize, queueCapacity, threadNamePrefix);
return executor;
}
/**
* Configures the default async executor for @Async annotations.
*
* @return the default async executor
*/
@Override
public Executor getAsyncExecutor() {
return asyncTaskExecutor();
}
}
```
## /commons/src/main/java/com/demo/constants/CorrelationConstants.java
```java path="/commons/src/main/java/com/demo/constants/CorrelationConstants.java"
package com.demo.constants;
import lombok.Getter;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Getter
public enum CorrelationConstants {
CONTEXT_CORRELATION_ID("correlation_id"),
CONTEXT_REQUEST_URL("request_url"),
CONTEXT_REQUEST_METHOD("request_method"),
CONTEXT_REQUEST_TYPE("request_type"),
CONTEXT_RESPONSE_TIME("response_time"),
CONTEXT_RESPONSE_STATUS("http_status");
private final String value;
CorrelationConstants(String value) {
this.value = value;
}
@Override
public String toString() {
return value;
}
}
```
## /commons/src/main/java/com/demo/context/CommonContextHolder.java
```java path="/commons/src/main/java/com/demo/context/CommonContextHolder.java"
package com.demo.context;
import com.demo.config.thread.CommonContext;
import com.demo.config.thread.DefaultCommonContext;
import com.demo.constants.CorrelationConstants;
import com.demo.util.CorrelationUtils;
import org.slf4j.MDC;
import org.springframework.core.NamedInheritableThreadLocal;
import org.springframework.core.NamedThreadLocal;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import jakarta.annotation.Nullable;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
public abstract class CommonContextHolder {
private static final ThreadLocal<CommonContext> COMMON_CONTEXT_HOLDER =
new NamedThreadLocal<>("Common context");
private static final ThreadLocal<CommonContext> INHERITABLE_COMMON_CONTEXT_HOLDER =
new NamedInheritableThreadLocal<>("Common context");
private CommonContextHolder() {
// Utility class - prevent instantiation
}
public static void resetContext() {
COMMON_CONTEXT_HOLDER.remove();
INHERITABLE_COMMON_CONTEXT_HOLDER.remove();
}
public static void setContext(@Nullable CommonContext context) {
setContext(context, false);
}
public static void setContext(@Nullable CommonContext context, boolean inheritable) {
if (context == null) {
resetContext();
} else {
if (inheritable) {
INHERITABLE_COMMON_CONTEXT_HOLDER.set(context);
COMMON_CONTEXT_HOLDER.remove();
} else {
COMMON_CONTEXT_HOLDER.set(context);
INHERITABLE_COMMON_CONTEXT_HOLDER.remove();
}
}
}
@Nullable
public static CommonContext getContext() {
CommonContext context = COMMON_CONTEXT_HOLDER.get();
if (context == null) {
context = INHERITABLE_COMMON_CONTEXT_HOLDER.get();
}
return context;
}
public static String getCorrelationId() {
CommonContext context = getContext();
if (context != null && context.getCorrelationId() != null) {
return context.getCorrelationId();
}
return MDC.get(CorrelationConstants.CONTEXT_CORRELATION_ID.getValue());
}
@Nullable
public static String getUserId() {
CommonContext context = getContext();
if (context != null) {
return context.getUserId();
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() != null) {
Object principal = authentication.getPrincipal();
if (principal instanceof org.springframework.security.core.userdetails.UserDetails) {
return authentication.getName();
}
}
return null;
}
@Nullable
public static String getUsername() {
CommonContext context = getContext();
if (context != null) {
return context.getUsername();
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
return authentication.getName();
}
return null;
}
public static CommonContext createFromSecurityContext() {
String correlationId = CorrelationUtils.currentCorrelationId();
String username = getUsername();
String userId = getUserId();
return new DefaultCommonContext(correlationId, userId, username);
}
}
```
## /commons/src/main/java/com/demo/exception/ApplicationException.java
```java path="/commons/src/main/java/com/demo/exception/ApplicationException.java"
package com.demo.exception;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import java.util.Arrays;
import java.util.List;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Getter
public abstract class ApplicationException extends RuntimeException {
public abstract HttpStatus getHttpStatus();
private final List<String> messages;
protected ApplicationException() {
this.messages = null;
}
protected ApplicationException(String... messages) {
this.messages = Arrays.asList(messages);
}
}
```
## /commons/src/main/java/com/demo/exception/FeignResponseException.java
```java path="/commons/src/main/java/com/demo/exception/FeignResponseException.java"
package com.demo.exception;
import com.demo.exception.model.ApiError;
import lombok.Getter;
import org.springframework.http.HttpStatus;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Getter
public class FeignResponseException extends ApplicationException {
private final HttpStatus status;
private final transient ApiError errorModel;
public FeignResponseException(ApiError errorModel) {
this.status = HttpStatus.valueOf(errorModel.getStatusCode());
this.errorModel = errorModel;
}
@Override
public HttpStatus getHttpStatus() {
return this.status;
}
}
```
## /commons/src/main/java/com/demo/exception/ForbiddenException.java
```java path="/commons/src/main/java/com/demo/exception/ForbiddenException.java"
package com.demo.exception;
import org.springframework.http.HttpStatus;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
public class ForbiddenException extends ApplicationException {
public ForbiddenException() {
super();
}
public ForbiddenException(String message) {
super(message);
}
@Override
public HttpStatus getHttpStatus() {
return HttpStatus.FORBIDDEN;
}
}
```
## /commons/src/main/java/com/demo/exception/FormValidateException.java
```java path="/commons/src/main/java/com/demo/exception/FormValidateException.java"
package com.demo.exception;
import org.springframework.http.HttpStatus;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
public class FormValidateException extends ApplicationException {
public FormValidateException() {
super();
}
public FormValidateException(String message) {
super(message);
}
@Override
public HttpStatus getHttpStatus() {
return HttpStatus.BAD_REQUEST;
}
}
```
## /commons/src/main/java/com/demo/exception/HttpException.java
```java path="/commons/src/main/java/com/demo/exception/HttpException.java"
package com.demo.exception;
import org.springframework.http.HttpStatus;
import java.util.List;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
public class HttpException extends ApplicationException {
private final HttpStatus httpStatus;
public HttpException(HttpStatus httpStatus, List<String> messages) {
super(messages.toArray(new String[0]));
this.httpStatus = httpStatus;
}
@Override
public HttpStatus getHttpStatus() {
return httpStatus;
}
}
```
## /commons/src/main/java/com/demo/exception/NotFoundException.java
```java path="/commons/src/main/java/com/demo/exception/NotFoundException.java"
package com.demo.exception;
import org.springframework.http.HttpStatus;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
public class NotFoundException extends ApplicationException {
public NotFoundException() {
super("Object not found");
}
public NotFoundException(String message) {
super(message);
}
@Override
public HttpStatus getHttpStatus() {
return HttpStatus.NOT_FOUND;
}
}
```
## /commons/src/main/java/com/demo/exception/UnauthorizedException.java
```java path="/commons/src/main/java/com/demo/exception/UnauthorizedException.java"
package com.demo.exception;
import org.springframework.http.HttpStatus;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
public class UnauthorizedException extends ApplicationException {
public UnauthorizedException() {
super();
}
public UnauthorizedException(String message) {
super(message);
}
@Override
public HttpStatus getHttpStatus() {
return HttpStatus.UNAUTHORIZED;
}
}
```
## /commons/src/main/java/com/demo/exception/UnprocessableEntityException.java
```java path="/commons/src/main/java/com/demo/exception/UnprocessableEntityException.java"
package com.demo.exception;
import org.springframework.http.HttpStatus;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
public class UnprocessableEntityException extends ApplicationException {
public UnprocessableEntityException() {
super();
}
public UnprocessableEntityException(String message) {
super(message);
}
@Override
public HttpStatus getHttpStatus() {
return HttpStatus.UNPROCESSABLE_ENTITY;
}
}
```
## /commons/src/main/java/com/demo/exception/handler/CommonExceptionHandler.java
```java path="/commons/src/main/java/com/demo/exception/handler/CommonExceptionHandler.java"
package com.demo.exception.handler;
import com.demo.exception.ApplicationException;
import com.demo.exception.FeignResponseException;
import com.demo.exception.model.ApiError;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@RestControllerAdvice
@Slf4j
public class CommonExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleException(Exception ex) {
log.error(ExceptionUtils.getMessage(ex));
HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
ApiError errorModel = new ApiError(httpStatus.value(), "An error occurred");
return new ResponseEntity<>(errorModel, header(), httpStatus);
}
@ExceptionHandler({ApplicationException.class})
public ResponseEntity<ApiError> handleBaseException(ApplicationException ex, WebRequest request) {
log.error(ExceptionUtils.getMessage(ex));
ApiError errorModel = new ApiError(ex.getHttpStatus().value(), ex.getMessages());
return new ResponseEntity<>(errorModel, ex.getHttpStatus());
}
@ExceptionHandler({FeignResponseException.class})
public ResponseEntity<Object> handleFeignException(Exception ex, WebRequest request) {
log.error(ExceptionUtils.getMessage(ex));
FeignResponseException customEx = (FeignResponseException) ex;
ApiError errorModel = customEx.getErrorModel();
HttpStatus httpStatus = customEx.getHttpStatus();
return new ResponseEntity<>(errorModel, header(), httpStatus);
}
@ExceptionHandler({AccessDeniedException.class})
public ResponseEntity<Object> handleAccessDenied(Exception ex, WebRequest request) {
log.error(ExceptionUtils.getMessage(ex));
HttpStatus httpStatus = HttpStatus.FORBIDDEN;
ApiError errorModel = new ApiError(httpStatus.value(), "Forbidden");
return new ResponseEntity<>(errorModel, header(), httpStatus);
}
@ExceptionHandler({AuthenticationException.class})
public ResponseEntity<Object> handleAuthentication(Exception ex, WebRequest request) {
log.error(ExceptionUtils.getMessage(ex));
HttpStatus httpStatus = HttpStatus.UNAUTHORIZED;
ApiError errorModel = new ApiError(httpStatus.value(), "Unauthorized");
return new ResponseEntity<>(errorModel, header(), httpStatus);
}
@ExceptionHandler({MaxUploadSizeExceededException.class})
public ResponseEntity<Object> handleMaxUploadSizeException(Exception ex, WebRequest request) {
log.error(ExceptionUtils.getMessage(ex));
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
ApiError errorModel = new ApiError(
httpStatus.value(),
"The max size is: {}" + ((MaxUploadSizeExceededException) ex).getMaxUploadSize()
);
return new ResponseEntity<>(errorModel, header(), httpStatus);
}
@ExceptionHandler({HttpMessageNotReadableException.class})
public ResponseEntity<Object> handleHttpMessageNotReadableException(Exception ex, WebRequest request) {
log.error(ExceptionUtils.getMessage(ex));
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
ApiError errorModel = new ApiError(
httpStatus.value(),
ex.getMessage()
);
return new ResponseEntity<>(errorModel, header(), httpStatus);
}
private HttpHeaders header(){
HttpHeaders customHeader = new HttpHeaders();
customHeader.setContentType(MediaType.APPLICATION_JSON);
return customHeader;
}
}
```
## /commons/src/main/java/com/demo/exception/model/ApiError.java
```java path="/commons/src/main/java/com/demo/exception/model/ApiError.java"
package com.demo.exception.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.*;
import java.util.List;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Getter
@Setter
@NoArgsConstructor
@ToString
public class ApiError {
private int statusCode;
private List<String> messages;
public ApiError(int statusCode, List<String> messages) {
this.statusCode = statusCode;
this.messages = messages;
}
public ApiError(int statusCode, String message) {
this.statusCode = statusCode;
this.messages = List.of(message);
}
}
```
## /commons/src/main/java/com/demo/exception/model/ErrorCode.java
```java path="/commons/src/main/java/com/demo/exception/model/ErrorCode.java"
package com.demo.exception.model;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
public interface ErrorCode {
String getCode();
}
```
## /commons/src/main/java/com/demo/kafka/interceptor/KafkaProducerInterceptor.java
```java path="/commons/src/main/java/com/demo/kafka/interceptor/KafkaProducerInterceptor.java"
package com.demo.kafka.interceptor;
import com.demo.constants.CorrelationConstants;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.slf4j.MDC;
import java.util.Map;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
public class KafkaProducerInterceptor<K, V> implements ProducerInterceptor<K, V> {
@Override
public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record) {
String correlationId = MDC.get(CorrelationConstants.CONTEXT_CORRELATION_ID.getValue());
if (correlationId != null) {
record.headers().add(CorrelationConstants.CONTEXT_CORRELATION_ID.getValue(), correlationId.getBytes());
}
return record;
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
}
```
## /commons/src/main/java/com/demo/kafka/interceptor/KafkaRecordInterceptor.java
```java path="/commons/src/main/java/com/demo/kafka/interceptor/KafkaRecordInterceptor.java"
package com.demo.kafka.interceptor;
import org.apache.commons.lang3.StringUtils;
import com.demo.constants.CorrelationConstants;
import com.demo.util.CorrelationUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.common.header.Header;
import org.slf4j.MDC;
import org.springframework.kafka.listener.RecordInterceptor;
import java.nio.charset.StandardCharsets;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Slf4j
public class KafkaRecordInterceptor<K, V> implements RecordInterceptor<K, V> {
@Override
public ConsumerRecord<K, V> intercept(ConsumerRecord<K, V> consumerRecord, Consumer<K, V> consumer) {
String correlationId = null;
Header header = consumerRecord.headers().lastHeader(CorrelationConstants.CONTEXT_CORRELATION_ID.getValue());
if (header != null && header.value() != null) {
correlationId = new String(header.value(), StandardCharsets.UTF_8);
}
if (StringUtils.isBlank(correlationId)) {
correlationId = CorrelationUtils.generateCorrelationId();
}
MDC.put(CorrelationConstants.CONTEXT_CORRELATION_ID.getValue(), correlationId);
String payloadStr = String.valueOf(consumerRecord.value());
log.info("Kafka message intercepted: topic={}, partition={}, offset={}, key={}, payload={}",
consumerRecord.topic(), consumerRecord.partition(), consumerRecord.offset(),
consumerRecord.key(), payloadStr);
return consumerRecord;
}
}
```
## /commons/src/main/java/com/demo/logging/CommonRequestBodyLogger.java
```java path="/commons/src/main/java/com/demo/logging/CommonRequestBodyLogger.java"
package com.demo.logging;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import net.logstash.logback.argument.StructuredArguments;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Slf4j
@ControllerAdvice
public class CommonRequestBodyLogger extends RequestBodyAdviceAdapter {
@Value("${management.endpoints.web.base-path:/actuator}")
private String actuatorBasePath;
@Autowired
HttpServletRequest httpServletRequest;
@Override
public boolean supports(MethodParameter methodParameter, Type type,
Class<? extends HttpMessageConverter<?>> aClass) {
String uri = httpServletRequest.getRequestURI();
return !uri.startsWith(actuatorBasePath);
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage,
MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
log.info("Request body: URI={}, QueryString={}, Payload={}",
httpServletRequest.getRequestURI(),
httpServletRequest.getQueryString(),
body);
return super.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
@Override
public Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
if(httpServletRequest.getQueryString() != null) {
log.info("Request empty body: URI={}, QueryString={}",
httpServletRequest.getRequestURI(),
httpServletRequest.getQueryString());
}
return body;
}
}
```
## /commons/src/main/java/com/demo/logging/CommonResponseBodyLogger.java
```java path="/commons/src/main/java/com/demo/logging/CommonResponseBodyLogger.java"
package com.demo.logging;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import net.logstash.logback.argument.StructuredArguments;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Slf4j
@ControllerAdvice
public class CommonResponseBodyLogger implements ResponseBodyAdvice<Object> {
@Value("${management.endpoints.web.base-path:/actuator}")
private String actuatorBasePath;
@Autowired
HttpServletRequest httpServletRequest;
@Override
public boolean supports(MethodParameter methodParameter,
Class<? extends HttpMessageConverter<?>> aClass) {
String uri = httpServletRequest.getRequestURI();
return !uri.startsWith(actuatorBasePath);
}
@Override
public Object beforeBodyWrite(Object response,
MethodParameter methodParameter,
MediaType mediaType,
Class<? extends HttpMessageConverter<?>> aClass,
ServerHttpRequest serverHttpRequest,
ServerHttpResponse serverHttpResponse) {
if (serverHttpRequest instanceof ServletServerHttpRequest
&& serverHttpResponse instanceof ServletServerHttpResponse) {
HttpServletResponse servletResponse = ((ServletServerHttpResponse) serverHttpResponse).getServletResponse();
log.info("Response body: Status={}, Response={}",
servletResponse.getStatus(),
response);
}
return response;
}
}
```
## /commons/src/main/java/com/demo/logging/CorrelationLoggingFilter.java
```java path="/commons/src/main/java/com/demo/logging/CorrelationLoggingFilter.java"
package com.demo.logging;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import com.demo.constants.CorrelationConstants;
import com.demo.util.CorrelationUtils;
import java.io.IOException;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Slf4j
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorrelationLoggingFilter implements Filter {
@Value("${management.endpoints.web.base-path:/actuator}")
private String actuatorBasePath;
/**
* Purpose: Attach correlation and request/response metadata to the logging MDC for
* end-to-end tracing. Generates or propagates correlation_id, records URL/method,
* measures latency, logs HTTP status, and cleans up response-specific MDC keys.
* Actuator endpoints are skipped to reduce noise.
*/
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// Skip actuator endpoints
if (actuatorBasePath != null && !actuatorBasePath.isBlank() && request.getRequestURI().startsWith(actuatorBasePath)) {
chain.doFilter(req, res);
return;
}
// Correlation and request MDC context
String correlationId = request.getHeader(CorrelationConstants.CONTEXT_CORRELATION_ID.getValue());
if (!StringUtils.hasText(correlationId)) {
correlationId = CorrelationUtils.generateCorrelationId();
}
response.setHeader(CorrelationConstants.CONTEXT_CORRELATION_ID.getValue(), correlationId);
MDC.put(CorrelationConstants.CONTEXT_CORRELATION_ID.getValue(), correlationId);
MDC.put(CorrelationConstants.CONTEXT_REQUEST_URL.getValue(), request.getRequestURI());
MDC.put(CorrelationConstants.CONTEXT_REQUEST_METHOD.getValue(), request.getMethod());
MDC.put(CorrelationConstants.CONTEXT_REQUEST_TYPE.getValue(), "request");
long startTime = System.currentTimeMillis();
try {
chain.doFilter(req, res);
} finally {
// Response MDC context, logging, and cleanup
MDC.put(CorrelationConstants.CONTEXT_REQUEST_TYPE.getValue(), "response");
MDC.put(CorrelationConstants.CONTEXT_RESPONSE_TIME.getValue(), String.valueOf(System.currentTimeMillis() - startTime));
MDC.put(CorrelationConstants.CONTEXT_RESPONSE_STATUS.getValue(), String.valueOf(response.getStatus()));
log.info(HttpStatus.valueOf(response.getStatus()).name());
MDC.remove(CorrelationConstants.CONTEXT_RESPONSE_TIME.getValue());
MDC.remove(CorrelationConstants.CONTEXT_RESPONSE_STATUS.getValue());
}
}
@Override
public void init(FilterConfig filterConfig) {
// No initialization needed
}
@Override
public void destroy() {
// No cleanup needed
}
}
```
## /commons/src/main/java/com/demo/logging/OpenFeignRequestInterceptor.java
```java path="/commons/src/main/java/com/demo/logging/OpenFeignRequestInterceptor.java"
package com.demo.logging;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import com.demo.constants.CorrelationConstants;
import com.demo.util.CorrelationUtils;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
public class OpenFeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
String correlationId = CorrelationUtils.currentCorrelationId();
requestTemplate.header(CorrelationConstants.CONTEXT_CORRELATION_ID.getValue(), correlationId);
}
}
```
## /commons/src/main/java/com/demo/security/CORSFilter.java
```java path="/commons/src/main/java/com/demo/security/CORSFilter.java"
package com.demo.security;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CORSFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
final HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
response.setHeader("Access-Control-Max-Age", "3600");
if (HttpMethod.OPTIONS.name().equalsIgnoreCase(((HttpServletRequest) req).getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(req, res);
}
}
@Override
public void destroy() {
}
@Override
public void init(FilterConfig config) {
}
}
```
## /commons/src/main/java/com/demo/security/WebConfiguration.java
```java path="/commons/src/main/java/com/demo/security/WebConfiguration.java"
package com.demo.security;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Component
public class WebConfiguration implements WebMvcConfigurer {
@Value("${cors.allowed_origins:*}")
protected String[] allowedOrigins;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(allowedOrigins);
}
}
```
## /commons/src/main/java/com/demo/security/jwt/AuthEntryPointJwt.java
```java path="/commons/src/main/java/com/demo/security/jwt/AuthEntryPointJwt.java"
package com.demo.security.jwt;
import com.demo.exception.UnauthorizedException;
import com.demo.exception.model.ApiError;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
UnauthorizedException unauthorized = new UnauthorizedException("Unauthorized");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
ApiError body = new ApiError(HttpServletResponse.SC_UNAUTHORIZED, unauthorized.getMessages());
objectMapper.writeValue(response.getOutputStream(), body);
}
}
```
## /commons/src/main/java/com/demo/security/jwt/AuthTokenFilter.java
```java path="/commons/src/main/java/com/demo/security/jwt/AuthTokenFilter.java"
package com.demo.security.jwt;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Component
public class AuthTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
String username = jwtUtils.getUserNameFromJwtToken(jwt);
Long userId = jwtUtils.getUserIdFromJwtToken(jwt);
List<String> roles = jwtUtils.getRolesFromJwtToken(jwt);
// Create authorities from JWT roles
List<SimpleGrantedAuthority> authorities = roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// Create a simple authentication object for microservices
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
username,
null,
authorities);
// Store userId in authentication details for easy access
authentication.setDetails(userId);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}
return null;
}
}
```
## /commons/src/main/java/com/demo/security/jwt/JwtUtils.java
```java path="/commons/src/main/java/com/demo/security/jwt/JwtUtils.java"
package com.demo.security.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.security.Key;
import java.util.Date;
import java.util.List;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Component
public class JwtUtils {
private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
@Value("${auth.app.jwtSecret}")
private String jwtSecret;
@Value("${auth.app.jwtExpirationMs}")
private int jwtExpirationMs;
private Key key() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
}
public String generateJwtToken(Authentication authentication) {
Object principal = authentication.getPrincipal();
String username = authentication.getName();
// Extract user ID and roles from the principal
String userId = null;
if (principal instanceof org.springframework.security.core.userdetails.UserDetails) {
// Try to get user ID from the principal if it has a getId method
try {
java.lang.reflect.Method getIdMethod = principal.getClass().getMethod("getId");
Object id = getIdMethod.invoke(principal);
userId = id != null ? id.toString() : null;
} catch (Exception ignored) {
}
}
return Jwts.builder().subject(username)
.claim("userId", userId)
.claim("roles", authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toArray()).issuedAt(new Date()).expiration(new Date((new Date()).getTime() + jwtExpirationMs))
.signWith(key(), SignatureAlgorithm.HS256)
.compact();
}
public String getUserNameFromJwtToken(String token) {
return Jwts.parser().verifyWith((SecretKey) key()).build()
.parseSignedClaims(token).getPayload().getSubject();
}
public Long getUserIdFromJwtToken(String token) {
Claims claims = Jwts.parser().verifyWith((SecretKey) key()).build()
.parseSignedClaims(token).getPayload();
return Long.valueOf(claims.get("userId", String.class));
}
@SuppressWarnings("unchecked")
public List<String> getRolesFromJwtToken(String token) {
Claims claims = Jwts.parser().verifyWith((SecretKey) key()).build()
.parseSignedClaims(token).getPayload();
return (List<String>) claims.get("roles", List.class);
}
public boolean validateJwtToken(String authToken) {
try {
Jwts.parser().verifyWith((SecretKey) key()).build().parse(authToken);
return true;
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
logger.error("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
logger.error("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty: {}", e.getMessage());
}
return false;
}
}
```
## /commons/src/main/java/com/demo/service/AsyncExampleService.java
```java path="/commons/src/main/java/com/demo/service/AsyncExampleService.java"
package com.demo.service;
import com.demo.context.CommonContextHolder;
import com.demo.util.AsyncUtils;
import com.demo.exception.HttpException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
/**
* Example service demonstrating async operations with context preservation.
*
* This service shows how to use @Async annotations and AsyncUtils
* while maintaining correlation IDs and user context across threads.
*/
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Slf4j
@Service
public class AsyncExampleService {
/**
* Example of using @Async annotation with context preservation.
* The context will be automatically preserved by CommonContextTaskDecorator.
*/
public CompletableFuture<String> processWithAsyncAnnotation(String data) {
log.info("Starting async processing with @Async - correlationId: {}",
CommonContextHolder.getCorrelationId());
// Simulate some processing time
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("Async processing interrupted");
}
String correlationId = CommonContextHolder.getCorrelationId();
String userId = CommonContextHolder.getUserId();
String username = CommonContextHolder.getUsername();
log.info("Async processing completed - correlationId: {}, userId: {}, username: {}",
correlationId, userId, username);
return CompletableFuture.completedFuture(
String.format("Processed: %s by user: %s (correlationId: %s)",
data, username, correlationId)
);
}
/**
* Example of using AsyncUtils for manual async operations.
*/
public CompletableFuture<String> processWithAsyncUtils(String data) {
log.info("Starting async processing with AsyncUtils - correlationId: {}",
CommonContextHolder.getCorrelationId());
return AsyncUtils.supplyAsync(() -> {
String correlationId = CommonContextHolder.getCorrelationId();
String userId = CommonContextHolder.getUserId();
String username = CommonContextHolder.getUsername();
log.info("Processing in async thread - correlationId: {}, userId: {}, username: {}",
correlationId, userId, username);
// Simulate some processing time
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("Async processing interrupted");
}
return String.format("AsyncUtils processed: %s by user: %s (correlationId: %s)",
data, username, correlationId);
});
}
/**
* Example of chaining multiple async operations.
*/
public CompletableFuture<String> processWithChaining(String data) {
log.info("Starting chained async processing - correlationId: {}",
CommonContextHolder.getCorrelationId());
return AsyncUtils.supplyAsync(() -> {
log.info("Step 1: Initial processing - correlationId: {}",
CommonContextHolder.getCorrelationId());
return "Step1: " + data;
})
.thenCompose(step1Result -> AsyncUtils.supplyAsync(() -> {
log.info("Step 2: Secondary processing - correlationId: {}",
CommonContextHolder.getCorrelationId());
return step1Result + " -> Step2: Enhanced";
}))
.thenCompose(step2Result -> AsyncUtils.supplyAsync(() -> {
log.info("Step 3: Final processing - correlationId: {}",
CommonContextHolder.getCorrelationId());
return step2Result + " -> Step3: Complete";
}));
}
/**
* Example of error handling in async operations.
*/
public CompletableFuture<String> processWithErrorHandling(String data) {
log.info("Starting async processing with error handling - correlationId: {}",
CommonContextHolder.getCorrelationId());
return AsyncUtils.supplyAsync(() -> {
String correlationId = CommonContextHolder.getCorrelationId();
log.info("Processing with potential error - correlationId: {}", correlationId);
if ("error".equals(data)) {
throw new HttpException(HttpStatus.INTERNAL_SERVER_ERROR,
Arrays.asList("Simulated error in async processing"));
}
return "Successfully processed: " + data;
})
.handle((result, throwable) -> {
if (throwable != null) {
log.error("Async processing failed - correlationId: {}, error: {}",
CommonContextHolder.getCorrelationId(), throwable.getMessage());
return "Error occurred: " + throwable.getMessage();
}
return result;
});
}
}
```
## /commons/src/main/java/com/demo/util/AsyncUtils.java
```java path="/commons/src/main/java/com/demo/util/AsyncUtils.java"
package com.demo.util;
import com.demo.context.CommonContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.TaskExecutor;
import org.springframework.stereotype.Component;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
/**
* Utility class for asynchronous operations with context preservation.
*
* This class provides convenient methods for executing async operations
* while preserving correlation IDs and user context across thread boundaries.
*/
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Slf4j
@Component
public class AsyncUtils {
private static TaskExecutor taskExecutor;
@Autowired
public void setTaskExecutor(TaskExecutor taskExecutor) {
AsyncUtils.taskExecutor = taskExecutor;
}
/**
* Executes a task asynchronously with context preservation.
*
* @param task the task to execute
* @return CompletableFuture that completes when the task finishes
*/
public static CompletableFuture<Void> runAsync(Runnable task) {
if (taskExecutor == null) {
log.warn("TaskExecutor not initialized, falling back to default executor");
return CompletableFuture.runAsync(task);
}
log.debug("Executing async task with context preservation - correlationId: {}",
CommonContextHolder.getCorrelationId());
return CompletableFuture.runAsync(task, taskExecutor);
}
/**
* Executes a supplier asynchronously with context preservation.
*
* @param supplier the supplier to execute
* @param <T> the return type
* @return CompletableFuture that completes with the supplier's result
*/
public static <T> CompletableFuture<T> supplyAsync(Supplier<T> supplier) {
if (taskExecutor == null) {
log.warn("TaskExecutor not initialized, falling back to default executor");
return CompletableFuture.supplyAsync(supplier);
}
log.debug("Executing async supplier with context preservation - correlationId: {}",
CommonContextHolder.getCorrelationId());
return CompletableFuture.supplyAsync(supplier, taskExecutor);
}
/**
* Executes a task asynchronously with a specific executor.
*
* @param task the task to execute
* @param executor the executor to use
* @return CompletableFuture that completes when the task finishes
*/
public static CompletableFuture<Void> runAsync(Runnable task, TaskExecutor executor) {
log.debug("Executing async task with custom executor - correlationId: {}",
CommonContextHolder.getCorrelationId());
return CompletableFuture.runAsync(task, executor);
}
/**
* Executes a supplier asynchronously with a specific executor.
*
* @param supplier the supplier to execute
* @param executor the executor to use
* @param <T> the return type
* @return CompletableFuture that completes with the supplier's result
*/
public static <T> CompletableFuture<T> supplyAsync(Supplier<T> supplier, TaskExecutor executor) {
log.debug("Executing async supplier with custom executor - correlationId: {}",
CommonContextHolder.getCorrelationId());
return CompletableFuture.supplyAsync(supplier, executor);
}
/**
* Creates a completed CompletableFuture with the given value.
*
* @param value the value to complete with
* @param <T> the type of the value
* @return a completed CompletableFuture
*/
public static <T> CompletableFuture<T> completedFuture(T value) {
return CompletableFuture.completedFuture(value);
}
/**
* Creates a failed CompletableFuture with the given exception.
*
* @param throwable the exception to fail with
* @param <T> the type of the future
* @return a failed CompletableFuture
*/
public static <T> CompletableFuture<T> failedFuture(Throwable throwable) {
return CompletableFuture.failedFuture(throwable);
}
}
```
## /commons/src/main/java/com/demo/util/AuthUtils.java
```java path="/commons/src/main/java/com/demo/util/AuthUtils.java"
package com.demo.util;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import com.demo.exception.UnauthorizedException;
import org.springframework.stereotype.Component;
/**
* Utility class for authentication-related operations.
* Provides methods to extract user information from Spring Security context.
*/
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
@Component
public class AuthUtils {
private AuthUtils() {
// Private constructor to prevent instantiation
}
/**
* Gets the current authenticated user's ID from the security context.
*
* @return the user ID
* @throws RuntimeException if user is not authenticated
*/
public static Long getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getDetails() instanceof Long) {
return (Long) authentication.getDetails();
}
throw new UnauthorizedException("User not authenticated");
}
/**
* Gets the current authenticated user's username from the security context.
*
* @return the username
* @throws RuntimeException if user is not authenticated
*/
public static String getCurrentUsername() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getName() != null) {
return authentication.getName();
}
throw new UnauthorizedException("User not authenticated");
}
/**
* Checks if the current authenticated user has admin role.
*
* @return true if user has admin role, false otherwise
*/
public static boolean isCurrentUserAdmin() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getAuthorities() != null) {
return authentication.getAuthorities().stream()
.anyMatch(authority -> authority.getAuthority().equals("ROLE_ADMIN"));
}
return false;
}
/**
* Checks if the current user is authenticated.
*
* @return true if user is authenticated, false otherwise
*/
public static boolean isAuthenticated() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication != null && authentication.isAuthenticated();
}
/**
* Gets the current authentication object.
*
* @return the authentication object, or null if not authenticated
*/
public static Authentication getCurrentAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}
}
```
## /commons/src/main/java/com/demo/util/CorrelationUtils.java
```java path="/commons/src/main/java/com/demo/util/CorrelationUtils.java"
package com.demo.util;
import com.demo.constants.CorrelationConstants;
import org.slf4j.MDC;
import java.util.UUID;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
public class CorrelationUtils {
private CorrelationUtils() {
}
public static String generateCorrelationId(){
return UUID.randomUUID().toString().replace("-", "");
}
public static String currentCorrelationId(){
return MDC.get(CorrelationConstants.CONTEXT_CORRELATION_ID.getValue());
}
}
```
## /commons/src/main/java/com/demo/util/StringToDateConverter.java
```java path="/commons/src/main/java/com/demo/util/StringToDateConverter.java"
package com.demo.util;
import org.springframework.core.convert.converter.Converter;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
/**
* @author Vito Nguyen (<a href="https://github.com/cuongnh28">...</a>)
*/
public class StringToDateConverter implements Converter<String, Date> {
private static final List<String> datePatterns = Arrays.asList(
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
"yyyy-MM-dd'T'HH:mm:ssXXX",
"yyyy-MM-dd'T'HH:mm:ss.SSS ZZZZZ",
"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ",
"yyyy-MM-dd'T'HH:mm:ss.SSS Z",
"yyyy-MM-dd'T'HH:mm:ss.SSS z",
"yyyy-MM-dd'T'HH:mm:ss.SSSZ"
);
@Override
public Date convert(String source) {
for(String pattern : datePatterns){
try{
return new SimpleDateFormat(pattern).parse(source);
}
catch(Exception e){
continue;
}
}
throw new IllegalArgumentException("Can not find matched date format");
}
}
```
## /docker-compose.yml
```yml path="/docker-compose.yml"
services:
account-service-db:
image: postgres:15-alpine
container_name: account-db
restart: unless-stopped
environment:
POSTGRES_DB: account-service
POSTGRES_USER: postgres
POSTGRES_PASSWORD: 123456
ports:
- "5432:5432"
volumes:
- account-postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 30s
timeout: 10s
retries: 3
account-service-app:
build:
context: .
dockerfile: account-service/Dockerfile
image: account-service-app:latest
container_name: account-service-app
restart: unless-stopped
environment:
DB_URL: jdbc:postgresql://account-service-db:5432/account-service
SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:29092
JAVA_OPTS: "-Xmx512m -Xms256m"
ports:
- "8088:8088"
- "5005:5005"
depends_on:
account-service-db:
condition: service_healthy
kafka:
condition: service_healthy
fluentd:
condition: service_started
logging:
driver: "fluentd"
options:
fluentd-address: host.docker.internal:24224
tag: '{{.Name}}'
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8088/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
product-service-db:
image: postgres:15-alpine
container_name: product-db
restart: unless-stopped
environment:
POSTGRES_DB: product-service
POSTGRES_USER: postgres
POSTGRES_PASSWORD: 123456
ports:
- "5434:5432"
volumes:
- product-postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 30s
timeout: 10s
retries: 3
product-service-app:
build:
context: .
dockerfile: product-service/Dockerfile
image: product-service-app:latest
container_name: product-service-app
restart: unless-stopped
environment:
DB_URL: jdbc:postgresql://product-service-db:5432/product-service
SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:29092
JAVA_OPTS: "-Xmx512m -Xms256m"
ports:
- "8089:8089"
- "5006:5006"
depends_on:
product-service-db:
condition: service_healthy
kafka:
condition: service_healthy
fluentd:
condition: service_started
logging:
driver: "fluentd"
options:
fluentd-address: host.docker.internal:24224
tag: '{{.Name}}'
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8089/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
fluentd:
build: ./fluentd
container_name: fluentd
restart: unless-stopped
volumes:
- ./fluentd/conf:/fluentd/etc
depends_on:
elasticsearch:
condition: service_healthy
ports:
- "24224:24224"
- "24224:24224/udp"
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.9
container_name: elasticsearch
restart: unless-stopped
ports:
- "9200:9200"
environment:
- discovery.type=single-node
- xpack.security.enabled=true
- ELASTIC_PASSWORD=elastic
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- esdata:/usr/share/elasticsearch/data
healthcheck:
test: ["CMD-SHELL", "curl -u elastic:elastic -f http://localhost:9200/_cluster/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
kibana:
image: docker.elastic.co/kibana/kibana:7.17.9
container_name: kibana
restart: unless-stopped
depends_on:
elasticsearch:
condition: service_healthy
ports:
- "5601:5601"
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
- ELASTICSEARCH_USERNAME=elastic
- ELASTICSEARCH_PASSWORD=elastic
zookeeper:
image: confluentinc/cp-zookeeper:7.3.2
container_name: zookeeper
restart: unless-stopped
ports:
- "2181:2181"
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
healthcheck:
test: ["CMD", "echo", "stat", "|", "nc", "localhost", "2181"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
kafka:
image: confluentinc/cp-kafka:7.3.2
container_name: kafka
restart: unless-stopped
depends_on:
zookeeper:
condition: service_healthy
ports:
- "9092:9092"
environment:
KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181"
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_DELETE_TOPIC_ENABLE: "true"
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "9092"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
kafdrop:
image: obsidiandynamics/kafdrop
container_name: kafdrop
restart: unless-stopped
depends_on:
kafka:
condition: service_healthy
ports:
- "8085:9000"
environment:
KAFKA_BROKERCONNECT: kafka:29092
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
account-postgres-data:
product-postgres-data:
esdata:
```
## /docs/01-Quick-Setup.md
# 01 - Quick Setup Guide
🚀 **Get the Spring Microservices Blueprint running in 5 minutes!**
## ⚡ One-Command Setup
**Build and start everything:**
```bash
# Step 1: Build Maven artifacts
mvn clean install -DskipTests
# Step 2: Build Docker images and start all services
docker compose up -d --build
```
> **Why both commands?** Maven builds the JAR files, Docker builds the container images with those JARs.
## ⏱️ Wait for Services (30-60 seconds)
**Check service status:**
```bash
docker compose ps
```
**Expected output - all services should show `Up` and `(healthy)`:**
```
NAME STATUS
account-service-app Up (healthy)
product-service-app Up (healthy)
kafka Up (healthy)
elasticsearch Up (healthy)
zookeeper Up (healthy)
account-db Up (healthy)
product-db Up (healthy)
kibana Up
fluentd Up
kafdrop Up (healthy)
```
## ✅ Verify Everything Works
### Test 1: Account Service
```bash
curl http://localhost:8088/api/test/all
```
**Expected:** `"Public Content."`
### Test 2: Product Service
```bash
curl http://localhost:8089/api/product/search
```
**Expected:** JSON response with products
### Test 3: Health Checks
```bash
curl http://localhost:8088/actuator/health
curl http://localhost:8089/actuator/health
```
**Expected:** `{"status":"UP"}`
## 🌐 Access All Services
Once everything is running:
| Service | URL | Login | Purpose |
|---------|-----|-------|---------|
| **Account Service API** | http://localhost:8088 | - | User management & auth |
| **Product Service API** | http://localhost:8089 | - | Product operations |
| **Kibana (Logs)** | http://localhost:5601 | elastic/elastic | View application logs |
| **Kafdrop (Kafka UI)** | http://localhost:8085 | - | Monitor Kafka messages |
| **Elasticsearch** | http://localhost:9200 | elastic/elastic | Search engine |
## 🎮 Quick Demo
### Register and Login
```bash
# Register a new user
curl -X POST http://localhost:8088/api/auth/signup \
-H "Content-Type: application/json" \
-d '{"username": "testuser", "email": "test@example.com", "password": "password123"}'
# Login to get JWT token
curl -X POST http://localhost:8088/api/auth/signin \
-H "Content-Type: application/json" \
-d '{"username": "testuser", "password": "password123"}'
```
### Search Products
```bash
# Search all products (no authentication needed)
curl "http://localhost:8089/api/product/search"
# Search with filters
curl "http://localhost:8089/api/product/search?name=Sample&page=0&size=5"
```
### View Logs
1. Open http://localhost:5601
2. Login with `elastic/elastic`
3. Create index pattern: `fluentd-*`
4. Go to Discover to view logs
## 🔧 Troubleshooting Setup
### Issue: Services not starting
```bash
# Check what's running
docker compose ps
# View logs for failed services
docker compose logs account-service-app
docker compose logs product-service-app
```
### Issue: Port conflicts
```bash
# Check if ports are in use
netstat -an | findstr :8088
netstat -an | findstr :8089
# Kill processes using the ports or change ports in docker-compose.yml
```
### Issue: Build failures
```bash
# Clean Maven cache
mvn clean
# Rebuild everything
mvn clean install -DskipTests
# Clean Docker and rebuild
docker compose down -v
docker system prune -f
docker compose up -d --build
```
### Issue: Out of disk space
```bash
# Clean up Docker resources
docker system prune -af --volumes
# This will free up space used by unused containers, images, and volumes
```
### Issue: Services show "unhealthy"
```bash
# Wait longer - services need time to start
sleep 60
docker compose ps
# Check specific service logs
docker compose logs -f account-service-app
```
## 🛑 Stop Everything
```bash
# Stop all services (keeps data)
docker compose down
# Stop and remove all data (clean slate)
docker compose down -v
```
## 🔄 Daily Development Commands
### After changing code:
```bash
# For commons module changes (affects all services)
mvn clean install -DskipTests
docker compose up -d --build --force-recreate
# For account-service changes only
docker compose up -d --build --force-recreate account-service-app
# For product-service changes only
docker compose up -d --build --force-recreate product-service-app
```
### View logs:
```bash
# All services
docker compose logs -f
# Specific services
docker compose logs -f account-service-app product-service-app
# Last 100 lines
docker compose logs --tail=100 account-service-app
```
## 📊 System Resource Usage
**Typical resource usage:**
- **RAM:** ~6-8GB total
- **CPU:** Moderate during startup, low during idle
- **Disk:** ~2-3GB for images and data
- **Network:** Local ports 5432, 5434, 5601, 8085, 8088, 8089, 9092, 9200, 24224
**If you have limited resources:**
```bash
# Start only essential services
docker compose up -d account-service-db product-service-db kafka zookeeper
docker compose up -d account-service-app product-service-app
# Add monitoring later
docker compose up -d elasticsearch kibana fluentd kafdrop
```
## 🎯 Next Steps
Now that everything is running:
1. **Try the APIs** → [03-API-Reference.md](03-API-Reference.md)
2. **Understand the system** → [02-System-Architecture.md](02-System-Architecture.md)
3. **Import Postman collection** → [10-Postman-Collection.md](10-Postman-Collection.md)
4. **Start developing** → [04-Development-Guide.md](04-Development-Guide.md)
## 🆘 Need Help?
- **Common issues:** [09-Troubleshooting.md](09-Troubleshooting.md)
- **Configuration:** [11-Configuration-Reference.md](11-Configuration-Reference.md)
- **Docker operations:** [07-Docker-Operations.md](07-Docker-Operations.md)
---
**Setup complete! 🎉 Ready to explore microservices!**
## /docs/02-System-Architecture.md
# 02 - System Architecture Guide
🏗️ **Understanding the microservices architecture and design patterns**
## 🏗️ Multi-Module Maven Architecture
This project demonstrates **Maven Multi-Module** best practices for microservices development:
### Project Structure
```
spring-microservices-blueprint/
├── pom.xml # Parent POM with dependency management
├── commons/ # Shared utilities and DTOs
│ ├── pom.xml # Commons module POM
│ └── src/main/java/
│ └── com/microservices/commons/
│ ├── dto/ # Shared Data Transfer Objects
│ ├── exception/ # Common exception classes
│ └── util/ # Utility classes
├── account-service/ # User management microservice
│ ├── pom.xml # Account service POM
│ ├── Dockerfile # Container build file
│ └── src/main/java/
│ └── com/microservices/account/
└── product-service/ # Product management microservice
├── pom.xml # Product service POM
├── Dockerfile # Container build file
└── src/main/java/
└── com/microservices/product/
```
### Maven Module Dependencies
```
Parent POM
├── Commons Module (shared utilities)
├── Account Service Module
│ └── depends on: Commons
└── Product Service Module
└── depends on: Commons
```
### Benefits of Multi-Module Architecture
- **Shared Dependencies**: Common libraries managed in parent POM
- **Code Reusability**: Shared DTOs and utilities in commons module
- **Consistent Versioning**: All modules use same version from parent
- **Simplified Build**: Single `mvn clean install` builds everything
- **Dependency Management**: Centralized version control for all dependencies
- **IDE Support**: Better project navigation and refactoring
### Build Process
```bash
# Build all modules in correct order
mvn clean install -DskipTests
# Build specific module (with dependencies)
mvn clean install -pl account-service -am -DskipTests
# Build without dependencies
mvn clean install -pl commons -DskipTests
```
## 🎯 Architecture Overview
This project implements a **distributed microservices architecture** with modern patterns and technologies for learning purposes.
```
┌─────────────────────────────────────────────────────────────┐
│ Client Layer │
│ Web Browser, Mobile App, Postman, Swagger UI, curl │
└─────────────────────┬───────────────────────────────────────┘
│ HTTP/REST
┌─────────────────────▼───────────────────────────────────────┐
│ Service Layer │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Account Service │◄────────┤ Product Service │ │
│ │ Port 8088 │ Feign │ Port 8089 │ │
│ │ │ Client │ │ │
│ │ • Authentication│ │ • Product CRUD │ │
│ │ • User Mgmt │ │ • Search/Filter │ │
│ │ • JWT Tokens │ │ • Authorization │ │
│ │ • Swagger UI │ │ • Swagger UI │ │
│ └─────────┬───────┘ └─────────┬───────┘ │
└────────────┼─────────────────────────────┼───────────────────┘
│ │
│ Kafka Events │ Kafka Events
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Message Layer │
│ ┌─────────────────┐ │
│ │ Kafka Cluster │ │
│ │ Port 9092 │ │
│ │ │ │
│ │ Topics: │ │
│ │ • user-events │ │
│ │ • product-events│ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ │
┌────────────▼───────────┐ ┌───────────▼───────────────────┐
│ Data Layer │ │ Observability Layer │
│ ┌─────────┐ ┌────────┐ │ │ ┌─────────────┐ ┌───────────┐ │
│ │Account │ │Product │ │ │ │Elasticsearch│ │ Kibana │ │
│ │ DB │ │ DB │ │ │ │Port 9200 │ │Port 5601 │ │
│ │(5432) │ │(5434) │ │ │ │ │ │ │ │
│ └─────────┘ └────────┘ │ │ │ Log Storage │ │Log Viewer │ │
└─────────────────────────┘ │ └─────────────┘ └───────────┘ │
│ ┌─────────────┐ ┌───────────┐ │
│ │ Fluentd │ │ Kafdrop │ │
│ │(24224) │ │(8085) │ │
│ │Log Shipper │ │Kafka UI │ │
│ └─────────────┘ └───────────┘ │
└─────────────────────────────────┘
```
## 🎯 Core Services
### Account Service (Port 8088)
**Primary Responsibilities:**
- User registration and authentication
- JWT token generation and validation
- User profile management
- Role-based access control (RBAC)
**Key Features:**
- Spring Security with JWT
- Password encryption (BCrypt)
- Role management (USER, ADMIN)
- User profile APIs
- **Swagger UI:** http://localhost:8088/swagger-ui/index.html
**Technology Stack:**
- Spring Boot 3.x
- Spring Security 6.x
- Spring Data JPA
- PostgreSQL
- JWT (jsonwebtoken)
- Swagger/OpenAPI 3
**Database Schema:**
```sql
-- Users table
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(20) UNIQUE NOT NULL,
email VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(120) NOT NULL,
enabled BOOLEAN DEFAULT true
);
-- Roles table
CREATE TABLE roles (
id SERIAL PRIMARY KEY,
name VARCHAR(20) UNIQUE NOT NULL
);
-- User-Role mapping
CREATE TABLE user_roles (
user_id BIGINT REFERENCES users(id),
role_id INT REFERENCES roles(id),
PRIMARY KEY (user_id, role_id)
);
```
### Product Service (Port 8089)
**Primary Responsibilities:**
- Product CRUD operations
- Advanced product search with filtering
- Product ownership validation
- Business logic for product management
**Key Features:**
- Advanced search with pagination
- Price range filtering
- Creator-based filtering
- Role-based product creation (ADMIN only)
- **Swagger UI:** http://localhost:8089/swagger-ui/index.html
**Technology Stack:**
- Spring Boot 3.x
- Spring Data JPA
- PostgreSQL
- OpenFeign (service communication)
- Swagger/OpenAPI 3
**Database Schema:**
```sql
-- Products table
CREATE TABLE product (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10,2),
creator_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Indexes for performance
CREATE INDEX idx_product_creator_id ON product(creator_id);
CREATE INDEX idx_product_name ON product(name);
CREATE INDEX idx_product_price ON product(price);
```
## 🔄 Communication Patterns
### 1. Synchronous Communication (REST + Feign)
**Implementation:**
```java
// Product Service calls Account Service
@FeignClient(name = "account-service", url = "${app.services.account-service.url}")
public interface AccountServiceClient {
@GetMapping("/api/user/{userId}")
UserProfile getUserById(@PathVariable Long userId);
}
```
**Communication Flow:**
```
Product Service ──HTTP──► Account Service
│ │
│ 1. GET /api/user/123 │
│ ──────────────────────► │
│ │ 2. Query Database
│ │ ──────────────►
│ │
│ 3. UserProfile JSON │ 4. Return User
│ ◄────────────────────── │ ◄──────────────
│ │
5. Continue Processing
```
**Use Cases:**
- Product Service validates user existence before operations
- Real-time data consistency requirements
- Direct API calls between services
### 2. Asynchronous Communication (Kafka)
**Event Publishing:**
```java
// Account Service publishes user events
@EventListener
public void handleUserRegistration(UserRegistrationEvent event) {
kafkaTemplate.send("user-events", event.getUserId().toString(), event);
}
// Product Service publishes product events
@EventListener
public void handleProductCreation(ProductCreatedEvent event) {
kafkaTemplate.send("product-events", event.getProductId().toString(), event);
}
```
**Event Consumption:**
```java
// Cross-service event handling
@KafkaListener(topics = "product-events")
public void handleProductEvent(ProductCreatedEvent event) {
// Update user statistics
userService.updateProductStats(event.getCreatorId(), "CREATED");
}
```
**Message Flow:**
```
Service A ──publish──► Kafka Topic ──consume──► Service B
│ │ │
│ 1. Product Created │ │
│ ────────────────────► │ │
│ │ 2. Event Available │
│ │ ────────────────────► │
│ │ │ 3. Process Event
│ │ │ ──────────────►
```
**Use Cases:**
- Event-driven architecture
- Audit logging and analytics
- Loose coupling between services
- Eventual consistency
## 🗄️ Data Architecture
### Database Per Service Pattern
Each service owns its data completely:
**Benefits:**
- **Data Encapsulation:** Services can't directly access other services' data
- **Technology Diversity:** Each service can use different database technologies
- **Independent Scaling:** Scale databases based on service needs
- **Fault Isolation:** Database issues don't cascade across services
**Challenges:**
- **Data Consistency:** Must handle eventual consistency
- **Cross-Service Queries:** Need to aggregate data from multiple services
- **Transaction Management:** Distributed transactions are complex
### Data Consistency Strategy
**Strong Consistency:** Within service boundaries
```java
@Transactional
public Product createProduct(CreateProductRequest request) {
// All operations within same database transaction
Product product = new Product(request);
product = productRepository.save(product);
// Publish event after successful transaction
publishProductCreatedEvent(product);
return product;
}
```
**Eventual Consistency:** Across service boundaries
```java
@KafkaListener(topics = "user-events")
public void handleUserEvent(UserEvent event) {
// Eventually consistent update based on events
if (event.getType() == UserEventType.DELETED) {
productService.handleUserDeletion(event.getUserId());
}
}
```
## 🔐 Security Architecture
### JWT-Based Authentication Flow
```
┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Client │ │ Account Service │ │ Product Service │
└──────┬──────┘ └─────────┬───────┘ └─────────┬───────┘
│ │ │
│ 1. Login Request │ │
├────────────────────►│ │
│ │ │
│ 2. JWT Token │ │
│◄────────────────────┤ │
│ │ │
│ 3. API Call + JWT │ │
├─────────────────────┼─────────────────────►│
│ │ │
│ │ 4. Validate User │
│ │◄─────────────────────┤
│ │ │
│ │ 5. User Info │
│ ├─────────────────────►│
│ │ │
│ 6. API Response │ │
│◄─────────────────────┼──────────────────────┤
```
### Role-Based Access Control (RBAC)
**Role Hierarchy:**
```
ROLE_ADMIN
│
├── Can create products
├── Can update any product
├── Can delete any product
├── Can access admin endpoints
└── Has all USER permissions
ROLE_USER
│
├── Can view own profile
├── Can search products
├── Can update own products
└── Can access user endpoints
```
**Implementation:**
```java
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/api/product")
public ResponseEntity<Product> createProduct(@RequestBody CreateProductRequest request) {
// Only admins can create products
}
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
@GetMapping("/api/user/me")
public ResponseEntity<UserProfile> getCurrentUser() {
// Both users and admins can access
}
@PreAuthorize("@productService.isOwnerOrAdmin(#productId, authentication.name)")
@PatchMapping("/api/product/{productId}")
public ResponseEntity<Product> updateProduct(@PathVariable Long productId, @RequestBody UpdateProductRequest request) {
// Only product owner or admin can update
}
```
## 📊 Observability Architecture
### Centralized Logging (ELK Stack)
**Log Flow:**
```
┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ ┌─────────────┐
│ Service │ │ Fluentd │ │ Elasticsearch │ │ Kibana │
│ Logs ├───►│ (Collector) ├───►│ (Storage) ├───►│ (Visualize) │
└─────────────┘ └─────────────┘ └─────────────────┘ └─────────────┘
```
**Structured Logging:**
```java
// Automatic correlation ID injection
@Component
public class CorrelationLoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
String correlationId = UUID.randomUUID().toString();
MDC.put("correlationId", correlationId);
try {
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
```
**Log Format:**
```json
{
"@timestamp": "2024-01-01T12:00:00.000Z",
"level": "INFO",
"logger_name": "com.demo.service.ProductService",
"thread_name": "http-nio-8089-exec-1",
"correlation_id": "abc123-def456-ghi789",
"request_method": "POST",
"request_url": "/api/product",
"message": "Product created successfully",
"user_id": "123",
"product_id": "456"
}
```
### Distributed Tracing
**Correlation ID Flow:**
```
Client Request ──correlation_id──► Service A ──same_id──► Service B ──same_id──► Database
│ │ │ │
│ │ Log: correlation_id │ Log: correlation_id │ Log: correlation_id
│ │ ──────────────────► │ ──────────────────► │ ──────────────────►
│ │ │ │
│ │ │ │
│ ◄──────────────────────────── │ ◄──────────────────── │ ◄──────────────────── │
Response with same correlation_id
```
**Implementation:**
```java
// Automatic correlation ID propagation
@Component
public class CorrelationInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(
HttpRequest request,
byte[] body,
ClientHttpRequestExecution execution) throws IOException {
String correlationId = MDC.get("correlationId");
if (correlationId != null) {
request.getHeaders().add("X-Correlation-ID", correlationId);
}
return execution.execute(request, body);
}
}
```
## 🚀 Deployment Architecture
### Containerization Strategy
**Multi-stage Docker Build:**
```dockerfile
# Build stage
FROM maven:3.9.6-eclipse-temurin-17-alpine AS build
WORKDIR /app
COPY pom.xml ./
COPY commons/pom.xml commons/pom.xml
COPY account-service/pom.xml account-service/pom.xml
COPY product-service/pom.xml product-service/pom.xml
RUN mvn -q -DskipTests dependency:go-offline
COPY . .
RUN mvn -q -pl account-service -am -DskipTests package
# Runtime stage
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && adduser -u 1001 -S appuser -G appgroup
RUN apk add --no-cache curl
COPY --from=build --chown=appuser:appgroup /app/account-service/target/*.jar app.jar
USER appuser
EXPOSE 8088 5005
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8088/actuator/health || exit 1
ENTRYPOINT ["java", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "/app/app.jar"]
```
**Benefits:**
- **Smaller images:** Multi-stage builds reduce final image size
- **Security:** Non-root user execution
- **Debugging:** Debug ports enabled for development
- **Health checks:** Container health monitoring
- **Resource limits:** JVM container awareness
### Container Orchestration
**Docker Compose Structure:**
```yaml
services:
# Application Services
account-service-app:
build: ./account-service
depends_on:
account-service-db:
condition: service_healthy
kafka:
condition: service_healthy
environment:
- SPRING_PROFILES_ACTIVE=docker
- DB_URL=jdbc:postgresql://account-service-db:5432/account-service
ports:
- "8088:8088"
- "5005:5005" # Debug port
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8088/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
restart: unless-stopped
# Infrastructure Services
kafka:
image: confluentinc/cp-kafka:7.3.2
depends_on:
zookeeper:
condition: service_healthy
environment:
KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181"
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "9092"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
```
## 🔧 Configuration Management
### Environment-Based Configuration
**Application Properties:**
```yaml
# application.yml (default)
spring:
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
app:
services:
account-service:
url: ${ACCOUNT_SERVICE_URL:http://localhost:8088}
product-service:
url: ${PRODUCT_SERVICE_URL:http://localhost:8089}
---
# application-dev.yml (development)
spring:
profiles: dev
datasource:
url: jdbc:postgresql://localhost:5432/account-service
username: postgres
password: 123456
---
# application-docker.yml (container)
spring:
profiles: docker
datasource:
url: ${DB_URL}
username: postgres
password: 123456
```
**External Configuration:**
```bash
# Environment variables override application properties
export SPRING_PROFILES_ACTIVE=docker
export DB_URL=jdbc:postgresql://account-service-db:5432/account-service
export KAFKA_BOOTSTRAP_SERVERS=kafka:29092
```
## 📈 API Documentation (Swagger)
### Access Swagger UI
**Account Service API Documentation:**
- **URL:** http://localhost:8088/swagger-ui/index.html
- **OpenAPI JSON:** http://localhost:8088/v3/api-docs
**Product Service API Documentation:**
- **URL:** http://localhost:8089/swagger-ui/index.html
- **OpenAPI JSON:** http://localhost:8089/v3/api-docs
### Swagger Configuration
**Implementation:**
```java
@Configuration
@EnableWebSecurity
public class SwaggerConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Account Service API")
.version("1.0")
.description("Microservices Account Management API"))
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
.components(new Components()
.addSecuritySchemes("bearerAuth",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
}
```
**Features:**
- **Interactive API testing** directly in browser
- **JWT authentication** support in Swagger UI
- **Request/response examples** for all endpoints
- **Schema documentation** for all DTOs
- **Try it out** functionality for immediate testing
## 🎯 Design Patterns Implemented
### 1. Microservices Pattern
- **Single Responsibility:** Each service has one business domain
- **Independent Deployment:** Services can be deployed separately
- **Technology Diversity:** Different tech stacks per service (if needed)
- **Fault Isolation:** Failures don't cascade across services
### 2. Database Per Service
- **Data Ownership:** Each service owns its data
- **Schema Independence:** Services can evolve schemas independently
- **Technology Choice:** Different databases per service if needed
- **Scalability:** Scale databases based on service needs
### 3. API Gateway Pattern (Future Enhancement)
- **Single Entry Point:** Centralized client access
- **Cross-cutting Concerns:** Authentication, logging, rate limiting
- **Request Routing:** Route requests to appropriate services
- **Response Aggregation:** Combine responses from multiple services
### 4. Event Sourcing (Partial Implementation)
- **Domain Events:** Business events are captured and stored
- **Event-driven Communication:** Services communicate via events
- **Audit Trail:** Complete history of all changes
- **Eventual Consistency:** Data consistency through events
### 5. CQRS (Command Query Responsibility Segregation) - Future Enhancement
- **Not Currently Implemented:** This pattern is planned for future versions
- **Potential Implementation:** Separate read/write models for product search
- **Benefits:** Would allow optimized queries and independent scaling
### 6. Circuit Breaker (Future Enhancement)
- **Fault Tolerance:** Prevent cascading failures
- **Graceful Degradation:** Fallback responses when services are down
- **Automatic Recovery:** Automatically retry when services recover
## 🔮 Future Architecture Enhancements
### 1. API Gateway (Spring Cloud Gateway)
```yaml
# Future implementation
spring:
cloud:
gateway:
routes:
- id: account-service
uri: http://account-service:8088
predicates:
- Path=/api/auth/**, /api/user/**
- id: product-service
uri: http://product-service:8089
predicates:
- Path=/api/product/**
```
### 2. Service Discovery (Eureka/Consul)
```java
// Future implementation
@EnableEurekaClient
@SpringBootApplication
public class AccountServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AccountServiceApplication.class, args);
}
}
```
### 3. Configuration Server (Spring Cloud Config)
```yaml
# Future implementation
spring:
cloud:
config:
server:
git:
uri: https://github.com/company/microservices-config
```
### 4. Distributed Caching (Redis)
```java
// Future implementation
@Cacheable(value = "users", key = "#userId")
public UserProfile getUserById(Long userId) {
return userRepository.findById(userId);
}
```
### 5. Metrics and Monitoring (Prometheus + Grafana)
```yaml
# Future implementation
management:
endpoints:
web:
exposure:
include: "*"
metrics:
export:
prometheus:
enabled: true
```
---
This architecture provides a solid foundation for learning microservices patterns while maintaining simplicity for educational purposes. Each component is designed to demonstrate real-world patterns and best practices.
**Next:** [03-API-Reference.md](03-API-Reference.md) for detailed API documentation
## /docs/03-API-Reference.md
# 03 - API Reference Guide
📚 **Complete API documentation with examples and testing instructions**
## 🔗 Service Endpoints
- **Account Service:** http://localhost:8088
- **Product Service:** http://localhost:8089
## 🔐 Authentication Overview
Most endpoints require JWT authentication. Get your token by logging in first.
### Quick Auth Flow
```bash
# 1. Register user (optional if user exists)
curl -X POST http://localhost:8088/api/auth/signup \
-H "Content-Type: application/json" \
-d '{"username": "demo", "email": "demo@example.com", "password": "password123"}'
# 2. Login to get JWT token
curl -X POST http://localhost:8088/api/auth/signin \
-H "Content-Type: application/json" \
-d '{"username": "demo", "password": "password123"}'
# 3. Save token and use in requests
TOKEN="your_jwt_token_here"
curl -H "Authorization: Bearer $TOKEN" http://localhost:8088/api/user/me
```
---
## 👤 Account Service APIs
### 🔐 Authentication Endpoints
#### Register User
```http
POST /api/auth/signup
Content-Type: application/json
```
**Request Body:**
```json
{
"username": "vito",
"email": "vito@example.com",
"password": "secret123",
"roles": ["ADMIN"]
}
```
**Role Options:**
- `[]` or omit - Default USER role
- `["ADMIN"]` - Admin role with full permissions
**Success Response (200):**
```json
{
"message": "User registered successfully!"
}
```
**Validation Errors (400):**
```json
{
"statusCode": 400,
"messages": [
"username: size must be between 3 and 20",
"email: must be a well-formed email address"
],
"errorCode": null
}
```
**Example:**
```bash
curl -X POST http://localhost:8088/api/auth/signup \
-H "Content-Type: application/json" \
-d '{"username": "alice", "email": "alice@example.com", "password": "password123", "roles": ["ADMIN"]}'
```
#### Login User
```http
POST /api/auth/signin
Content-Type: application/json
```
**Request Body:**
```json
{
"username": "vito1",
"password": "secret123"
}
```
**Success Response (200):**
```json
{
"token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb2huX2RvZSIsInVzZXJJZCI6IjEwIiwiaWF0IjoxNzU3NjAxMzEzLCJleHAiOjE3NTc2ODc3MTN9.vSasdMos4hvnX7x0wKx43ii7uLbTYxeNr7k7-A3D7ws",
"type": "Bearer",
"id": 10,
"username": "john_doe",
"email": "john@example.com",
"roles": ["ROLE_USER"]
}
```
**Invalid Credentials (401):**
```json
{
"statusCode": 401,
"messages": ["Bad credentials"],
"errorCode": null
}
```
**Example:**
```bash
curl -X POST http://localhost:8088/api/auth/signin \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "password123"}'
```
### 👤 User Management Endpoints
#### Get Current User Profile
```http
GET /api/user/me
Authorization: Bearer {token}
```
**Success Response (200):**
```json
{
"id": 10,
"username": "john_doe",
"email": "john@example.com",
"roles": ["ROLE_USER"]
}
```
**Unauthorized (401):**
```json
{
"statusCode": 401,
"messages": ["Unauthorized"],
"errorCode": null
}
```
**Example:**
```bash
curl -H "Authorization: Bearer $TOKEN" http://localhost:8088/api/user/me
```
#### Get User by ID
```http
GET /api/user/{userId}
```
**Success Response (200):**
```json
{
"id": 1,
"username": "wyatt.ratke",
"email": "chase.waelchi@yahoo.com",
"roles": []
}
```
**User Not Found (404):**
```json
{
"statusCode": 404,
"messages": ["User not found"],
"errorCode": null
}
```
**Example:**
```bash
curl http://localhost:8088/api/user/1
```
### 🧪 Test Endpoints
#### Public Test Endpoint
```http
GET /api/test/all
```
**Response:** `"Public Content."`
**Example:**
```bash
curl http://localhost:8088/api/test/all
```
#### User Test Endpoint
```http
GET /api/test/user
Authorization: Bearer {token}
```
**Response:** `"User Content."`
**Example:**
```bash
curl -H "Authorization: Bearer $TOKEN" http://localhost:8088/api/test/user
```
#### Admin Test Endpoint
```http
GET /api/test/admin
Authorization: Bearer {admin_token}
```
**Response:** `"Admin Board."`
**Access Denied (403):**
```json
{
"statusCode": 403,
"messages": ["Access Denied"],
"errorCode": null
}
```
---
## 🛍️ Product Service APIs
### 🔍 Product Search (Public Access)
#### Search Products
```http
GET /api/product/search
```
**Query Parameters:**
- `id` - Search by product ID
- `name` - Search by name (partial match, case-insensitive)
- `description` - Search by description (partial match)
- `creatorId` - Filter by creator user ID
- `minPrice` - Minimum price filter
- `maxPrice` - Maximum price filter
- `page` - Page number (default: 0)
- `size` - Page size (default: 10)
- `sortBy` - Sort field (default: createdAt)
- `sortDirection` - Sort direction: asc/desc (default: desc)
**Examples:**
```bash
# Get all products
curl "http://localhost:8089/api/product/search"
# Search by name
curl "http://localhost:8089/api/product/search?name=Sample"
# Search by creator
curl "http://localhost:8089/api/product/search?creatorId=1"
# Price range search
curl "http://localhost:8089/api/product/search?minPrice=50&maxPrice=200"
# Paginated search with sorting
curl "http://localhost:8089/api/product/search?page=0&size=5&sortBy=name&sortDirection=asc"
# Combined filters
curl "http://localhost:8089/api/product/search?name=Product&creatorId=1&minPrice=100&page=0&size=10"
```
**Success Response (200) - Paginated:**
```json
{
"content": [
{
"id": 1,
"name": "Sample Product A",
"description": "A great product for testing",
"price": 99.99,
"creatorId": 1,
"createdAt": "2024-01-01T10:00:00",
"updatedAt": "2024-01-01T10:00:00"
}
],
"pageable": {
"sort": [
{
"direction": "ASC",
"property": "name",
"ignoreCase": false,
"nullHandling": "NATIVE",
"ascending": true,
"descending": false
}
],
"offset": 0,
"pageNumber": 0,
"pageSize": 10,
"paged": true,
"unpaged": false
},
"totalElements": 1,
"totalPages": 1,
"last": true,
"first": true,
"numberOfElements": 1
}
```
### 🛍️ Product Management
#### Get Products by User ID
```http
GET /api/product?userId={userId}
```
**Success Response (200):**
```json
[
{
"id": 1,
"name": "Sample Product A",
"price": null,
"createdBy": 1
},
{
"id": 2,
"name": "Sample Product B",
"price": null,
"createdBy": 1
}
]
```
**User Not Found (404):**
```json
{
"statusCode": 404,
"messages": ["User not found"],
"errorCode": null
}
```
**Example:**
```bash
curl "http://localhost:8089/api/product?userId=1"
```
#### Get Current User's Products
```http
GET /api/product/me
Authorization: Bearer {token}
```
**Success Response (200):**
```json
[
{
"id": 3,
"name": "My Product",
"description": "Created by me",
"price": 199.99,
"creatorId": 10,
"createdAt": "2024-01-01T12:00:00",
"updatedAt": "2024-01-01T12:00:00"
}
]
```
**Example:**
```bash
curl -H "Authorization: Bearer $TOKEN" http://localhost:8089/api/product/me
```
#### Create Product (Admin Only)
```http
POST /api/product
Authorization: Bearer {admin_token}
Content-Type: application/json
```
**Request Body:**
```json
{
"name": "My New Product",
"description": "A product created via API",
"price": 199.99
}
```
**Success Response (201):**
```json
{
"id": 3,
"name": "My New Product",
"description": "A product created via API",
"price": 199.99,
"creatorId": 2,
"createdAt": "2024-01-01T12:00:00",
"updatedAt": "2024-01-01T12:00:00"
}
```
**Access Denied (403):**
```json
{
"statusCode": 403,
"messages": ["Access Denied"],
"errorCode": null
}
```
**Example:**
```bash
curl -X POST http://localhost:8089/api/product \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{"name": "My Product", "description": "A great product", "price": 299.99}'
```
#### Update Product
```http
PATCH /api/product/{productId}
Authorization: Bearer {token}
Content-Type: application/json
```
**Request Body (partial update):**
```json
{
"name": "Updated Product Name",
"price": 299.99
}
```
**Success Response (200):**
```json
{
"id": 1,
"name": "Updated Product Name",
"description": "Original description",
"price": 299.99,
"creatorId": 1,
"createdAt": "2024-01-01T10:00:00",
"updatedAt": "2024-01-01T12:30:00"
}
```
**Authorization Rules:**
- Product creators can update their own products
- Users with ROLE_ADMIN can update any product
**Example:**
```bash
curl -X PATCH http://localhost:8089/api/product/1 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"name": "Updated Name", "price": 399.99}'
```
### 🔄 Kafka Integration
#### Generate Random User (Kafka Event)
```http
POST /api/user
correlation_id: {optional_correlation_id}
```
**Purpose:** Triggers Kafka event for testing event-driven architecture
**Success Response (200):**
```json
{
"message": "Random user generated and published to Kafka"
}
```
**Example:**
```bash
curl -X POST http://localhost:8089/api/user \
-H "correlation_id: test123"
```
---
## 🚨 Error Response Format
All APIs return consistent error responses:
```json
{
"statusCode": 400,
"messages": ["Error message 1", "Error message 2"],
"errorCode": "VALIDATION_ERROR"
}
```
### HTTP Status Codes
| Code | Meaning | When |
|------|---------|------|
| **200** | OK | Success |
| **201** | Created | Resource created |
| **400** | Bad Request | Validation errors |
| **401** | Unauthorized | Missing/invalid JWT |
| **403** | Forbidden | Insufficient permissions |
| **404** | Not Found | Resource not found |
| **500** | Internal Server Error | Server error |
### Common Error Examples
#### Validation Error (400)
```bash
curl -X POST http://localhost:8088/api/auth/signup \
-H "Content-Type: application/json" \
-d '{"username": "ab", "email": "", "password": "123"}'
```
**Response:**
```json
{
"statusCode": 400,
"messages": [
"username: size must be between 3 and 20",
"password: size must be between 6 and 40",
"email: must not be blank"
],
"errorCode": null
}
```
#### Unauthorized (401)
```bash
curl http://localhost:8088/api/user/me
```
**Response:**
```json
{
"statusCode": 401,
"messages": ["Unauthorized"],
"errorCode": null
}
```
#### Forbidden (403)
```bash
# Try to create product with USER role
curl -X POST http://localhost:8089/api/product \
-H "Authorization: Bearer $USER_TOKEN" \
-d '{"name": "Test"}'
```
**Response:**
```json
{
"statusCode": 403,
"messages": ["Access Denied"],
"errorCode": null
}
```
---
## 🧪 Complete Testing Script
```bash
#!/bin/bash
echo "=== Spring Boot Microservices API Testing ==="
BASE_ACCOUNT="http://localhost:8088"
BASE_PRODUCT="http://localhost:8089"
# 1. Register admin user
echo "1. Registering admin user..."
curl -s -X POST $BASE_ACCOUNT/api/auth/signup \
-H "Content-Type: application/json" \
-d '{"username": "admin_test", "email": "admin@test.com", "password": "password123", "roles": ["ADMIN"]}'
# 2. Register regular user
echo "2. Registering regular user..."
curl -s -X POST $BASE_ACCOUNT/api/auth/signup \
-H "Content-Type: application/json" \
-d '{"username": "user_test", "email": "user@test.com", "password": "password123"}'
# 3. Login as admin
echo "3. Logging in as admin..."
ADMIN_RESPONSE=$(curl -s -X POST $BASE_ACCOUNT/api/auth/signin \
-H "Content-Type: application/json" \
-d '{"username": "admin_test", "password": "password123"}')
ADMIN_TOKEN=$(echo $ADMIN_RESPONSE | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
# 4. Login as user
echo "4. Logging in as user..."
USER_RESPONSE=$(curl -s -X POST $BASE_ACCOUNT/api/auth/signin \
-H "Content-Type: application/json" \
-d '{"username": "user_test", "password": "password123"}')
USER_TOKEN=$(echo $USER_RESPONSE | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
# 5. Test user profile
echo "5. Getting user profile..."
curl -s -H "Authorization: Bearer $USER_TOKEN" $BASE_ACCOUNT/api/user/me
# 6. Create product (admin only)
echo "6. Creating product as admin..."
curl -s -X POST $BASE_PRODUCT/api/product \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{"name": "Test Product", "description": "Created via API", "price": 99.99}'
# 7. Search products
echo "7. Searching products..."
curl -s "$BASE_PRODUCT/api/product/search?name=Test"
# 8. Get products by user
echo "8. Getting products by user..."
curl -s "$BASE_PRODUCT/api/product?userId=1"
# 9. Test error case
echo "9. Testing error case..."
curl -s "$BASE_PRODUCT/api/product?userId=999"
echo "=== Testing Complete ==="
```
---
## 📚 Related Documentation
- **Postman Collection:** [10-Postman-Collection.md](10-Postman-Collection.md)
- **Testing Guide:** [05-Testing-Guide.md](05-Testing-Guide.md)
- **Development Guide:** [04-Development-Guide.md](04-Development-Guide.md)
---
**Ready to explore the APIs! 🚀**
## /docs/04-Development-Guide.md
# 04 - Development Guide
🛠️ **Complete guide for developing and extending the microservices system**
## 🚀 Development Setup
### Prerequisites
- **Java 17** or higher
- **Maven 3.9+**
- **Docker** and **Docker Compose**
- **IDE** (IntelliJ IDEA, VS Code, Eclipse)
### Quick Development Setup
```bash
# Clone and setup
git clone <repository-url>
cd spring-boot-micro
# Build everything
mvn clean install -DskipTests
# Start development environment
docker compose up -d --build
```
## 📁 Project Structure
```
spring-boot-micro/
├── 📁 commons/ # Shared libraries
│ ├── 📁 src/main/java/com/demo/
│ │ ├── 📁 config/ # Shared configurations
│ │ │ ├── 📁 openfeign/ # Feign client config
│ │ │ ├── 📁 thread/ # Async thread config
│ │ │ └── 📁 web/ # Web configurations
│ │ ├── 📁 dto/ # Data Transfer Objects
│ │ ├── 📁 exception/ # Common exceptions
│ │ ├── 📁 kafka/ # Kafka configurations
│ │ ├── 📁 logging/ # Logging utilities
│ │ └── 📁 security/ # Security utilities
│ └── 📄 pom.xml
├── 📁 account-service/ # User management service
│ ├── 📁 src/main/java/com/demo/
│ │ ├── 📁 config/ # Service-specific config
│ │ ├── 📁 controller/ # REST controllers
│ │ ├── 📁 dto/ # Request/Response DTOs
│ │ ├── 📁 entity/ # JPA entities
│ │ ├── 📁 kafka/ # Kafka producers/consumers
│ │ ├── 📁 repository/ # Data repositories
│ │ ├── 📁 security/ # JWT & Security
│ │ └── 📁 service/ # Business logic
│ ├── 📁 src/main/resources/
│ │ ├── 📄 application.yml # Configuration
│ │ └── 📄 data.sql # Sample data
│ ├── 📄 Dockerfile
│ └── 📄 pom.xml
├── 📁 product-service/ # Product management service
│ ├── 📁 src/main/java/com/demo/
│ │ ├── 📁 client/ # Feign clients
│ │ ├── 📁 controller/ # REST controllers
│ │ ├── 📁 dto/ # Request/Response DTOs
│ │ ├── 📁 entity/ # JPA entities
│ │ ├── 📁 kafka/ # Kafka producers/consumers
│ │ ├── 📁 repository/ # Data repositories
│ │ └── 📁 service/ # Business logic
│ ├── 📁 src/main/resources/
│ │ ├── 📄 application.yml # Configuration
│ │ └── 📄 data.sql # Sample data
│ ├── 📄 Dockerfile
│ └── 📄 pom.xml
├── 📁 fluentd/ # Log aggregation
├── 📁 docs/ # Documentation
├── 📄 docker-compose.yml # Container orchestration
├── 📄 spring microservices.postman_collection.json
└── 📄 pom.xml # Parent POM
```
## 🔄 Development Workflow
### Making Changes to Commons
When you modify shared code in the `commons` module:
```bash
# 1. Build commons and install to local repository
mvn clean install -DskipTests
# 2. Rebuild and restart all services
docker compose up -d --build --force-recreate account-service-app product-service-app
```
### Making Changes to Account Service
```bash
# Option 1: Quick restart (if only Java code changed)
docker compose up -d --build --force-recreate account-service-app
# Option 2: Full rebuild (if dependencies changed)
mvn -pl account-service -am clean install -DskipTests
docker compose build account-service-app
docker compose up -d --force-recreate account-service-app
```
### Making Changes to Product Service
```bash
# Option 1: Quick restart
docker compose up -d --build --force-recreate product-service-app
# Option 2: Full rebuild
mvn -pl product-service -am clean install -DskipTests
docker compose build product-service-app
docker compose up -d --force-recreate product-service-app
```
### Clean Restart Everything
```bash
# Stop everything and clean up
docker compose down -v
# Rebuild from scratch
mvn clean install -DskipTests
docker compose build
docker compose up -d
```
## 🐛 Debugging Setup
### Debug Ports (Already Enabled)
- **Account Service:** localhost:5005
- **Product Service:** localhost:5006
### IntelliJ IDEA Setup
1. **Run → Edit Configurations**
2. **Add → Remote JVM Debug**
3. **Configure:**
- Host: `localhost`
- Port: `5005` (Account) or `5006` (Product)
- Module: Select your service module
4. **Start debugging and set breakpoints**
### VS Code Setup
Add to `.vscode/launch.json`:
```json
{
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "Debug Account Service",
"request": "attach",
"hostName": "localhost",
"port": 5005
},
{
"type": "java",
"name": "Debug Product Service",
"request": "attach",
"hostName": "localhost",
"port": 5006
}
]
}
```
## 🧪 Testing During Development
### Unit Tests
```bash
# Run tests for specific service
mvn -pl account-service test
mvn -pl product-service test
# Run all tests
mvn test
```
### Integration Tests
```bash
# Start test environment
docker compose up -d
# Wait for services to be ready
sleep 30
# Run API tests
curl http://localhost:8088/api/test/all
curl http://localhost:8089/api/product/search
```
### API Testing with Swagger
- **Account Service:** http://localhost:8088/swagger-ui/index.html
- **Product Service:** http://localhost:8089/swagger-ui/index.html
## 🔄 Adding New Features
### Adding a New REST Endpoint
#### 1. Create DTO Classes
```java
// Request DTO
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateFeatureRequest {
@NotBlank(message = "Name is required")
private String name;
@NotNull(message = "Description is required")
private String description;
@DecimalMin(value = "0.0", message = "Price must be positive")
private BigDecimal price;
}
// Response DTO
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FeatureResponse {
private Long id;
private String name;
private String description;
private BigDecimal price;
private Long creatorId;
private LocalDateTime createdAt;
}
```
#### 2. Create Controller
```java
@RestController
@RequestMapping("/api/feature")
@Tag(name = "Feature Management", description = "APIs for managing features")
public class FeatureController {
@Autowired
private FeatureService featureService;
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Create new feature", description = "Create a new feature (Admin only)")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "Feature created successfully"),
@ApiResponse(responseCode = "400", description = "Invalid input"),
@ApiResponse(responseCode = "403", description = "Access denied")
})
public ResponseEntity<FeatureResponse> createFeature(
@Valid @RequestBody CreateFeatureRequest request,
Authentication authentication) {
FeatureResponse response = featureService.createFeature(request, authentication.getName());
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@GetMapping("/{id}")
@Operation(summary = "Get feature by ID")
public ResponseEntity<FeatureResponse> getFeature(@PathVariable Long id) {
FeatureResponse response = featureService.getFeatureById(id);
return ResponseEntity.ok(response);
}
}
```
#### 3. Implement Service Logic
```java
@Service
@Transactional
public class FeatureService {
@Autowired
private FeatureRepository featureRepository;
@Autowired
private UserService userService;
public FeatureResponse createFeature(CreateFeatureRequest request, String username) {
// Get current user
UserProfile user = userService.getUserByUsername(username);
// Create feature entity
Feature feature = new Feature();
feature.setName(request.getName());
feature.setDescription(request.getDescription());
feature.setPrice(request.getPrice());
feature.setCreatorId(user.getId());
feature.setCreatedAt(LocalDateTime.now());
// Save to database
feature = featureRepository.save(feature);
// Publish event
publishFeatureCreatedEvent(feature);
// Return response
return mapToResponse(feature);
}
private void publishFeatureCreatedEvent(Feature feature) {
FeatureCreatedEvent event = new FeatureCreatedEvent(
feature.getId(),
feature.getName(),
feature.getCreatorId(),
LocalDateTime.now()
);
applicationEventPublisher.publishEvent(event);
}
}
```
#### 4. Test the Endpoint
```bash
# Test with Swagger UI
# Go to http://localhost:8088/swagger-ui/index.html
# Or test with curl
curl -X POST http://localhost:8088/api/feature \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-d '{"name": "New Feature", "description": "Feature description", "price": 99.99}'
```
### Adding Inter-Service Communication
#### 1. Create Feign Client
```java
@FeignClient(name = "target-service", url = "${app.services.target-service.url}")
public interface TargetServiceClient {
@GetMapping("/api/data/{id}")
@Operation(summary = "Get data from target service")
DataResponse getData(@PathVariable("id") Long id);
@PostMapping("/api/data")
@Operation(summary = "Create data in target service")
DataResponse createData(@RequestBody CreateDataRequest request);
}
```
#### 2. Configure Service URL
```yaml
# application.yml
app:
services:
target-service:
url: ${TARGET_SERVICE_URL:http://localhost:8090}
```
#### 3. Use in Service
```java
@Service
public class MyService {
@Autowired
private TargetServiceClient targetServiceClient;
public void processData(Long id) {
try {
DataResponse data = targetServiceClient.getData(id);
// Process data
log.info("Received data: {}", data);
} catch (FeignException e) {
log.error("Failed to get data from target service: {}", e.getMessage());
throw new ServiceCommunicationException("Target service unavailable");
}
}
}
```
### Adding Kafka Events
#### 1. Create Event Class
```java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FeatureCreatedEvent {
private Long featureId;
private String featureName;
private Long creatorId;
private LocalDateTime timestamp;
private String eventType = "FEATURE_CREATED";
}
```
#### 2. Publish Event
```java
@Service
public class FeatureEventPublisher {
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
@EventListener
public void handleFeatureCreated(FeatureCreatedEvent event) {
try {
kafkaTemplate.send("feature-events", event.getFeatureId().toString(), event);
log.info("Published feature created event: {}", event.getFeatureId());
} catch (Exception e) {
log.error("Failed to publish feature event: {}", e.getMessage());
}
}
}
```
#### 3. Consume Event
```java
@Component
public class FeatureEventConsumer {
@KafkaListener(topics = "feature-events")
public void handleFeatureEvent(FeatureCreatedEvent event) {
try {
log.info("Received feature event: {} for feature: {}",
event.getEventType(), event.getFeatureName());
// Process event (update statistics, send notifications, etc.)
processFeatureEvent(event);
} catch (Exception e) {
log.error("Failed to process feature event: {}", e.getMessage());
}
}
private void processFeatureEvent(FeatureCreatedEvent event) {
// Business logic for handling the event
// Update user statistics, send notifications, etc.
}
}
```
## 📊 Development Monitoring
### Application Logs
```bash
# View structured logs in Kibana
# 1. Open http://localhost:5601
# 2. Login with elastic/elastic
# 3. Create index pattern: fluentd-*
# 4. View logs in Discover tab
# Or view logs directly
docker compose logs -f account-service-app product-service-app
```
### Kafka Messages
```bash
# View Kafka topics and messages
# 1. Open http://localhost:8085
# 2. Browse topics: user-events, product-events
# 3. View message details
# Or use command line
docker exec kafka kafka-console-consumer --bootstrap-server localhost:9092 --topic user-events --from-beginning
```
### Database Changes
```bash
# Connect to databases
docker exec -it account-db psql -U postgres -d account-service
docker exec -it product-db psql -U postgres -d product-service
# View tables and data
\dt
SELECT * FROM users LIMIT 5;
SELECT * FROM product LIMIT 5;
# Monitor database activity
SELECT * FROM pg_stat_activity;
```
### Health Checks
```bash
# Check service health
curl http://localhost:8088/actuator/health
curl http://localhost:8089/actuator/health
# View detailed health info
curl http://localhost:8088/actuator/health/db
curl http://localhost:8089/actuator/health/kafka
# Check all actuator endpoints
curl http://localhost:8088/actuator
```
## 🚀 Performance Optimization
### Development Performance
- Use Docker layer caching for faster builds
- Run only needed services during development
- Use IDE's hot reload capabilities
- Keep test data minimal
### Build Optimization
```bash
# Parallel builds
mvn -T 4 clean install -DskipTests
# Skip tests during development
mvn clean install -DskipTests
# Build specific modules only
mvn -pl account-service -am clean install -DskipTests
```
### Docker Optimization
```bash
# Use build cache
docker compose build
# Rebuild without cache (when needed)
docker compose build --no-cache
# Remove unused images
docker image prune -f
```
## 🔧 Configuration Management
### Environment-Specific Configuration
```yaml
# application.yml
spring:
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
---
spring:
profiles: dev
datasource:
url: jdbc:postgresql://localhost:5432/account-service
kafka:
bootstrap-servers: localhost:9092
---
spring:
profiles: docker
datasource:
url: jdbc:postgresql://account-service-db:5432/account-service
kafka:
bootstrap-servers: kafka:29092
```
### External Configuration
```bash
# Override via environment variables
export SPRING_PROFILES_ACTIVE=dev
export DB_URL=jdbc:postgresql://localhost:5432/account-service
# Or via Docker Compose
environment:
- SPRING_PROFILES_ACTIVE=docker
- DB_URL=jdbc:postgresql://account-service-db:5432/account-service
```
## 📚 Development Best Practices
### Code Organization
- Keep controllers thin, put logic in services
- Use DTOs for API contracts
- Implement proper exception handling
- Add validation annotations
- Write meaningful tests
### Database Design
- Use appropriate indexes
- Follow naming conventions
- Implement proper constraints
- Use migrations for schema changes
### API Design
- Follow RESTful principles
- Use consistent response formats
- Implement proper HTTP status codes
- Add comprehensive validation
- Document all endpoints with Swagger
### Security
- Always validate input
- Use parameterized queries
- Implement proper authorization
- Log security events
- Keep dependencies updated
### Logging
- Use structured logging (JSON format)
- Include correlation IDs
- Log at appropriate levels
- Don't log sensitive information
- Use meaningful log messages
### Error Handling
```java
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ErrorResponse> handleValidation(ValidationException e) {
ErrorResponse error = new ErrorResponse(400, e.getMessages(), "VALIDATION_ERROR");
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException e) {
ErrorResponse error = new ErrorResponse(404, List.of(e.getMessage()), "NOT_FOUND");
return ResponseEntity.notFound().build();
}
}
```
## 🔄 Development Lifecycle
### Feature Development Process
1. **Create feature branch** from main
2. **Implement feature** with tests
3. **Test locally** with Docker environment
4. **Update documentation** if needed
5. **Create pull request**
6. **Code review** and merge
### Testing Strategy
- **Unit tests** for business logic
- **Integration tests** for API endpoints
- **Contract tests** for service communication
- **End-to-end tests** for critical user journeys
### Deployment Pipeline
1. **Code commit** triggers build
2. **Run tests** (unit, integration)
3. **Build Docker images**
4. **Deploy to staging**
5. **Run E2E tests**
6. **Deploy to production**
---
**Happy Coding! 🚀**
Next: [05-Testing-Guide.md](05-Testing-Guide.md) for comprehensive testing strategies
## /docs/05-Testing-Guide.md
# 05 - Testing Guide
🧪 **Complete testing strategy for microservices system**
## 🚀 Quick Test
**Verify everything is working:**
```bash
# 1. Start services
mvn clean install -DskipTests
docker compose up -d --build
# 2. Wait for services to be ready (30-60 seconds)
docker compose ps
# 3. Quick health check
curl http://localhost:8088/api/test/all
curl http://localhost:8089/api/product/search
```
If both return data, you're ready to go! 🎉
## 🧪 Testing Levels
### 1. Smoke Tests (Basic Functionality)
**Purpose:** Verify services are up and responding
```bash
#!/bin/bash
echo "=== Smoke Tests ==="
# Test Account Service
echo "Testing Account Service..."
ACCOUNT_RESPONSE=$(curl -s http://localhost:8088/api/test/all)
if [ "$ACCOUNT_RESPONSE" = '"Public Content."' ]; then
echo "✅ Account Service: OK"
else
echo "❌ Account Service: FAILED"
echo "Response: $ACCOUNT_RESPONSE"
fi
# Test Product Service
echo "Testing Product Service..."
PRODUCT_RESPONSE=$(curl -s http://localhost:8089/api/product/search)
if [[ $PRODUCT_RESPONSE == *"content"* ]]; then
echo "✅ Product Service: OK"
else
echo "❌ Product Service: FAILED"
echo "Response: $PRODUCT_RESPONSE"
fi
# Test Health Endpoints
echo "Testing Health Endpoints..."
ACCOUNT_HEALTH=$(curl -s http://localhost:8088/actuator/health | grep -o '"status":"UP"')
PRODUCT_HEALTH=$(curl -s http://localhost:8089/actuator/health | grep -o '"status":"UP"')
if [ "$ACCOUNT_HEALTH" = '"status":"UP"' ]; then
echo "✅ Account Health: OK"
else
echo "❌ Account Health: FAILED"
fi
if [ "$PRODUCT_HEALTH" = '"status":"UP"' ]; then
echo "✅ Product Health: OK"
else
echo "❌ Product Health: FAILED"
fi
echo "=== Smoke Tests Complete ==="
```
### 2. API Tests (Functional Testing)
#### Authentication Flow Test
```bash
#!/bin/bash
echo "=== Authentication Flow Test ==="
BASE_URL="http://localhost:8088"
TIMESTAMP=$(date +%s)
# Register new user
echo "1. Registering user..."
REGISTER_RESPONSE=$(curl -s -X POST $BASE_URL/api/auth/signup \
-H "Content-Type: application/json" \
-d '{
"username": "testuser_'$TIMESTAMP'",
"email": "test'$TIMESTAMP'@example.com",
"password": "password123"
}')
if [[ $REGISTER_RESPONSE == *"successfully"* ]]; then
echo "✅ User Registration: OK"
USERNAME="testuser_$TIMESTAMP"
else
echo "❌ User Registration: FAILED"
echo "Response: $REGISTER_RESPONSE"
exit 1
fi
# Login user
echo "2. Logging in..."
LOGIN_RESPONSE=$(curl -s -X POST $BASE_URL/api/auth/signin \
-H "Content-Type: application/json" \
-d '{
"username": "testuser_'$TIMESTAMP'",
"password": "password123"
}')
TOKEN=$(echo $LOGIN_RESPONSE | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
if [ ! -z "$TOKEN" ]; then
echo "✅ User Login: OK"
echo "Token: ${TOKEN:0:20}..."
else
echo "❌ User Login: FAILED"
echo "Response: $LOGIN_RESPONSE"
exit 1
fi
# Test authenticated endpoint
echo "3. Testing authenticated endpoint..."
PROFILE_RESPONSE=$(curl -s -H "Authorization: Bearer $TOKEN" $BASE_URL/api/user/me)
if [[ $PROFILE_RESPONSE == *"username"* ]]; then
echo "✅ Authenticated Access: OK"
else
echo "❌ Authenticated Access: FAILED"
echo "Response: $PROFILE_RESPONSE"
fi
echo "=== Authentication Flow Test Complete ==="
```
#### Product Service Test
```bash
#!/bin/bash
echo "=== Product Service Test ==="
BASE_URL="http://localhost:8089"
# Test public search
echo "1. Testing public product search..."
SEARCH_RESPONSE=$(curl -s "$BASE_URL/api/product/search")
if [[ $SEARCH_RESPONSE == *"content"* ]]; then
echo "✅ Product Search: OK"
else
echo "❌ Product Search: FAILED"
echo "Response: $SEARCH_RESPONSE"
fi
# Test search with parameters
echo "2. Testing parameterized search..."
PARAM_SEARCH=$(curl -s "$BASE_URL/api/product/search?name=Sample&page=0&size=5")
if [[ $PARAM_SEARCH == *"pageable"* ]]; then
echo "✅ Parameterized Search: OK"
else
echo "❌ Parameterized Search: FAILED"
echo "Response: $PARAM_SEARCH"
fi
# Test user products endpoint
echo "3. Testing user products..."
USER_PRODUCTS=$(curl -s "$BASE_URL/api/product?userId=1")
if [[ $USER_PRODUCTS == *"["* ]]; then
echo "✅ User Products: OK"
else
echo "❌ User Products: FAILED"
echo "Response: $USER_PRODUCTS"
fi
# Test error handling
echo "4. Testing error handling..."
ERROR_RESPONSE=$(curl -s "$BASE_URL/api/product?userId=999")
if [[ $ERROR_RESPONSE == *"not found"* ]] || [[ $ERROR_RESPONSE == *"404"* ]]; then
echo "✅ Error Handling: OK"
else
echo "❌ Error Handling: FAILED"
echo "Response: $ERROR_RESPONSE"
fi
echo "=== Product Service Test Complete ==="
```
### 3. Integration Tests (Service Communication)
#### Inter-Service Communication Test
```bash
#!/bin/bash
echo "=== Inter-Service Communication Test ==="
# This tests that Product Service can communicate with Account Service
echo "Testing Feign client communication..."
# Get products for user 1 (should trigger Account Service call)
RESPONSE=$(curl -s "http://localhost:8089/api/product?userId=1")
if [[ $RESPONSE == *"["* ]]; then
echo "✅ Feign Client Communication: OK"
echo "Product Service successfully called Account Service"
else
echo "❌ Feign Client Communication: FAILED"
echo "Response: $RESPONSE"
fi
# Test with non-existent user (should return 404 from Account Service)
ERROR_RESPONSE=$(curl -s "http://localhost:8089/api/product?userId=999")
if [[ $ERROR_RESPONSE == *"404"* ]] || [[ $ERROR_RESPONSE == *"not found"* ]]; then
echo "✅ Error Propagation: OK"
echo "Account Service error properly propagated through Product Service"
else
echo "❌ Error Propagation: FAILED"
echo "Response: $ERROR_RESPONSE"
fi
echo "=== Inter-Service Communication Test Complete ==="
```
#### Kafka Integration Test
```bash
#!/bin/bash
echo "=== Kafka Integration Test ==="
# First, get admin token
ADMIN_LOGIN=$(curl -s -X POST http://localhost:8088/api/auth/signin \
-H "Content-Type: application/json" \
-d '{"username": "vito", "password": "123456"}')
ADMIN_TOKEN=$(echo $ADMIN_LOGIN | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
if [ -z "$ADMIN_TOKEN" ]; then
echo "❌ Could not get admin token for Kafka test"
exit 1
fi
# Create a product (should trigger Kafka event)
echo "Creating product to trigger Kafka event..."
PRODUCT_RESPONSE=$(curl -s -X POST http://localhost:8089/api/product \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{
"name": "Kafka Test Product",
"description": "Testing Kafka integration",
"price": 99.99
}')
if [[ $PRODUCT_RESPONSE == *"Kafka Test Product"* ]]; then
echo "✅ Product Creation: OK"
echo "Kafka event should be published"
# Check Kafdrop for the event (manual verification)
echo "📋 Manual Check: Visit http://localhost:8085 to verify Kafka message"
echo " - Look for 'product-events' topic"
echo " - Check for recent message with product name 'Kafka Test Product'"
else
echo "❌ Product Creation: FAILED"
echo "Response: $PRODUCT_RESPONSE"
fi
echo "=== Kafka Integration Test Complete ==="
```
### 4. Load Tests (Performance Testing)
#### Simple Load Test
```bash
#!/bin/bash
echo "=== Simple Load Test ==="
# Test concurrent requests to public endpoint
echo "Testing 10 concurrent requests..."
for i in {1..10}; do
curl -s "http://localhost:8089/api/product/search?page=0&size=5" > /dev/null &
done
wait
echo "✅ Load Test Complete"
# Test response time
echo "Testing response time..."
START_TIME=$(date +%s%N)
curl -s "http://localhost:8089/api/product/search" > /dev/null
END_TIME=$(date +%s%N)
RESPONSE_TIME=$(( (END_TIME - START_TIME) / 1000000 ))
echo "Response time: ${RESPONSE_TIME}ms"
if [ $RESPONSE_TIME -lt 1000 ]; then
echo "✅ Response Time: OK (< 1000ms)"
else
echo "⚠️ Response Time: SLOW (> 1000ms)"
fi
echo "=== Load Test Complete ==="
```
## 🔍 Advanced Testing
### Database Testing
```bash
#!/bin/bash
echo "=== Database Testing ==="
# Test Account Service database
echo "Testing Account Service database..."
ACCOUNT_DB_TEST=$(docker exec account-db psql -U postgres -d account-service -c "SELECT COUNT(*) FROM users;" 2>/dev/null)
if [[ $ACCOUNT_DB_TEST == *"count"* ]]; then
echo "✅ Account Database: OK"
USER_COUNT=$(echo "$ACCOUNT_DB_TEST" | grep -o '[0-9]\+' | tail -1)
echo " Users in database: $USER_COUNT"
else
echo "❌ Account Database: FAILED"
fi
# Test Product Service database
echo "Testing Product Service database..."
PRODUCT_DB_TEST=$(docker exec product-db psql -U postgres -d product-service -c "SELECT COUNT(*) FROM product;" 2>/dev/null)
if [[ $PRODUCT_DB_TEST == *"count"* ]]; then
echo "✅ Product Database: OK"
PRODUCT_COUNT=$(echo "$PRODUCT_DB_TEST" | grep -o '[0-9]\+' | tail -1)
echo " Products in database: $PRODUCT_COUNT"
else
echo "❌ Product Database: FAILED"
fi
echo "=== Database Testing Complete ==="
```
### Logging System Test
```bash
#!/bin/bash
echo "=== Logging System Test ==="
# Test Elasticsearch
echo "Testing Elasticsearch..."
ES_RESPONSE=$(curl -s -u elastic:elastic http://localhost:9200/_cluster/health)
if [[ $ES_RESPONSE == *"green"* ]] || [[ $ES_RESPONSE == *"yellow"* ]]; then
echo "✅ Elasticsearch: OK"
STATUS=$(echo "$ES_RESPONSE" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
echo " Cluster status: $STATUS"
else
echo "❌ Elasticsearch: FAILED"
echo "Response: $ES_RESPONSE"
fi
# Test Kibana
echo "Testing Kibana..."
KIBANA_RESPONSE=$(curl -s http://localhost:5601/api/status)
if [[ $KIBANA_RESPONSE == *"available"* ]] || [[ $KIBANA_RESPONSE == *"green"* ]]; then
echo "✅ Kibana: OK"
else
echo "⚠️ Kibana: Check manually at http://localhost:5601"
fi
# Check for log entries
echo "Checking for log entries..."
LOG_COUNT=$(curl -s -u elastic:elastic "http://localhost:9200/fluentd-*/_count" 2>/dev/null | grep -o '"count":[0-9]*' | cut -d':' -f2)
if [ "$LOG_COUNT" -gt 0 ] 2>/dev/null; then
echo "✅ Log Entries Found: $LOG_COUNT"
else
echo "⚠️ No log entries found (may need time to populate)"
fi
echo "=== Logging System Test Complete ==="
```
## 🎯 Test Scenarios
### Scenario 1: New User Journey
```bash
#!/bin/bash
echo "=== New User Journey Test ==="
TIMESTAMP=$(date +%s)
USERNAME="journey_user_$TIMESTAMP"
EMAIL="journey_$TIMESTAMP@example.com"
# 1. Register
echo "1. User registers..."
REGISTER=$(curl -s -X POST http://localhost:8088/api/auth/signup \
-H "Content-Type: application/json" \
-d "{\"username\": \"$USERNAME\", \"email\": \"$EMAIL\", \"password\": \"password123\"}")
if [[ $REGISTER == *"successfully"* ]]; then
echo "✅ Registration: OK"
else
echo "❌ Registration: FAILED"
echo "Response: $REGISTER"
exit 1
fi
# 2. Login
echo "2. User logs in..."
LOGIN=$(curl -s -X POST http://localhost:8088/api/auth/signin \
-H "Content-Type: application/json" \
-d "{\"username\": \"$USERNAME\", \"password\": \"password123\"}")
TOKEN=$(echo $LOGIN | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
if [ ! -z "$TOKEN" ]; then
echo "✅ Login: OK"
else
echo "❌ Login: FAILED"
exit 1
fi
# 3. View profile
echo "3. User views profile..."
PROFILE=$(curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8088/api/user/me)
if [[ $PROFILE == *"$USERNAME"* ]]; then
echo "✅ Profile Access: OK"
else
echo "❌ Profile Access: FAILED"
fi
# 4. Browse products
echo "4. User browses products..."
PRODUCTS=$(curl -s "http://localhost:8089/api/product/search?page=0&size=5")
if [[ $PRODUCTS == *"content"* ]]; then
echo "✅ Product Browsing: OK"
else
echo "❌ Product Browsing: FAILED"
fi
# 5. Search products
echo "5. User searches products..."
SEARCH=$(curl -s "http://localhost:8089/api/product/search?name=Sample")
if [[ $SEARCH == *"content"* ]]; then
echo "✅ Product Search: OK"
else
echo "❌ Product Search: FAILED"
fi
echo "✅ New User Journey: Complete"
```
### Scenario 2: Admin Operations
```bash
#!/bin/bash
echo "=== Admin Operations Test ==="
# Login as admin (using default admin user)
ADMIN_LOGIN=$(curl -s -X POST http://localhost:8088/api/auth/signin \
-H "Content-Type: application/json" \
-d '{"username": "vito", "password": "123456"}')
ADMIN_TOKEN=$(echo $ADMIN_LOGIN | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
if [ -z "$ADMIN_TOKEN" ]; then
echo "❌ Admin login failed"
exit 1
fi
# 1. Create product
echo "1. Admin creates product..."
CREATE_PRODUCT=$(curl -s -X POST http://localhost:8089/api/product \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{
"name": "Admin Test Product",
"description": "Created by admin",
"price": 199.99
}')
PRODUCT_ID=$(echo $CREATE_PRODUCT | grep -o '"id":[0-9]*' | cut -d':' -f2)
if [ ! -z "$PRODUCT_ID" ]; then
echo "✅ Product Creation: OK (ID: $PRODUCT_ID)"
else
echo "❌ Product Creation: FAILED"
echo "Response: $CREATE_PRODUCT"
fi
# 2. Update product
if [ ! -z "$PRODUCT_ID" ]; then
echo "2. Admin updates product..."
UPDATE_PRODUCT=$(curl -s -X PATCH http://localhost:8089/api/product/$PRODUCT_ID \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{"price": 299.99}')
if [[ $UPDATE_PRODUCT == *"299.99"* ]]; then
echo "✅ Product Update: OK"
else
echo "❌ Product Update: FAILED"
fi
fi
# 3. Access admin endpoint
echo "3. Admin accesses admin endpoint..."
ADMIN_ACCESS=$(curl -s -H "Authorization: Bearer $ADMIN_TOKEN" http://localhost:8088/api/test/admin)
if [[ $ADMIN_ACCESS == *"Admin Board"* ]]; then
echo "✅ Admin Access: OK"
else
echo "❌ Admin Access: FAILED"
echo "Response: $ADMIN_ACCESS"
fi
echo "✅ Admin Operations: Complete"
```
## 📊 Test Reports
### Generate Test Report
```bash
#!/bin/bash
echo "=== Microservices Test Report ===" > test_report.txt
echo "Generated: $(date)" >> test_report.txt
echo "" >> test_report.txt
# Service Status
echo "## Service Status" >> test_report.txt
docker compose ps >> test_report.txt
echo "" >> test_report.txt
# Health Checks
echo "## Health Checks" >> test_report.txt
echo "Account Service: $(curl -s http://localhost:8088/actuator/health | grep -o '"status":"[^"]*"')" >> test_report.txt
echo "Product Service: $(curl -s http://localhost:8089/actuator/health | grep -o '"status":"[^"]*"')" >> test_report.txt
echo "" >> test_report.txt
# API Tests
echo "## API Test Results" >> test_report.txt
echo "Public Endpoint: $(curl -s http://localhost:8088/api/test/all)" >> test_report.txt
echo "Product Search: $(curl -s http://localhost:8089/api/product/search | wc -c) bytes" >> test_report.txt
echo "" >> test_report.txt
# Infrastructure
echo "## Infrastructure Status" >> test_report.txt
echo "Elasticsearch: $(curl -s -u elastic:elastic http://localhost:9200/_cluster/health | grep -o '"status":"[^"]*"')" >> test_report.txt
echo "Kafka Topics: $(curl -s http://localhost:8085/topic 2>/dev/null | grep -c 'topic' || echo 'N/A')" >> test_report.txt
echo "Test report generated: test_report.txt"
```
## 🛠️ Testing Tools
### Postman Collection
Use the provided Postman collection for GUI testing:
1. **Import collection:** `spring microservices.postman_collection.json`
2. **Set environment variables:**
- `account_base_url`: http://localhost:8088
- `product_base_url`: http://localhost:8089
3. **Run collection** with automated tests
### Swagger UI Testing
Interactive API testing:
- **Account Service:** http://localhost:8088/swagger-ui/index.html
- **Product Service:** http://localhost:8089/swagger-ui/index.html
### curl Scripts
All test scripts use curl for maximum compatibility:
```bash
# Make all scripts executable
chmod +x test-*.sh
# Run specific test
./test-smoke.sh
./test-api.sh
./test-integration.sh
```
## 🚨 Troubleshooting Tests
### Common Test Failures
#### Services Not Ready
```bash
# Wait for services
echo "Waiting for services to be ready..."
sleep 30
# Check status
docker compose ps
# Check health
curl http://localhost:8088/actuator/health
curl http://localhost:8089/actuator/health
```
#### Connection Refused
```bash
# Check if ports are accessible
netstat -an | findstr :8088
netstat -an | findstr :8089
# Restart services if needed
docker compose restart account-service-app product-service-app
```
#### Authentication Failures
```bash
# Check if default users exist
docker exec account-db psql -U postgres -d account-service -c "SELECT username FROM users;"
# Reset test data if needed
docker compose down -v
mvn clean install -DskipTests
docker compose up -d --build
```
#### Database Connection Issues
```bash
# Check database connectivity
docker exec account-db pg_isready -U postgres
docker exec product-db pg_isready -U postgres
# Check database logs
docker compose logs account-service-db product-service-db
```
### Test Environment Reset
```bash
#!/bin/bash
echo "Resetting test environment..."
# Stop everything
docker compose down -v
# Clean up
docker system prune -f
# Rebuild and start
mvn clean install -DskipTests
docker compose up -d --build
# Wait for readiness
sleep 60
echo "Test environment reset complete"
```
## 📈 Continuous Testing
### Automated Test Pipeline
```bash
#!/bin/bash
# ci-test.sh - Continuous Integration Test Script
set -e # Exit on any error
echo "=== CI Test Pipeline ==="
# 1. Build
echo "Building application..."
mvn clean install -DskipTests
# 2. Start services
echo "Starting services..."
docker compose up -d --build
# 3. Wait for readiness
echo "Waiting for services..."
sleep 60
# 4. Run tests
echo "Running smoke tests..."
./test-smoke.sh
echo "Running API tests..."
./test-api.sh
echo "Running integration tests..."
./test-integration.sh
# 5. Generate report
echo "Generating test report..."
./generate-test-report.sh
# 6. Cleanup
echo "Cleaning up..."
docker compose down -v
echo "=== CI Test Pipeline Complete ==="
```
### Test Monitoring
```bash
# Monitor test results
watch -n 30 './test-smoke.sh'
# Continuous health monitoring
while true; do
curl -f http://localhost:8088/actuator/health || echo "Account service down"
curl -f http://localhost:8089/actuator/health || echo "Product service down"
sleep 30
done
```
---
**Happy Testing! 🧪**
Next: [06-Debugging-Guide.md](06-Debugging-Guide.md) for debugging in containers
## /fluentd/Dockerfile
``` path="/fluentd/Dockerfile"
FROM fluent/fluentd:v1.16.3-1.0
USER root
RUN gem uninstall -I elasticsearch || true \
&& gem install elasticsearch -v 7.17.0 \
&& gem install fluent-plugin-elasticsearch --no-document --version 5.0.3
```
## /fluentd/conf/fluent.conf
```conf path="/fluentd/conf/fluent.conf"
# bind fluentd on IP 0.0.0.0
# port 24224
<source>
@type forward
port 24224
bind 0.0.0.0
</source>
# parse JSON payload from 'log' field and merge into record
# but skip parsing for error logs to avoid mapping conflicts
<filter **>
@type parser
key_name log
reserve_data true
<parse>
@type json
</parse>
</filter>
# Handle error logs separately to avoid mapping conflicts
<filter fluent.error>
@type record_transformer
<record>
error_message ${record["message"]}
</record>
</filter>
# sendlog to the elasticsearch
# the host must match to the elasticsearch
# container service
<match *.**>
@type copy
<store>
@type elasticsearch_dynamic
hosts elasticsearch:9200
user elastic
password elastic
logstash_format true
logstash_prefix fluentd
logstash_dateformat %Y%m%d
include_tag_key true
tag_key @log_name
include_timestamp true
flush_interval 30s
</store>
<store>
@type stdout
</store>
</match>
```
## /product-service/.gitignore
```gitignore path="/product-service/.gitignore"
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
```
## /product-service/.mvn/wrapper/maven-wrapper.jar
Binary file available at https://raw.githubusercontent.com/cuongnh28/spring-microservices-blueprint/refs/heads/main/product-service/.mvn/wrapper/maven-wrapper.jar
The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.