diff --git a/README.md b/README.md
index 4bfe102..602200c 100644
--- a/README.md
+++ b/README.md
@@ -155,6 +155,33 @@ src/
└── java/ # Test classes
```
+## Docker Support
+
+## Kubernetes/Minikube Deployment
+
+### Prerequisites
+- Minikube
+- kubectl
+- Docker
+
+### Deploy to Minikube
+```bash
+# Create k8s directory and copy configuration files
+mkdir k8s
+chmod +x k8s/deploy.sh
+
+# Deploy to Minikube
+./k8s/deploy.sh
+
+# Get application URL
+minikube service spring-app --url
+```
+
+### Cleanup
+```bash
+kubectl delete -f k8s/
+```
+
## Contributing
1. Fork the repository
diff --git a/build.gradle b/build.gradle
index 4f72d9b..5d81320 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,6 +2,7 @@ plugins {
id 'org.springframework.boot' version '3.1.4'
id 'io.spring.dependency-management' version '1.1.3'
id 'java'
+ id 'org.flywaydb.flyway' version '9.22.3'
}
group = 'com.boilerplate'
@@ -38,15 +39,30 @@ dependencies {
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'jakarta.validation:jakarta.validation-api:3.0.0'
- implementation 'org.hibernate.validator:hibernate-validator:6.2.0.Final'
- implementation 'org.glassfish:javax.el:3.0.0'
- implementation 'org.springdoc:springdoc-openapi-ui:1.6.14'
+ implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final'
+ implementation 'org.glassfish:jakarta.el:4.0.2'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
- implementation 'io.swagger.core.v3:swagger-annotations:2.1.2'
- implementation 'org.flywaydb:flyway-core:9.16.0'
+ implementation 'org.flywaydb:flyway-database-postgresql'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
compileOnly 'org.projectlombok:lombok:1.18.28'
annotationProcessor 'org.projectlombok:lombok:1.18.28'
+}
+
+flyway {
+ url = 'jdbc:postgresql://localhost:5432/boilerplate'
+ user = 'postgres'
+ password = 'postgres'
+}
+
+task createMigration {
+ doLast {
+ def migrationName = project.hasProperty('migrationName') ? project.getProperty('migrationName') : 'migration'
+ def timestamp = new Date().format('yyyyMMddHHmmss')
+ def fileName = "V${timestamp}__${migrationName}.sql"
+ def migrationDir = new File("${projectDir}/src/main/resources/db/migration")
+ migrationDir.mkdirs()
+ new File(migrationDir, fileName).createNewFile()
+ }
}
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 0fd8a9d..5df5abe 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,6 +2,7 @@ version: '3.8'
services:
app:
+ container_name: spring-boilerplate
platform: linux/arm64
build:
context: .
@@ -21,6 +22,7 @@ services:
SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:9092
postgres:
+ container_name: pg_master
image: postgres:16.2
ports:
- "5432:5432"
@@ -32,12 +34,15 @@ services:
- postgres_data:/var/lib/postgresql/data
redis:
+ container_name: redis_master
image: redis:7.0.11
+ command: redis-server --requirepass redis123
ports:
- "6379:6379"
zookeeper:
- image: bitnami/zookeeper:3.8.0
+ container_name: zookeeper_master
+ image: bitnami/zookeeper:3.8.1
platform: linux/arm64
environment:
ZOOKEEPER_CLIENT_PORT: 2181
@@ -45,12 +50,19 @@ services:
ALLOW_ANONYMOUS_LOGIN: "yes"
ports:
- "22181:2181"
+ healthcheck:
+ test: ["CMD", "nc", "-z", "localhost", "2181"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
kafka:
+ container_name: kafka_master
image: bitnami/kafka:latest
platform: linux/arm64
depends_on:
- - zookeeper
+ zookeeper:
+ condition: service_healthy
ports:
- "29092:29092"
environment:
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index cea7a79..e18bc25 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/k8s/app-deployment.yml b/k8s/app-deployment.yml
new file mode 100644
index 0000000..0909a2a
--- /dev/null
+++ b/k8s/app-deployment.yml
@@ -0,0 +1,45 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: spring-app
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: spring-app
+ template:
+ metadata:
+ labels:
+ app: spring-app
+ spec:
+ containers:
+ - name: spring-app
+ image: spring-boot-boilerplate:latest
+ ports:
+ - containerPort: 8080
+ env:
+ - name: SPRING_DATASOURCE_URL
+ value: jdbc:postgresql://postgres:5432/boilerplate
+ - name: SPRING_REDIS_HOST
+ value: redis
+ - name: SPRING_KAFKA_BOOTSTRAP_SERVERS
+ value: kafka:9092
+ imagePullPolicy: Never
+ resources:
+ requests:
+ memory: "512Mi"
+ cpu: "250m"
+ limits:
+ memory: "1Gi"
+ cpu: "500m"
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: spring-app
+spec:
+ selector:
+ app: spring-app
+ ports:
+ - port: 8080
+ type: LoadBalancer
\ No newline at end of file
diff --git a/k8s/deploy.sh b/k8s/deploy.sh
new file mode 100755
index 0000000..25a0a40
--- /dev/null
+++ b/k8s/deploy.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+# Start Minikube if not running
+minikube status || minikube start
+
+# Build the application Docker image
+eval $(minikube docker-env)
+docker build -t spring-boot-boilerplate:latest .
+
+# Create k8s resources
+kubectl apply -f k8s/storage.yml
+kubectl apply -f k8s/postgres-deployment.yml
+kubectl apply -f k8s/redis-deployment.yml
+kubectl apply -f k8s/kafka-deployment.yml
+kubectl apply -f k8s/app-deployment.yml
+
+# Wait for deployments
+echo "Waiting for deployments to be ready..."
+kubectl wait --for=condition=available --timeout=300s deployment/postgres
+kubectl wait --for=condition=available --timeout=300s deployment/redis
+kubectl wait --for=condition=available --timeout=300s deployment/zookeeper
+kubectl wait --for=condition=available --timeout=300s deployment/kafka
+kubectl wait --for=condition=available --timeout=300s deployment/spring-app
+
+# Get the application URL
+echo "Application URL:"
+minikube service spring-app --url
\ No newline at end of file
diff --git a/k8s/kafka-deployment.yml b/k8s/kafka-deployment.yml
new file mode 100644
index 0000000..13af0d6
--- /dev/null
+++ b/k8s/kafka-deployment.yml
@@ -0,0 +1,75 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: zookeeper
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: zookeeper
+ template:
+ metadata:
+ labels:
+ app: zookeeper
+ spec:
+ containers:
+ - name: zookeeper
+ image: bitnami/zookeeper:3.8.0
+ ports:
+ - containerPort: 2181
+ env:
+ - name: ALLOW_ANONYMOUS_LOGIN
+ value: "yes"
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: zookeeper
+spec:
+ selector:
+ app: zookeeper
+ ports:
+ - port: 2181
+ type: ClusterIP
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: kafka
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: kafka
+ template:
+ metadata:
+ labels:
+ app: kafka
+ spec:
+ containers:
+ - name: kafka
+ image: bitnami/kafka:latest
+ ports:
+ - containerPort: 9092
+ env:
+ - name: KAFKA_BROKER_ID
+ value: "1"
+ - name: KAFKA_ZOOKEEPER_CONNECT
+ value: "zookeeper:2181"
+ - name: KAFKA_LISTENERS
+ value: "PLAINTEXT://:9092"
+ - name: KAFKA_ADVERTISED_LISTENERS
+ value: "PLAINTEXT://kafka:9092"
+ - name: KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR
+ value: "1"
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: kafka
+spec:
+ selector:
+ app: kafka
+ ports:
+ - port: 9092
+ type: ClusterIP
\ No newline at end of file
diff --git a/k8s/postgres-deployment.yml b/k8s/postgres-deployment.yml
new file mode 100644
index 0000000..da2ce1b
--- /dev/null
+++ b/k8s/postgres-deployment.yml
@@ -0,0 +1,44 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: postgres
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: postgres
+ template:
+ metadata:
+ labels:
+ app: postgres
+ spec:
+ containers:
+ - name: postgres
+ image: postgres:16.2
+ ports:
+ - containerPort: 5432
+ env:
+ - name: POSTGRES_DB
+ value: boilerplate
+ - name: POSTGRES_USER
+ value: postgres
+ - name: POSTGRES_PASSWORD
+ value: postgres
+ volumeMounts:
+ - name: postgres-storage
+ mountPath: /var/lib/postgresql/data
+ volumes:
+ - name: postgres-storage
+ persistentVolumeClaim:
+ claimName: postgres-pvc
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: postgres
+spec:
+ selector:
+ app: postgres
+ ports:
+ - port: 5432
+ type: ClusterIP
\ No newline at end of file
diff --git a/k8s/redis-deployment.yml b/k8s/redis-deployment.yml
new file mode 100644
index 0000000..c372d12
--- /dev/null
+++ b/k8s/redis-deployment.yml
@@ -0,0 +1,31 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: redis
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: redis
+ template:
+ metadata:
+ labels:
+ app: redis
+ spec:
+ containers:
+ - name: redis
+ image: redis:7.0.11
+ ports:
+ - containerPort: 6379
+ args: ["--requirepass", "redis123"]
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: redis
+spec:
+ selector:
+ app: redis
+ ports:
+ - port: 6379
+ type: ClusterIP
\ No newline at end of file
diff --git a/k8s/storage.yml b/k8s/storage.yml
new file mode 100644
index 0000000..02c345b
--- /dev/null
+++ b/k8s/storage.yml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: postgres-pvc
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 1Gi
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..4f0e212
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,5 @@
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.1.0
+
\ No newline at end of file
diff --git a/src/main/java/com/boilerplate/SpringBootBoilerplateApplication.java b/src/main/java/com/boilerplate/SpringBootBoilerplateApplication.java
index 5aa7612..d14db21 100644
--- a/src/main/java/com/boilerplate/SpringBootBoilerplateApplication.java
+++ b/src/main/java/com/boilerplate/SpringBootBoilerplateApplication.java
@@ -1,15 +1,27 @@
package com.boilerplate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.boot.context.event.ApplicationStartingEvent;
+import org.springframework.context.event.EventListener;
@SpringBootApplication
@EntityScan("com.boilerplate.entity")
@EnableJpaRepositories("com.boilerplate.repository")
public class SpringBootBoilerplateApplication {
+ private static final Logger logger = LoggerFactory.getLogger(SpringBootBoilerplateApplication.class);
+
public static void main(String[] args) {
+ logger.info("🚀 Starting Spring Boot Boilerplate Application...");
SpringApplication.run(SpringBootBoilerplateApplication.class, args);
}
+
+ @EventListener(ApplicationStartingEvent.class)
+ public void onStart() {
+ logger.info("⚙️ Initializing application components...");
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/boilerplate/config/ApplicationStartupListener.java b/src/main/java/com/boilerplate/config/ApplicationStartupListener.java
new file mode 100644
index 0000000..4e731df
--- /dev/null
+++ b/src/main/java/com/boilerplate/config/ApplicationStartupListener.java
@@ -0,0 +1,70 @@
+package com.boilerplate.config;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.context.event.ApplicationStartedEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.stereotype.Component;
+import javax.sql.DataSource;
+import org.springframework.core.env.Environment;
+import redis.clients.jedis.Jedis;
+
+@Component
+public class ApplicationStartupListener implements ApplicationListener {
+ private static final Logger logger = LoggerFactory.getLogger(ApplicationStartupListener.class);
+
+ private final DataSource dataSource;
+ private final KafkaTemplate kafkaTemplate;
+ private final Environment environment;
+
+ public ApplicationStartupListener(DataSource dataSource,
+ KafkaTemplate kafkaTemplate,
+ Environment environment) {
+ this.dataSource = dataSource;
+ this.kafkaTemplate = kafkaTemplate;
+ this.environment = environment;
+ }
+
+ @Override
+ public void onApplicationEvent(ApplicationStartedEvent event) {
+ checkDatabaseConnection();
+ checkRedisConnection();
+ checkKafkaConnection();
+ logger.info("All components initialized successfully");
+ }
+
+ private void checkDatabaseConnection() {
+ try {
+ dataSource.getConnection();
+ logger.info("✅ Database connection established successfully");
+ } catch (Exception e) {
+ logger.error("❌ Failed to connect to database: {}", e.getMessage());
+ }
+ }
+
+ private void checkRedisConnection() {
+ try {
+ Jedis jedis = new Jedis(
+ environment.getProperty("spring.redis.host", "localhost"),
+ environment.getProperty("spring.redis.port", Integer.class, 6379)
+ );
+ jedis.auth(environment.getProperty("spring.redis.password"));
+
+ String response = jedis.ping();
+ jedis.close();
+ logger.info("✅ Redis connection established successfully: {}", response);
+ } catch (Exception e) {
+ logger.error("❌ Failed to connect to Redis: {}", e.getMessage());
+ }
+ }
+
+ private void checkKafkaConnection() {
+ try {
+ kafkaTemplate.getDefaultTopic();
+ logger.info("✅ Kafka connection initialized successfully");
+ } catch (Exception e) {
+ logger.error("❌ Failed to initialize Kafka: {}", e.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/boilerplate/config/RedisConfig.java b/src/main/java/com/boilerplate/config/RedisConfig.java
index 0b9cf80..9a5c362 100644
--- a/src/main/java/com/boilerplate/config/RedisConfig.java
+++ b/src/main/java/com/boilerplate/config/RedisConfig.java
@@ -15,12 +15,15 @@ public class RedisConfig {
@Value("${spring.redis.port}")
private int redisPort;
+ @Value("${spring.redis.password}")
+ private String redisPassword;
+
@Bean
public JedisPool jedisPool() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(128);
poolConfig.setMaxIdle(128);
poolConfig.setMinIdle(16);
- return new JedisPool(poolConfig, redisHost, redisPort);
+ return new JedisPool(poolConfig, redisHost, redisPort, 2000, redisPassword);
}
}
\ No newline at end of file
diff --git a/src/main/java/com/boilerplate/config/SecurityConfig.java b/src/main/java/com/boilerplate/config/SecurityConfig.java
index c2e10a1..e4594e6 100644
--- a/src/main/java/com/boilerplate/config/SecurityConfig.java
+++ b/src/main/java/com/boilerplate/config/SecurityConfig.java
@@ -26,13 +26,15 @@ public class SecurityConfig {
};
@Bean
- public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
- .requestMatchers(NO_AUTH_WHITELIST).permitAll()
+ .requestMatchers("/v3/api-docs/**",
+ "/swagger-ui/**",
+ "/swagger-ui.html").permitAll()
+ .requestMatchers(NO_AUTH_WHITELIST).permitAll()
.requestMatchers("/api/v1/auth/**").permitAll()
- .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
diff --git a/src/main/java/com/boilerplate/config/SwaggerConfig.java b/src/main/java/com/boilerplate/config/SwaggerConfig.java
deleted file mode 100644
index d55a243..0000000
--- a/src/main/java/com/boilerplate/config/SwaggerConfig.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.boilerplate.config;
-
-import io.swagger.v3.oas.annotations.OpenAPIDefinition;
-import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
-import io.swagger.v3.oas.annotations.info.Info;
-import io.swagger.v3.oas.annotations.security.SecurityScheme;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-@OpenAPIDefinition(
- info = @Info(
- title = "Spring Boot Boilerplate API",
- version = "1.0",
- description = "Spring Boot Boilerplate API Documentation"
- )
-)
-@SecurityScheme(
- name = "bearerAuth",
- type = SecuritySchemeType.HTTP,
- bearerFormat = "JWT",
- scheme = "bearer"
-)
-public class SwaggerConfig {
-}
\ No newline at end of file
diff --git a/src/main/java/com/boilerplate/controller/AuthController.java b/src/main/java/com/boilerplate/controller/AuthController.java
index 376a644..920c1c8 100644
--- a/src/main/java/com/boilerplate/controller/AuthController.java
+++ b/src/main/java/com/boilerplate/controller/AuthController.java
@@ -4,8 +4,6 @@
import com.boilerplate.dto.LoginRequestDto;
import com.boilerplate.dto.UserRegistrationDto;
import com.boilerplate.service.AuthService;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
@@ -14,13 +12,11 @@
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/auth")
-@Tag(name = "Authentication", description = "Authentication management APIs")
public class AuthController {
private final AuthService authService;
@PostMapping("/register")
- @Operation(summary = "Register a new user")
public ResponseEntity register(
@Valid @RequestBody UserRegistrationDto request
) {
@@ -28,7 +24,6 @@ public ResponseEntity register(
}
@PostMapping("/login")
- @Operation(summary = "Login with email and password")
public ResponseEntity login(
@Valid @RequestBody LoginRequestDto request
) {
@@ -36,7 +31,6 @@ public ResponseEntity login(
}
@PostMapping("/verify-otp")
- @Operation(summary = "Verify OTP for account activation")
public ResponseEntity verifyOtp(
@RequestParam String email,
@RequestParam String otp
@@ -45,7 +39,6 @@ public ResponseEntity verifyOtp(
}
@PostMapping("/refresh-token")
- @Operation(summary = "Refresh authentication token")
public ResponseEntity refreshToken(
@RequestHeader("Authorization") String token
) {
@@ -53,7 +46,6 @@ public ResponseEntity refreshToken(
}
@PostMapping("/logout")
- @Operation(summary = "Logout and invalidate token")
public ResponseEntity logout(
@RequestHeader("Authorization") String token
) {
diff --git a/src/main/java/com/boilerplate/controller/HealthCheckController.java b/src/main/java/com/boilerplate/controller/HealthCheckController.java
index f584959..eac456e 100644
--- a/src/main/java/com/boilerplate/controller/HealthCheckController.java
+++ b/src/main/java/com/boilerplate/controller/HealthCheckController.java
@@ -3,13 +3,79 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.core.env.Environment;
+import javax.sql.DataSource;
+import redis.clients.jedis.Jedis;
+import lombok.RequiredArgsConstructor;
+import java.util.HashMap;
+import java.util.Map;
@RestController
+@RequiredArgsConstructor
public class HealthCheckController {
+ private final DataSource dataSource;
+ private final KafkaTemplate kafkaTemplate;
+ private final Environment environment;
+
@GetMapping("/health")
- public ResponseEntity health() {
- return ResponseEntity.ok("Healthy");
+ public ResponseEntity