Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
0513d96
Run configlet create
andreatanky Sep 8, 2025
bfe4dbb
Update rate limiter implementation
andreatanky Sep 12, 2025
81c6728
Add instructions
andreatanky Sep 12, 2025
ae33983
Add instructions
andreatanky Sep 12, 2025
4f5abf3
Merge branch 'main' into add-rate-limiter-exercise
andreatanky Sep 12, 2025
4f0c836
Remove interfaces
andreatanky Sep 14, 2025
a697b81
Update instructions
andreatanky Sep 14, 2025
8092b9a
Merge branch 'add-rate-limiter-exercise' of github.com:andreatanky/ja…
andreatanky Sep 14, 2025
87fc175
Add github handle to authors array
andreatanky Sep 14, 2025
4479173
Add blurb
andreatanky Sep 14, 2025
d9520b7
Merge branch 'main' into add-rate-limiter-exercise
andreatanky Sep 14, 2025
63ea8a3
Update long to use Duration
andreatanky Sep 18, 2025
9f6a8bf
Update advance method to use Duration in tests
andreatanky Sep 19, 2025
a616a10
Remove unused advanceNanos method
andreatanky Sep 19, 2025
9f077d0
Add non string tests
andreatanky Sep 19, 2025
89ec426
Break down test cases
andreatanky Sep 19, 2025
9657afc
Update to one sentence per line
andreatanky Sep 20, 2025
98d6103
Update WindowState long type to use Instant
andreatanky Sep 20, 2025
43610f1
Remove advanceNanos
andreatanky Sep 20, 2025
a61ef4d
Merge branch 'main' into add-rate-limiter-exercise
andreatanky Sep 20, 2025
50b280c
Update naming of key to clientId
andreatanky Sep 22, 2025
5971500
Merge branch 'add-rate-limiter-exercise' of github.com:andreatanky/ja…
andreatanky Sep 22, 2025
69c1ba9
Update exercises/practice/rate-limiter/.docs/instructions.md
andreatanky Sep 22, 2025
6f27c12
Update exercises/practice/rate-limiter/.docs/instructions.md
andreatanky Sep 22, 2025
368d7ea
Remove custom object test
andreatanky Sep 22, 2025
87c3d3c
Update exercises/practice/rate-limiter/src/test/java/RateLimiterTest.…
andreatanky Sep 22, 2025
d451060
Add tiny clock advances in tests
andreatanky Sep 22, 2025
67baa14
Update difficulty to 4
andreatanky Sep 22, 2025
aba22d5
Add generic types to prereq
andreatanky Sep 22, 2025
715f181
Update exercises/practice/rate-limiter/src/main/java/TimeSource.java
andreatanky Sep 22, 2025
efadb02
Update exercises/practice/rate-limiter/.meta/config.json
andreatanky Sep 22, 2025
8dc4059
Update UUID test to include mixture of clock advance duration
andreatanky Sep 22, 2025
b6dea08
Merge branch 'add-rate-limiter-exercise' of github.com:andreatanky/ja…
andreatanky Sep 22, 2025
ae8d97e
Merge branch 'main' into add-rate-limiter-exercise
andreatanky Sep 22, 2025
60736bb
Shift exercise in config.json
andreatanky Sep 23, 2025
25a2876
Merge branch 'add-rate-limiter-exercise' of github.com:andreatanky/ja…
andreatanky Sep 23, 2025
d418567
Copy gradle wrapper
andreatanky Sep 23, 2025
baf39d2
Update exercises/practice/rate-limiter/.docs/instructions.md
andreatanky Sep 23, 2025
7907757
Update exercises/practice/rate-limiter/.docs/instructions.md
andreatanky Sep 23, 2025
6100e11
Add display name annotation
andreatanky Sep 23, 2025
4d9b1bf
Merge branch 'add-rate-limiter-exercise' of github.com:andreatanky/ja…
andreatanky Sep 23, 2025
6ccd97a
Run configlet to format config.json
andreatanky Sep 23, 2025
b839de1
Reformat files
andreatanky Sep 23, 2025
a8a6048
Update exercises/practice/rate-limiter/src/test/java/RateLimiterTest.…
andreatanky Sep 24, 2025
6b8e310
Update exercises/practice/rate-limiter/src/test/java/RateLimiterTest.…
andreatanky Sep 24, 2025
b3667bc
Update exercises/practice/rate-limiter/src/test/java/RateLimiterTest.…
andreatanky Sep 24, 2025
0b158f7
Update exercises/practice/rate-limiter/src/main/java/RateLimiter.java
andreatanky Sep 24, 2025
d5b13e9
Merge branch 'main' into add-rate-limiter-exercise
andreatanky Sep 24, 2025
5e4cc5c
Update config.json
andreatanky Sep 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1900,6 +1900,14 @@
"lists"
],
"difficulty": 10
},
{
"slug": "rate-limiter",
"name": "rate-limiter",
"uuid": "b4b0c60e-4ce1-488e-948f-bcb6821c773c",
"practices": [],
"prerequisites": [],
"difficulty": 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the difficulty, would you say it is similar in difficulty as Luhn? If so, we could say it is a 4 (a 4 is one of the lower medium difficulty, 3 is easy). I think 1 is too low.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think low‑medium (4) is reasonable! it only requires per‑key state and fixed‑window boundary handling with java.time, but avoids concurrency and advanced algorithms like sliding windows or token buckets.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! The exercises are sorted by difficulty and then name though. Could you please move this entry to between proverb and rotational-cipher in the exercises list?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shifted! Thanks!

}
],
"foregone": [
Expand Down
19 changes: 19 additions & 0 deletions exercises/practice/rate-limiter/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Your task is to build a fixed‑window rate limiter.

Imagine a single server connected to one or more clients. A client sends a request, the server
does some work, and returns a response. But processing takes time. If a client sends too many
requests too quickly, the server can become overwhelmed — everything slows down or fails.

A rate limiter is a small component that decides whether to allow or reject a request based on
how frequently that client has been making requests. Different strategies exist; in this exercise
you’ll implement a fixed‑window rate limiter.

Fixed‑window rate limiting groups time into equal‑length windows (for example, every 10 seconds)
and allows up to a certain number of requests within each window for each client. Once the window
resets, the allowance refreshes for the next window. Each client is tracked separately.

Examples:
- Limit: 3 requests per 10 seconds per client.
- A client’s first three requests in the same window are allowed; a fourth is rejected.
- After the window resets, the next request is allowed again and the counting starts fresh.

17 changes: 17 additions & 0 deletions exercises/practice/rate-limiter/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"authors": ["andreatanky"],
"files": {
"solution": [
"src/main/java/RateLimiter.java",
"src/main/java/TimeSource.java"
],
"test": [
"src/test/java/RateLimiterTest.java"
],
"example": [
".meta/src/reference/java/RateLimiter.java",
".meta/src/reference/java/TimeSource.java"
]
},
"blurb": "Practice stateful logic and time handling by implementing a fixed-window rate limiter"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;

public class RateLimiter<K> {

private static final class WindowState {
long windowStartNanos;
int usedCount;

WindowState(long windowStartNanos, int usedCount) {
this.windowStartNanos = windowStartNanos;
this.usedCount = usedCount;
}
}

private final int limit;
private final long windowSizeNanos;
private final TimeSource timeSource;
private final Map<K, WindowState> states = new HashMap<>();

public RateLimiter(int limit, long windowSizeNanos, TimeSource timeSource) {
this.limit = limit;
this.windowSizeNanos = windowSizeNanos;
this.timeSource = timeSource;
}

public boolean allow(K key) {
Instant nowInstant = timeSource.now();
long now = nowInstant.getEpochSecond() * 1_000_000_000L + nowInstant.getNano();

WindowState s = states.get(key);
if (s == null) {
s = new WindowState(now, 0);
states.put(key, s);
}

long elapsed = now - s.windowStartNanos;
if (elapsed >= windowSizeNanos) {
s.windowStartNanos = now;
s.usedCount = 0;
}

if (s.usedCount < limit) {
s.usedCount += 1;
return true;
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import java.time.Duration;
import java.time.Instant;

public class TimeSource {
private Instant now;

public TimeSource(Instant start) {
this.now = start;
}

public Instant now() {
return now;
}

public void advance(Duration d) {
this.now = this.now.plus(d);
}

public void advanceNanos(long nanos) {
this.now = this.now.plusNanos(nanos);
}
}
25 changes: 25 additions & 0 deletions exercises/practice/rate-limiter/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
plugins {
id "java"
}

repositories {
mavenCentral()
}

dependencies {
testImplementation platform("org.junit:junit-bom:5.10.0")
testImplementation "org.junit.jupiter:junit-jupiter"
testImplementation "org.assertj:assertj-core:3.25.1"

testRuntimeOnly "org.junit.platform:junit-platform-launcher"
}

test {
useJUnitPlatform()

testLogging {
exceptionFormat = "full"
showStandardStreams = true
events = ["passed", "failed", "skipped"]
}
}
12 changes: 12 additions & 0 deletions exercises/practice/rate-limiter/src/main/java/RateLimiter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import java.time.Instant;

public class RateLimiter<K> {

public RateLimiter(int limit, long windowSizeNanos, TimeSource timeSource) {

}

public boolean allow(K key) {
throw new UnsupportedOperationException("Delete this statement and write your own implementation.");
}
}
22 changes: 22 additions & 0 deletions exercises/practice/rate-limiter/src/main/java/TimeSource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import java.time.Duration;
import java.time.Instant;

public class TimeSource {
private Instant now;

public TimeSource(Instant start) {
this.now = start;
}

public Instant now() {
return now;
}

public void advance(Duration d) {
this.now = this.now.plus(d);
}

public void advanceNanos(long nanos) {
this.now = this.now.plusNanos(nanos);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you might have forgotten to update the tests to use the advance(Duration d) method.

}
94 changes: 94 additions & 0 deletions exercises/practice/rate-limiter/src/test/java/RateLimiterTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.time.Instant;

import static org.assertj.core.api.Assertions.assertThat;

class RateLimiterTest {

@Test
void allowsUpToLimitThenDeniesUntilBoundary() {
TimeSource clock = new TimeSource(Instant.EPOCH);
RateLimiter<String> limiter = new RateLimiter<>(3, 10_000L, clock);

assertThat(limiter.allow("A")).isTrue();
assertThat(limiter.allow("A")).isTrue();
assertThat(limiter.allow("A")).isTrue();
assertThat(limiter.allow("A")).isFalse();

// Just before boundary: still same window
clock.advanceNanos(9_999L);
assertThat(limiter.allow("A")).isFalse();

// At exact boundary: new window
clock.advanceNanos(1L);
assertThat(limiter.allow("A")).isTrue();
}

@Disabled("Remove to run test")
@Test
void continuesCountingWithinWindowAfterBoundaryReset() {
TimeSource clock = new TimeSource(Instant.EPOCH);
RateLimiter<String> limiter = new RateLimiter<>(2, 5_000L, clock);

assertThat(limiter.allow("key")).isTrue();
assertThat(limiter.allow("key")).isTrue();
assertThat(limiter.allow("key")).isFalse();

// Jump to next window
clock.advanceNanos(5_000L);
assertThat(limiter.allow("key")).isTrue();
assertThat(limiter.allow("key")).isTrue();
assertThat(limiter.allow("key")).isFalse();
}

@Disabled("Remove to run test")
@Test
void separateKeysHaveIndependentCountersAndWindows() {
TimeSource clock = new TimeSource(Instant.EPOCH.plusNanos(42L));
RateLimiter<String> limiter = new RateLimiter<>(1, 100L, clock);

assertThat(limiter.allow("A")).isTrue();
assertThat(limiter.allow("A")).isFalse();
assertThat(limiter.allow("B")).isTrue(); // independent key
assertThat(limiter.allow("B")).isFalse();

clock.advanceNanos(100L); // new window for both at boundary
assertThat(limiter.allow("A")).isTrue();
assertThat(limiter.allow("B")).isTrue();
}

@Disabled("Remove to run test")
@Test
void longGapsResetWindowDeterministically() {
TimeSource clock = new TimeSource(Instant.EPOCH.plusNanos(1_000L));
RateLimiter<String> limiter = new RateLimiter<>(2, 50L, clock);

assertThat(limiter.allow("X")).isTrue();
assertThat(limiter.allow("X")).isTrue();
assertThat(limiter.allow("X")).isFalse();

// Advance several windows worth
clock.advanceNanos(1_000L);
assertThat(limiter.allow("X")).isTrue();
assertThat(limiter.allow("X")).isTrue();
assertThat(limiter.allow("X")).isFalse();
}

@Disabled("Remove to run test")
@Test
void exactBoundaryIsNewWindowEveryTime() {
TimeSource clock = new TimeSource(Instant.EPOCH);
RateLimiter<String> limiter = new RateLimiter<>(1, 10L, clock);

assertThat(limiter.allow("k")).isTrue();
assertThat(limiter.allow("k")).isFalse();

// Move exactly to boundary repeatedly; each time should allow once
for (int i = 0; i < 5; i++) {
clock.advanceNanos(10L);
assertThat(limiter.allow("k")).isTrue();
assertThat(limiter.allow("k")).isFalse();
}
}
}
1 change: 1 addition & 0 deletions exercises/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ include 'practice:proverb'
include 'practice:pythagorean-triplet'
include 'practice:queen-attack'
include 'practice:rail-fence-cipher'
include 'practice:rate-limiter'
include 'practice:raindrops'
include 'practice:rational-numbers'
include 'practice:react'
Expand Down