Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
12 changes: 12 additions & 0 deletions exercises/practice/rate-limiter/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
What this exercise is about

You will build a fixed‑window rate limiter: a small component that restricts how many
operations each key may perform within a fixed period of time (a “window”).

What you should implement

- Implement a single method in `src/main/java/FixedWindowRateLimiter.java`:
- `public boolean allow(K key)` — decide whether to allow the operation for the key
and update the limiter’s internal state accordingly.

Everything else (interfaces and constructor) is provided for you.
19 changes: 19 additions & 0 deletions exercises/practice/rate-limiter/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"authors": [],
Copy link
Member

Choose a reason for hiding this comment

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

Add your Github handle (andreatanky) to the authors array.

"files": {
"solution": [
"src/main/java/RateLimiter.java",
"src/main/java/TimeSource.java",
"src/main/java/FixedWindowRateLimiter.java"
],
"test": [
"src/test/java/RateLimiterTest.java"
],
"example": [
".meta/src/reference/java/RateLimiter.java",
".meta/src/reference/java/TimeSource.java",
".meta/src/reference/java/FixedWindowRateLimiter.java"
]
},
"blurb": ""
Copy link
Member

Choose a reason for hiding this comment

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

Missing a blurb. We'll need to come up with something. I'd suggest looking at Restful API's blurb for an example or inspiration.

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've updated it to "Practice stateful logic and time handling by implementing a fixed-window rate limiter". Let me know what you think!

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import java.util.HashMap;
import java.util.Map;

public class FixedWindowRateLimiter<K> implements 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 FixedWindowRateLimiter(int limit, long windowSizeNanos, TimeSource timeSource) {
this.limit = limit;
this.windowSizeNanos = windowSizeNanos;
this.timeSource = timeSource;
}

@Override
public boolean allow(K key) {
long now = timeSource.nowNanos();
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,3 @@
public interface RateLimiter<K> {
boolean allow(K key);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
public interface TimeSource {
long nowNanos();

final class Fake implements TimeSource {
private long now;

public Fake(long startNanos) {
this.now = startNanos;
}

@Override
public long nowNanos() {
return now;
}

public void advance(long nanos) {
this.now += 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"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import java.util.HashMap;
import java.util.Map;

public class FixedWindowRateLimiter<K> implements 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 FixedWindowRateLimiter(int limit, long windowSizeNanos, TimeSource timeSource) {
this.limit = limit;
this.windowSizeNanos = windowSizeNanos;
this.timeSource = timeSource;
}

@Override
public boolean allow(K key) {
throw new UnsupportedOperationException("Delete this statement and write your own implementation.");
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
public interface RateLimiter<K> {
boolean allow(K key);
}
24 changes: 24 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,24 @@
public interface TimeSource {
long nowNanos();

/**
* Deterministic fake clock for tests.
*/
final class Fake implements TimeSource {
private long now;

public Fake(long startNanos) {
this.now = startNanos;
}

@Override
public long nowNanos() {
return now;
}

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

Choose a reason for hiding this comment

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

The advance could perhaps take a Duration to further use Java's time API and allow other units (other than nano-seconds) to be used.

}
}

88 changes: 88 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,88 @@
import org.junit.jupiter.api.Test;

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

class RateLimiterTest {

@Test
void allowsUpToLimitThenDeniesUntilBoundary() {
TimeSource.Fake clock = new TimeSource.Fake(0L);
RateLimiter<String> limiter = new FixedWindowRateLimiter<>(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.advance(9_999L);
assertThat(limiter.allow("A")).isFalse();

// At exact boundary: new window
clock.advance(1L);
assertThat(limiter.allow("A")).isTrue();
}
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 there is quite a bit going on in this test case. Could we split up this into 3 tests? I think splitting would allow students to "ease" into the exercise better than having it all in the first test.

  • the first would be the assertions the from 12-15 (the simplest case)
  • the second checks the new window at the boundary (lines 21-23)
  • the third could check advance just before the boundary (lines 17-19)


@Test
void continuesCountingWithinWindowAfterBoundaryReset() {
TimeSource.Fake clock = new TimeSource.Fake(0L);
RateLimiter<String> limiter = new FixedWindowRateLimiter<>(2, 5_000L, clock);

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

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

@Test
void separateKeysHaveIndependentCountersAndWindows() {
TimeSource.Fake clock = new TimeSource.Fake(42L);
RateLimiter<String> limiter = new FixedWindowRateLimiter<>(1, 100L, clock);

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

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

@Test
void longGapsResetWindowDeterministically() {
TimeSource.Fake clock = new TimeSource.Fake(1_000L);
RateLimiter<String> limiter = new FixedWindowRateLimiter<>(2, 50L, clock);

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

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

@Test
void exactBoundaryIsNewWindowEveryTime() {
TimeSource.Fake clock = new TimeSource.Fake(0L);
RateLimiter<String> limiter = new FixedWindowRateLimiter<>(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.advance(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