diff --git a/pom.xml b/pom.xml
index 8b1feca..368e4be 100644
--- a/pom.xml
+++ b/pom.xml
@@ -91,6 +91,22 @@
org.flywaydb
flyway-database-postgresql
+
+ com.bucket4j
+ bucket4j-core
+ 8.6.0
+
+
+
+ com.bucket4j
+ bucket4j-redis
+ 8.6.0
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
diff --git a/src/main/java/com/vimaltech/contactapi/security/RedisRateLimitFilter.java b/src/main/java/com/vimaltech/contactapi/security/RedisRateLimitFilter.java
new file mode 100644
index 0000000..3d15d9e
--- /dev/null
+++ b/src/main/java/com/vimaltech/contactapi/security/RedisRateLimitFilter.java
@@ -0,0 +1,85 @@
+package com.vimaltech.contactapi.security;
+
+import io.github.bucket4j.Bandwidth;
+import io.github.bucket4j.Bucket;
+import io.github.bucket4j.BucketConfiguration;
+import io.github.bucket4j.distributed.proxy.ProxyManager;
+import io.github.bucket4j.redis.lettuce.cas.LettuceBasedProxyManager;
+
+import io.lettuce.core.RedisClient;
+import io.lettuce.core.RedisURI;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.Optional;
+
+@Component
+@Profile("prod")
+public class RedisRateLimitFilter extends OncePerRequestFilter {
+
+ private final ProxyManager proxyManager;
+
+ public RedisRateLimitFilter(
+ @Value("${spring.data.redis.host}") String host,
+ @Value("${spring.data.redis.port}") int port
+ ) {
+
+ RedisURI redisURI = RedisURI.Builder.redis(host)
+ .withPort(port)
+ .build();
+
+ RedisClient redisClient = RedisClient.create(redisURI);
+
+ this.proxyManager = LettuceBasedProxyManager
+ .builderFor(redisClient)
+ .build();
+ }
+
+ private BucketConfiguration configuration() {
+ return BucketConfiguration.builder()
+ .addLimit(Bandwidth.simple(10, Duration.ofMinutes(1)))
+ .build();
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain)
+ throws ServletException, IOException {
+
+ if (!request.getRequestURI().equals("/api/v1/contact")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ String ip = Optional.ofNullable(request.getHeader("X-Forwarded-For"))
+ .map(s -> s.split(",")[0].trim())
+ .orElse(request.getRemoteAddr());
+
+ Bucket bucket = proxyManager.builder()
+ .build(("ip:" + ip).getBytes(), this::configuration);
+
+ if (bucket.tryConsume(1)) {
+ filterChain.doFilter(request, response);
+ } else {
+ response.setStatus(429);
+ response.setContentType("application/json");
+ response.getWriter().write("""
+ {
+ "success": false,
+ "message": "Too many requests. Please try again later."
+ }
+ """);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/ApiResponse.java b/src/main/resources/ApiResponse.java
deleted file mode 100644
index 1ae0b7b..0000000
--- a/src/main/resources/ApiResponse.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.vimaltech.contactapi.dto;
-
-import java.time.LocalDateTime;
-
-public record ApiResponse(
- boolean success,
- String message,
- LocalDateTime timestamp
-) {
-}
\ No newline at end of file
diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml
index 40cc8b3..a6335c0 100644
--- a/src/main/resources/application-prod.yml
+++ b/src/main/resources/application-prod.yml
@@ -5,6 +5,12 @@ spring:
password: ${DB_PASSWORD:?DB_PASSWORD is required}
driver-class-name: org.postgresql.Driver
+ data:
+ redis:
+ host: redis
+ port: 6379
+ timeout: 2000
+
jpa:
hibernate:
ddl-auto: validate