limiters = new ConcurrentHashMap<>();
+ private final ScheduledExecutorService healthChecker = Executors.newScheduledThreadPool(1);
+
+ public AdaptiveRateLimiter(int initialLimit, int maxLimit) {
+ this.initialLimit = initialLimit;
+ this.maxLimit = maxLimit;
+ this.currentLimit = new AtomicInteger(initialLimit);
+ // Periodically increase limit to recover if system appears healthy
+ healthChecker.scheduleAtFixedRate(this::adjustLimits, 10, 10, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void check(String serviceName, String operationName) throws RateLimitException {
+ String key = serviceName + ":" + operationName;
+ int current = currentLimit.get();
+
+ // Reuse or create TokenBucket for this key using currentLimit
+ RateLimiter limiter =
+ limiters.computeIfAbsent(key, k -> new TokenBucketRateLimiter(current, current));
+
+ try {
+ limiter.check(serviceName, operationName);
+ System.out.printf(
+ "[Adaptive] Allowed %s.%s - CurrentLimit: %d%n", serviceName, operationName, current);
+ } catch (RateLimitException e) {
+ // On throttling, reduce system limit to reduce load
+ currentLimit.updateAndGet(curr -> Math.max(initialLimit, curr / 2));
+ System.out.printf(
+ "[Adaptive] Throttled %s.%s - Decreasing limit to %d%n",
+ serviceName, operationName, currentLimit.get());
+ throw e;
+ }
+ }
+
+ // Periodic recovery mechanism to raise limits when the system is under control
+ private void adjustLimits() {
+ int updated = currentLimit.updateAndGet(curr -> Math.min(maxLimit, curr + (initialLimit / 2)));
+ System.out.printf("[Adaptive] Health check passed - Increasing limit to %d%n", updated);
+ }
+}
diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/App.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/App.java
new file mode 100644
index 000000000000..e76ed5254345
--- /dev/null
+++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/App.java
@@ -0,0 +1,178 @@
+package com.iluwatar.rate.limiting.pattern;
+
+import java.security.SecureRandom;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The Rate Limiter pattern is a key defensive strategy used to prevent system overload and
+ * ensure fair usage of shared services. This demo showcases how different rate limiting techniques
+ * can regulate traffic in distributed systems.
+ *
+ * Specifically, this simulation implements three rate limiter strategies:
+ *
+ *
+ * - Token Bucket – Allows short bursts followed by steady request rates.
+ *
- Fixed Window – Enforces a strict limit per discrete time window (e.g., 3
+ * requests/sec).
+ *
- Adaptive – Dynamically scales limits based on system health, simulating elastic
+ * backoff.
+ *
+ *
+ * Each simulated service (e.g., S3, DynamoDB, Lambda) is governed by one of these limiters.
+ * Multiple concurrent client threads issue randomized requests to these services over a fixed
+ * duration. Each request is either:
+ *
+ *
+ * - ALLOWED – Permitted under the current rate limit
+ *
- THROTTLED – Rejected due to quota exhaustion
+ *
- FAILED – Dropped due to transient service failure
+ *
+ *
+ * Statistics are printed every few seconds, and the simulation exits gracefully after a fixed
+ * runtime, offering a clear view into how each limiter behaves under pressure.
+ *
+ *
Relation to AWS API Gateway:
+ * This implementation mirrors the throttling behavior described in the
+ * AWS API Gateway Request Throttling documentation, where limits are applied per second and
+ * over longer durations (burst and rate limits). The TokenBucketRateLimiter
mimics
+ * burst capacity, the FixedWindowRateLimiter
models steady rate enforcement, and the
+ * AdaptiveRateLimiter
reflects elasticity in real-world systems like AWS Lambda under
+ * variable load.
+ */
+public final class App {
+ private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
+
+ private static final int RUN_DURATION_SECONDS = 10;
+ private static final int SHUTDOWN_TIMEOUT_SECONDS = 5;
+
+ static final AtomicInteger successfulRequests = new AtomicInteger();
+ static final AtomicInteger throttledRequests = new AtomicInteger();
+ static final AtomicInteger failedRequests = new AtomicInteger();
+ static final AtomicBoolean running = new AtomicBoolean(true);
+ private static final String DIVIDER_LINE = "====================================";
+
+ public static void main(String[] args) {
+ LOGGER.info("Starting Rate Limiter Demo");
+ LOGGER.info(DIVIDER_LINE);
+
+ ExecutorService executor = Executors.newFixedThreadPool(3);
+ ScheduledExecutorService statsPrinter = Executors.newSingleThreadScheduledExecutor();
+
+ try {
+ TokenBucketRateLimiter tb = new TokenBucketRateLimiter(2, 1);
+ FixedWindowRateLimiter fw = new FixedWindowRateLimiter(3, 1);
+ AdaptiveRateLimiter ar = new AdaptiveRateLimiter(2, 6);
+
+ statsPrinter.scheduleAtFixedRate(App::printStats, 2, 2, TimeUnit.SECONDS);
+
+ for (int i = 1; i <= 3; i++) {
+ executor.submit(createClientTask(i, tb, fw, ar));
+ }
+
+ Thread.sleep(RUN_DURATION_SECONDS * 1000L);
+ LOGGER.info("Shutting down the demo...");
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ running.set(false);
+ shutdownExecutor(executor, "mainExecutor");
+ shutdownExecutor(statsPrinter, "statsPrinter");
+ printFinalStats();
+ LOGGER.info("Demo completed.");
+ }
+ }
+
+ private static void shutdownExecutor(ExecutorService service, String name) {
+ service.shutdown();
+ try {
+ if (!service.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+ service.shutdownNow();
+ LOGGER.warn("Forced shutdown of {}", name);
+ }
+ } catch (InterruptedException e) {
+ service.shutdownNow();
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ static Runnable createClientTask(
+ int clientId, RateLimiter s3Limiter, RateLimiter dynamoDbLimiter, RateLimiter lambdaLimiter) {
+
+ return () -> {
+ String[] services = {"s3", "dynamodb", "lambda"};
+ String[] operations = {
+ "GetObject", "PutObject", "Query", "Scan", "PutItem", "Invoke", "ListFunctions"
+ };
+ SecureRandom random = new SecureRandom(); // ✅ Safe & compliant for SonarCloud
+
+ while (running.get() && !Thread.currentThread().isInterrupted()) {
+ try {
+ String service = services[random.nextInt(services.length)];
+ String operation = operations[random.nextInt(operations.length)];
+
+ switch (service) {
+ case "s3" -> makeRequest(clientId, s3Limiter, service, operation);
+ case "dynamodb" -> makeRequest(clientId, dynamoDbLimiter, service, operation);
+ case "lambda" -> makeRequest(clientId, lambdaLimiter, service, operation);
+ default -> LOGGER.warn("Unknown service: {}", service);
+ }
+
+ Thread.sleep(30L + random.nextInt(50));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ };
+ }
+
+ static void makeRequest(int clientId, RateLimiter limiter, String service, String operation) {
+ try {
+ limiter.check(service, operation);
+ successfulRequests.incrementAndGet();
+ LOGGER.info("Client {}: {}.{} - ALLOWED", clientId, service, operation);
+ } catch (ThrottlingException e) {
+ throttledRequests.incrementAndGet();
+ LOGGER.warn(
+ "Client {}: {}.{} - THROTTLED (Retry in {}ms)",
+ clientId,
+ service,
+ operation,
+ e.getRetryAfterMillis());
+ } catch (ServiceUnavailableException e) {
+ failedRequests.incrementAndGet();
+ LOGGER.warn("Client {}: {}.{} - SERVICE UNAVAILABLE", clientId, service, operation);
+ } catch (Exception e) {
+ failedRequests.incrementAndGet();
+ LOGGER.error("Client {}: {}.{} - ERROR: {}", clientId, service, operation, e.getMessage());
+ }
+ }
+
+ static void printStats() {
+ if (!running.get()) return;
+ LOGGER.info("=== Current Statistics ===");
+ LOGGER.info("Successful Requests: {}", successfulRequests.get());
+ LOGGER.info("Throttled Requests : {}", throttledRequests.get());
+ LOGGER.info("Failed Requests : {}", failedRequests.get());
+ LOGGER.info(DIVIDER_LINE);
+ }
+
+ static void printFinalStats() {
+ LOGGER.info("Final Statistics");
+ LOGGER.info(DIVIDER_LINE);
+ LOGGER.info("Successful Requests: {}", successfulRequests.get());
+ LOGGER.info("Throttled Requests : {}", throttledRequests.get());
+ LOGGER.info("Failed Requests : {}", failedRequests.get());
+ LOGGER.info(DIVIDER_LINE);
+ }
+
+ static void resetCountersForTesting() {
+ successfulRequests.set(0);
+ throttledRequests.set(0);
+ failedRequests.set(0);
+ }
+}
diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequest.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequest.java
new file mode 100644
index 000000000000..a61fc35f496a
--- /dev/null
+++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequest.java
@@ -0,0 +1,40 @@
+package com.iluwatar.rate.limiting.pattern;
+
+/**
+ * A rate-limited customer lookup operation. This class wraps the rate limiting logic and represents
+ * an executable business request.
+ */
+public class FindCustomerRequest implements RateLimitOperation {
+ private final String customerId;
+ private final RateLimiter rateLimiter;
+
+ public FindCustomerRequest(String customerId, RateLimiter rateLimiter) {
+ this.customerId = customerId;
+ this.rateLimiter = rateLimiter;
+ }
+
+ @Override
+ public String getServiceName() {
+ return "CustomerService";
+ }
+
+ @Override
+ public String getOperationName() {
+ return "FindCustomer";
+ }
+
+ @Override
+ public String execute() throws RateLimitException {
+ // Ensure the operation respects the assigned rate limiter
+ rateLimiter.check(getServiceName(), getOperationName());
+
+ // Simulate actual operation
+ try {
+ Thread.sleep(50); // Simulate processing time
+ return "Customer-" + customerId;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new ServiceUnavailableException(getServiceName(), 1000);
+ }
+ }
+}
diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/FixedWindowRateLimiter.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/FixedWindowRateLimiter.java
new file mode 100644
index 000000000000..3a2b52888ca7
--- /dev/null
+++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/FixedWindowRateLimiter.java
@@ -0,0 +1,53 @@
+package com.iluwatar.rate.limiting.pattern;
+
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Implements a fixed window rate limiter. It allows up to 'limit' number of requests within a time
+ * window of fixed size.
+ */
+public class FixedWindowRateLimiter implements RateLimiter {
+ private final int limit;
+ private final long windowMillis;
+ private final ConcurrentHashMap counters = new ConcurrentHashMap<>();
+
+ public FixedWindowRateLimiter(int limit, long windowSeconds) {
+ this.limit = limit;
+ this.windowMillis = TimeUnit.SECONDS.toMillis(windowSeconds);
+ }
+
+ @Override
+ public synchronized void check(String serviceName, String operationName)
+ throws RateLimitException {
+ String key = serviceName + ":" + operationName;
+ WindowCounter counter = counters.computeIfAbsent(key, k -> new WindowCounter());
+
+ if (!counter.tryIncrement()) {
+ System.out.printf(
+ "[FixedWindow] Throttled %s.%s - Limit %d reached in window%n",
+ serviceName, operationName, limit);
+ throw new RateLimitException("Rate limit exceeded for " + key, windowMillis);
+ } else {
+ System.out.printf(
+ "[FixedWindow] Allowed %s.%s - Count within window%n", serviceName, operationName);
+ }
+ }
+
+ /** Tracks the count of requests within the current window. */
+ private class WindowCounter {
+ private AtomicInteger count = new AtomicInteger(0);
+ private volatile long windowStart = System.currentTimeMillis();
+
+ synchronized boolean tryIncrement() {
+ long now = System.currentTimeMillis();
+ // Reset window if expired
+ if (now - windowStart > windowMillis) {
+ count.set(0);
+ windowStart = now;
+ }
+ // Enforce the request limit within window
+ return count.incrementAndGet() <= limit;
+ }
+ }
+}
diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimitException.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimitException.java
new file mode 100644
index 000000000000..2b3cc1f3006b
--- /dev/null
+++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimitException.java
@@ -0,0 +1,15 @@
+package com.iluwatar.rate.limiting.pattern;
+
+/** Base exception for rate limiting errors. */
+public class RateLimitException extends Exception {
+ private final long retryAfterMillis;
+
+ public RateLimitException(String message, long retryAfterMillis) {
+ super(message);
+ this.retryAfterMillis = retryAfterMillis;
+ }
+
+ public long getRetryAfterMillis() {
+ return retryAfterMillis;
+ }
+}
diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimitOperation.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimitOperation.java
new file mode 100644
index 000000000000..59191e81fc04
--- /dev/null
+++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimitOperation.java
@@ -0,0 +1,10 @@
+package com.iluwatar.rate.limiting.pattern;
+
+/** Represents a business operation that needs rate limiting. Supports type-safe return values. */
+public interface RateLimitOperation {
+ String getServiceName();
+
+ String getOperationName();
+
+ T execute() throws RateLimitException;
+}
diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimiter.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimiter.java
new file mode 100644
index 000000000000..19495b401ddb
--- /dev/null
+++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/RateLimiter.java
@@ -0,0 +1,13 @@
+package com.iluwatar.rate.limiting.pattern;
+
+/** Base interface for all rate limiter strategies. */
+public interface RateLimiter {
+ /**
+ * Checks if a request is allowed under current rate limits
+ *
+ * @param serviceName Service being called (e.g., "dynamodb")
+ * @param operationName Operation being performed (e.g., "Query")
+ * @throws RateLimitException if request exceeds limits
+ */
+ void check(String serviceName, String operationName) throws RateLimitException;
+}
diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/ServiceUnavailableException.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/ServiceUnavailableException.java
new file mode 100644
index 000000000000..f9fc55f15d20
--- /dev/null
+++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/ServiceUnavailableException.java
@@ -0,0 +1,15 @@
+package com.iluwatar.rate.limiting.pattern;
+
+/** Exception for when a service is temporarily unavailable. */
+public class ServiceUnavailableException extends RateLimitException {
+ private final String serviceName;
+
+ public ServiceUnavailableException(String serviceName, long retryAfterMillis) {
+ super("Service temporarily unavailable: " + serviceName, retryAfterMillis);
+ this.serviceName = serviceName;
+ }
+
+ public String getServiceName() {
+ return serviceName;
+ }
+}
diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/ThrottlingException.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/ThrottlingException.java
new file mode 100644
index 000000000000..e07087dfee6c
--- /dev/null
+++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/ThrottlingException.java
@@ -0,0 +1,21 @@
+package com.iluwatar.rate.limiting.pattern;
+
+/** Exception thrown when AWS-style throttling occurs. */
+public class ThrottlingException extends RateLimitException {
+ private final String serviceName;
+ private final String errorCode;
+
+ public ThrottlingException(String serviceName, String operationName, long retryAfterMillis) {
+ super("AWS throttling error for " + serviceName + "/" + operationName, retryAfterMillis);
+ this.serviceName = serviceName;
+ this.errorCode = "ThrottlingException";
+ }
+
+ public String getServiceName() {
+ return serviceName;
+ }
+
+ public String getErrorCode() {
+ return errorCode;
+ }
+}
diff --git a/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/TokenBucketRateLimiter.java b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/TokenBucketRateLimiter.java
new file mode 100644
index 000000000000..3001c880aad3
--- /dev/null
+++ b/rate-limiting-pattern/src/main/java/com/iluwatar/rate/limiting/pattern/TokenBucketRateLimiter.java
@@ -0,0 +1,64 @@
+package com.iluwatar.rate.limiting.pattern;
+
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Token Bucket rate limiter implementation. Allows requests to proceed as long as there are tokens
+ * available in the bucket. Tokens are added at a fixed interval up to a defined capacity.
+ */
+public class TokenBucketRateLimiter implements RateLimiter {
+ private final int capacity;
+ private final int refillRate;
+ private final ConcurrentHashMap buckets = new ConcurrentHashMap<>();
+ private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
+
+ public TokenBucketRateLimiter(int capacity, int refillRate) {
+ this.capacity = capacity;
+ this.refillRate = refillRate;
+ // Refill tokens in all buckets every second
+ scheduler.scheduleAtFixedRate(this::refillBuckets, 1, 1, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void check(String serviceName, String operationName) throws RateLimitException {
+ String key = serviceName + ":" + operationName;
+ TokenBucket bucket = buckets.computeIfAbsent(key, k -> new TokenBucket(capacity));
+
+ if (!bucket.tryConsume()) {
+ System.out.printf(
+ "[TokenBucket] Throttled %s.%s - No tokens available%n", serviceName, operationName);
+ throw new ThrottlingException(serviceName, operationName, 1000);
+ } else {
+ System.out.printf(
+ "[TokenBucket] Allowed %s.%s - Tokens remaining%n", serviceName, operationName);
+ }
+ }
+
+ private void refillBuckets() {
+ buckets.forEach((k, b) -> b.refill(refillRate));
+ }
+
+ /** Inner class that represents the bucket holding tokens for each service-operation. */
+ private static class TokenBucket {
+ private final int capacity;
+ private final AtomicInteger tokens;
+
+ TokenBucket(int capacity) {
+ this.capacity = capacity;
+ this.tokens = new AtomicInteger(capacity);
+ }
+
+ boolean tryConsume() {
+ while (true) {
+ int current = tokens.get();
+ if (current <= 0) return false;
+ if (tokens.compareAndSet(current, current - 1)) return true;
+ }
+ }
+
+ void refill(int amount) {
+ tokens.getAndUpdate(current -> Math.min(current + amount, capacity));
+ }
+ }
+}
diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AdaptiveRateLimiterTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AdaptiveRateLimiterTest.java
new file mode 100644
index 000000000000..042d606490d0
--- /dev/null
+++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AdaptiveRateLimiterTest.java
@@ -0,0 +1,56 @@
+package com.iluwatar.rate.limiting.pattern;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+class AdaptiveRateLimiterTest {
+ @Test
+ void shouldDecreaseLimitWhenThrottled() throws Exception {
+ AdaptiveRateLimiter limiter = new AdaptiveRateLimiter(10, 20);
+
+ // Exceed initial limit
+ for (int i = 0; i < 11; i++) {
+ try {
+ limiter.check("test", "op");
+ } catch (RateLimitException e) {
+ // Expected after 10 requests
+ }
+ }
+
+ // Verify limit was reduced
+ assertThrows(
+ RateLimitException.class,
+ () -> {
+ for (int i = 0; i < 6; i++) { // New limit should be 5 (10/2)
+ limiter.check("test", "op");
+ }
+ });
+ }
+
+ @Test
+ void shouldGraduallyIncreaseLimitWhenHealthy() throws Exception {
+ AdaptiveRateLimiter limiter =
+ new AdaptiveRateLimiter(4, 10); // Start from 4 → expect 2 → expect increase to 4
+
+ // Force throttling to reduce limit
+ for (int i = 0; i < 5; i++) {
+ try {
+ limiter.check("test", "op");
+ } catch (RateLimitException e) {
+ // Expected to throttle and reduce limit
+ }
+ }
+
+ // Wait for health check to increase limit
+ Thread.sleep(11000); // Wait slightly more than 10 seconds
+
+ // Allow up to 4 requests again (limit should've increased to 4)
+ for (int i = 0; i < 4; i++) {
+ limiter.check("test", "op");
+ }
+
+ // 5th should throw exception again
+ assertThrows(RateLimitException.class, () -> limiter.check("test", "op"));
+ }
+}
diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AppTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AppTest.java
new file mode 100644
index 000000000000..11815a75de84
--- /dev/null
+++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AppTest.java
@@ -0,0 +1,59 @@
+package com.iluwatar.rate.limiting.pattern;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link App}. */
+class AppTest {
+
+ private RateLimiter mockLimiter;
+
+ @BeforeEach
+ void setUp() {
+ mockLimiter = mock(RateLimiter.class);
+ AppTestUtils.resetCounters(); // Ensures counters are clean before every test
+ }
+
+ @Test
+ void shouldAllowRequest() {
+ AppTestUtils.invokeMakeRequest(1, mockLimiter, "s3", "GetObject");
+ assertEquals(1, AppTestUtils.getSuccessfulRequests().get(), "Successful count should be 1");
+ assertEquals(0, AppTestUtils.getThrottledRequests().get(), "Throttled count should be 0");
+ assertEquals(0, AppTestUtils.getFailedRequests().get(), "Failed count should be 0");
+ }
+
+ @Test
+ void shouldHandleThrottlingException() throws Exception {
+ doThrow(new ThrottlingException("s3", "PutObject", 1000)).when(mockLimiter).check(any(), any());
+ AppTestUtils.invokeMakeRequest(2, mockLimiter, "s3", "PutObject");
+ assertEquals(0, AppTestUtils.getSuccessfulRequests().get());
+ assertEquals(1, AppTestUtils.getThrottledRequests().get());
+ assertEquals(0, AppTestUtils.getFailedRequests().get());
+ }
+
+ @Test
+ void shouldHandleServiceUnavailableException() throws Exception {
+ doThrow(new ServiceUnavailableException("lambda", 500)).when(mockLimiter).check(any(), any());
+ AppTestUtils.invokeMakeRequest(3, mockLimiter, "lambda", "Invoke");
+ assertEquals(0, AppTestUtils.getSuccessfulRequests().get());
+ assertEquals(0, AppTestUtils.getThrottledRequests().get());
+ assertEquals(1, AppTestUtils.getFailedRequests().get());
+ }
+
+ @Test
+ void shouldHandleGenericException() throws Exception {
+ doThrow(new RuntimeException("Unexpected")).when(mockLimiter).check(any(), any());
+ AppTestUtils.invokeMakeRequest(4, mockLimiter, "dynamodb", "Query");
+ assertEquals(0, AppTestUtils.getSuccessfulRequests().get());
+ assertEquals(0, AppTestUtils.getThrottledRequests().get());
+ assertEquals(1, AppTestUtils.getFailedRequests().get());
+ }
+
+ @Test
+ void shouldRunMainMethodWithoutException() {
+ assertDoesNotThrow(() -> App.main(new String[] {}));
+ }
+}
diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AppTestUtils.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AppTestUtils.java
new file mode 100644
index 000000000000..9d652ec96c63
--- /dev/null
+++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/AppTestUtils.java
@@ -0,0 +1,27 @@
+package com.iluwatar.rate.limiting.pattern;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class AppTestUtils {
+
+ public static void invokeMakeRequest(
+ int clientId, RateLimiter limiter, String service, String operation) {
+ App.makeRequest(clientId, limiter, service, operation);
+ }
+
+ public static void resetCounters() {
+ App.resetCountersForTesting();
+ }
+
+ public static AtomicInteger getSuccessfulRequests() {
+ return App.successfulRequests;
+ }
+
+ public static AtomicInteger getThrottledRequests() {
+ return App.throttledRequests;
+ }
+
+ public static AtomicInteger getFailedRequests() {
+ return App.failedRequests;
+ }
+}
diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/ConcurrencyTests.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/ConcurrencyTests.java
new file mode 100644
index 000000000000..35a1294a8ad3
--- /dev/null
+++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/ConcurrencyTests.java
@@ -0,0 +1,69 @@
+package com.iluwatar.rate.limiting.pattern;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.Test;
+
+class ConcurrencyTests {
+ @Test
+ void tokenBucketShouldHandleConcurrentRequests() throws Exception {
+ int threadCount = 10;
+ int requestLimit = 5;
+ RateLimiter limiter = new TokenBucketRateLimiter(requestLimit, requestLimit);
+ ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+ CountDownLatch latch = new CountDownLatch(threadCount);
+
+ AtomicInteger successCount = new AtomicInteger();
+ AtomicInteger failureCount = new AtomicInteger();
+
+ for (int i = 0; i < threadCount; i++) {
+ executor.submit(
+ () -> {
+ try {
+ limiter.check("test", "op");
+ successCount.incrementAndGet();
+ } catch (RateLimitException e) {
+ failureCount.incrementAndGet();
+ }
+ latch.countDown();
+ });
+ }
+
+ latch.await();
+ assertEquals(requestLimit, successCount.get());
+ assertEquals(threadCount - requestLimit, failureCount.get());
+ }
+
+ @Test
+ void adaptiveLimiterShouldAdjustUnderLoad() throws Exception {
+ AdaptiveRateLimiter limiter = new AdaptiveRateLimiter(10, 20);
+ ExecutorService executor = Executors.newFixedThreadPool(20);
+
+ // Flood with requests to trigger throttling
+ for (int i = 0; i < 30; i++) {
+ executor.submit(
+ () -> {
+ try {
+ limiter.check("test", "op");
+ } catch (RateLimitException ignored) {
+ }
+ });
+ }
+
+ Thread.sleep(15000); // Wait for adjustment
+
+ // Verify new limit is in effect
+ int allowed = 0;
+ for (int i = 0; i < 20; i++) {
+ try {
+ limiter.check("test", "op");
+ allowed++;
+ } catch (RateLimitException ignored) {
+ }
+ }
+
+ assertTrue(allowed > 5 && allowed < 15); // Should be between initial and max
+ }
+}
diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/ExceptionTests.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/ExceptionTests.java
new file mode 100644
index 000000000000..a7b037fbe73f
--- /dev/null
+++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/ExceptionTests.java
@@ -0,0 +1,28 @@
+package com.iluwatar.rate.limiting.pattern;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+class ExceptionTests {
+ @Test
+ void rateLimitExceptionShouldContainRetryInfo() {
+ RateLimitException exception = new RateLimitException("Test", 1000);
+ assertEquals(1000, exception.getRetryAfterMillis());
+ assertEquals("Test", exception.getMessage());
+ }
+
+ @Test
+ void throttlingExceptionShouldContainServiceInfo() {
+ ThrottlingException exception = new ThrottlingException("dynamodb", "Query", 500);
+ assertEquals("dynamodb", exception.getServiceName());
+ assertEquals("ThrottlingException", exception.getErrorCode());
+ }
+
+ @Test
+ void serviceUnavailableExceptionShouldContainRetryInfo() {
+ ServiceUnavailableException exception = new ServiceUnavailableException("s3", 2000);
+ assertEquals("s3", exception.getServiceName());
+ assertEquals(2000, exception.getRetryAfterMillis());
+ }
+}
diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequestTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequestTest.java
new file mode 100644
index 000000000000..d0c3197289cd
--- /dev/null
+++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FindCustomerRequestTest.java
@@ -0,0 +1,62 @@
+package com.iluwatar.rate.limiting.pattern;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+class FindCustomerRequestTest implements RateLimitOperationTest {
+
+ @Override
+ public RateLimitOperation createOperation(RateLimiter limiter) {
+ return new FindCustomerRequest("123", limiter);
+ }
+
+ @Test
+ void shouldExecuteWhenUnderRateLimit() throws Exception {
+ RateLimiter limiter = new TokenBucketRateLimiter(10, 10);
+ RateLimitOperation request = createOperation(limiter);
+
+ String result = request.execute();
+ assertEquals("Customer-123", result);
+ }
+
+ @Test
+ void shouldThrowWhenRateLimitExceeded() {
+ RateLimiter limiter = new TokenBucketRateLimiter(0, 0); // Always throttled
+ RateLimitOperation request = createOperation(limiter);
+
+ assertThrows(RateLimitException.class, request::execute);
+ }
+
+ @Test
+ void shouldReturnCorrectServiceAndOperationNames() {
+ RateLimiter limiter = new TokenBucketRateLimiter(10, 10);
+ FindCustomerRequest request = new FindCustomerRequest("123", limiter);
+
+ assertEquals("CustomerService", request.getServiceName());
+ assertEquals("FindCustomer", request.getOperationName());
+ }
+
+ // Reuse helper logic from the interface for coverage
+ @Test
+ void shouldExecuteUsingDefaultHelper() throws Exception {
+ RateLimiter limiter = new TokenBucketRateLimiter(5, 5);
+ shouldExecuteWhenUnderLimit(createOperation(limiter));
+ }
+
+ @Test
+ void shouldThrowServiceUnavailableOnInterruptedException() {
+ RateLimiter noOpLimiter = (service, operation) -> {}; // no throttling
+
+ FindCustomerRequest request =
+ new FindCustomerRequest("999", noOpLimiter) {
+ @Override
+ public String execute() throws RateLimitException {
+ Thread.currentThread().interrupt(); // Simulate thread interruption
+ return super.execute(); // Should throw ServiceUnavailableException
+ }
+ };
+
+ assertThrows(ServiceUnavailableException.class, request::execute);
+ }
+}
diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FixedWindowRateLimiterTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FixedWindowRateLimiterTest.java
new file mode 100644
index 000000000000..656185b7e68e
--- /dev/null
+++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/FixedWindowRateLimiterTest.java
@@ -0,0 +1,40 @@
+package com.iluwatar.rate.limiting.pattern;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.Test;
+
+class FixedWindowRateLimiterTest extends RateLimiterTest {
+ @Override
+ protected RateLimiter createRateLimiter(int limit, long windowMillis) {
+ return new FixedWindowRateLimiter(limit, windowMillis / 1000);
+ }
+
+ @Test
+ void shouldResetCounterAfterWindow() throws Exception {
+ FixedWindowRateLimiter limiter =
+ new FixedWindowRateLimiter(1, 1); // 1 request per 1 second window
+
+ // First request should pass
+ limiter.check("test", "op");
+
+ // Second request in same window should be throttled
+ assertThrows(RateLimitException.class, () -> limiter.check("test", "op"));
+
+ // Wait a bit more than 1 second to ensure window resets
+ TimeUnit.MILLISECONDS.sleep(1100);
+
+ // After window reset, this should pass again
+ limiter.check("test", "op");
+ }
+
+ @Test
+ void shouldNotAllowMoreThanLimitInWindow() throws Exception {
+ FixedWindowRateLimiter limiter = new FixedWindowRateLimiter(3, 1);
+ for (int i = 0; i < 3; i++) {
+ limiter.check("test", "op");
+ }
+ assertThrows(RateLimitException.class, () -> limiter.check("test", "op"));
+ }
+}
diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/RateLimitOperationTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/RateLimitOperationTest.java
new file mode 100644
index 000000000000..d4922bdfc073
--- /dev/null
+++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/RateLimitOperationTest.java
@@ -0,0 +1,22 @@
+package com.iluwatar.rate.limiting.pattern;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+interface RateLimitOperationTest {
+
+ RateLimitOperation createOperation(RateLimiter limiter);
+
+ @Test
+ default void shouldThrowWhenRateLimited() {
+ RateLimiter limiter = new TokenBucketRateLimiter(0, 0); // Always throttled
+ RateLimitOperation operation = createOperation(limiter);
+ assertThrows(RateLimitException.class, operation::execute);
+ }
+
+ // ✅ No @Test here, just a helper method
+ default void shouldExecuteWhenUnderLimit(RateLimitOperation operation) throws Exception {
+ assertNotNull(operation.execute());
+ }
+}
diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/RateLimiterTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/RateLimiterTest.java
new file mode 100644
index 000000000000..7f1e6b4a2806
--- /dev/null
+++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/RateLimiterTest.java
@@ -0,0 +1,25 @@
+package com.iluwatar.rate.limiting.pattern;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+public abstract class RateLimiterTest {
+ protected abstract RateLimiter createRateLimiter(int limit, long windowMillis);
+
+ @Test
+ void shouldAllowRequestsWithinLimit() throws Exception {
+ RateLimiter limiter = createRateLimiter(5, 1000);
+ for (int i = 0; i < 5; i++) {
+ limiter.check("test", "op");
+ }
+ }
+
+ @Test
+ void shouldThrowWhenLimitExceeded() throws Exception {
+ RateLimiter limiter = createRateLimiter(2, 1000);
+ limiter.check("test", "op");
+ limiter.check("test", "op");
+ assertThrows(RateLimitException.class, () -> limiter.check("test", "op"));
+ }
+}
diff --git a/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/TokenBucketRateLimiterTest.java b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/TokenBucketRateLimiterTest.java
new file mode 100644
index 000000000000..5696299512fb
--- /dev/null
+++ b/rate-limiting-pattern/src/test/java/com/iluwatar/rate/limiting/pattern/TokenBucketRateLimiterTest.java
@@ -0,0 +1,39 @@
+package com.iluwatar.rate.limiting.pattern;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.Test;
+
+class TokenBucketRateLimiterTest extends RateLimiterTest {
+ @Override
+ protected RateLimiter createRateLimiter(int limit, long windowMillis) {
+ return new TokenBucketRateLimiter(limit, (int) (limit * 1000 / windowMillis));
+ }
+
+ @Test
+ void shouldAllowBurstRequests() throws Exception {
+ TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(10, 5);
+ for (int i = 0; i < 10; i++) {
+ limiter.check("test", "op");
+ }
+ }
+
+ @Test
+ void shouldRefillTokensAfterTime() throws Exception {
+ TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(1, 1);
+ limiter.check("test", "op");
+ assertThrows(RateLimitException.class, () -> limiter.check("test", "op"));
+
+ TimeUnit.SECONDS.sleep(1);
+ limiter.check("test", "op");
+ }
+
+ @Test
+ void shouldHandleMultipleServicesSeparately() throws Exception {
+ TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(1, 1);
+ limiter.check("service1", "op");
+ limiter.check("service2", "op");
+ assertThrows(RateLimitException.class, () -> limiter.check("service1", "op"));
+ }
+}