Phase 05 Projects — Prove You Can Ship Concurrent Code¶
Reading about concurrency is the easy part. Shipping something that survives 10k concurrent requests and doesn’t deadlock at 3 AM is the part that gets you the offer. Three projects. Each one forces a different muscle group.
Project A: Thread-Safe Rate Limiter¶
Time estimate: 10-14 hours
Problem Statement¶
Build a rate limiter as a Java library with two algorithms — token bucket and sliding window log — that is safe under high concurrency, correct under contention, and observable in production.
Requirements¶
API:
boolean tryAcquire(String key, int permits)andlong acquireBlocking(String key, int permits)(returns wait time in nanos).Configurable rate per key: e.g., “user-42 is allowed 100 requests/sec, burst 200.”
Two implementations behind the same interface:
Token bucket — refills continuously, allows bursts up to capacity.
Sliding window log — exact count of requests in the last N milliseconds.
Per-key state stored in
ConcurrentHashMap<String, State>withcomputeIfAbsent.Metrics: total permits granted, total rejections, current in-flight count, hot keys.
Technical Constraints¶
Must use:
AtomicLongorLongAdderfor counters,ConcurrentHashMap,ReentrantLockwhere locking is needed (notsynchronized— you’re forward-compatible with virtual threads),System.nanoTime()for time (nevercurrentTimeMillis— it goes backwards on NTP adjustments).Must NOT use: Guava’s
RateLimiter, Resilience4j, or any external limiter library. You’re building it.Test with: at least 100 producer threads on a
CountDownLatchgate, driving 1,000,000 total attempts. Verify: (a) the total granted matches the configured rate within tolerance, (b) no over-grant, (c) throughput is > 5M ops/sec on a modern laptop fortryAcquire.
Acceptance Criteria¶
Both algorithms implement the same
RateLimiterinterface.Stress test with 100 concurrent threads shows no over-grant (total permits granted ≤ (rate × duration) + burst).
Under contention,
tryAcquirethroughput > 5M ops/sec (JMH-measured, see Phase 06 for JMH).Rate limits are per-key and independent — hammering key
Adoesn’t slow down keyB.Idle keys are eventually evicted (implement TTL or
Cleaner— unbounded map = OOM).Exposes
MicrometerMeterRegistry-compatible metrics (or write your own tiny metrics interface).Written up: README explaining trade-offs between token bucket and sliding window, and why sliding window is more accurate but more memory-intensive.
What This Tests¶
Concurrent map usage, atomic primitives, CAS retry loops, per-key contention isolation, backpressure, and observability. This is essentially the code you’d write for an API gateway or an internal quota service. It also forces you to prove correctness with a stress test — you cannot claim rate limiting is correct without evidence.
Project B: Virtual Threads vs Thread Pool vs CompletableFuture Shootout¶
Time estimate: 12-16 hours
Problem Statement¶
Build a small HTTP client demo that fetches 10,000 URLs and measures wall-clock time, thread count, memory, and p99 latency under three concurrency models. Write it up like a Netflix Tech Blog post — numbers, graphs, and a recommendation.
Requirements¶
Target service: a local Spring Boot app with an endpoint that
Thread.sleeps a random duration between 50-200 ms (simulating a slow downstream). Use MockServer, WireMock, or write it yourself.Three client implementations, same input list of 10,000 URLs:
Platform thread pool:
Executors.newFixedThreadPool(N)where N is swept over{16, 64, 200, 1000}.Virtual threads:
Executors.newVirtualThreadPerTaskExecutor().CompletableFuture: async pipeline on a fixed pool, non-blocking
HttpClient(sendAsync).
Measure for each: wall-clock time, peak live thread count (
jconsoleor JMX), heap after run (jcmd GC.heap_info), and p99 request latency.Introduce a
synchronizedblock that does an I/O call somewhere in one variant. Show that under Java 21-23, virtual thread throughput collapses. Show thatReentrantLockfixes it.
Technical Constraints¶
Must use:
java.net.http.HttpClient(JDK built-in), JFR to capturejdk.VirtualThreadPinnedevents for the virtual-thread run,-Djdk.tracePinnedThreads=shortin the pinning demo.Must NOT: call
System.out.printlninside the timed section (it’s synchronized — you’ll skew the numbers). Buffer output, print after.Target JDK: 21+. If you can run 24+, run the same benchmark on both and quantify the pinning fix from JEP 491.
Acceptance Criteria¶
All three variants complete 10,000 requests successfully.
Report includes a table of wall-clock, peak thread count, and heap for each variant.
Report includes p99 latency — not just mean.
Report explicitly demonstrates the
synchronizedpinning problem on Java 21-23 with numbers.Report includes a JFR flame graph or screenshot of the
jdk.VirtualThreadPinnedevents (JMC or equivalent).Report concludes with a decision table: when to use each.
Repo is public on GitHub with a README of your findings. This is a portfolio artifact.
What This Tests¶
Empirical thinking. study partners will ask “when do virtual threads help?” Anyone can quote the JEP. You’ll have measured it, and you’ll have the graphs. This is the project that answers the study question with numbers.
Project C: Deadlock Post-Mortem¶
Time estimate: 6-10 hours
Problem Statement¶
Given a deliberately deadlocked codebase (you’ll build the buggy version yourself, then debug it as if a colleague had written it), reproduce the deadlock, identify the root cause, propose two fixes, implement the better one, and write a post-mortem in the style of an incident review.
Setup¶
Build a small banking service with the classic ordering bug:
public class Account {
private final long id;
private BigDecimal balance;
public synchronized void transfer(Account other, BigDecimal amount) {
synchronized (other) {
this.balance = this.balance.subtract(amount);
other.balance = other.balance.add(amount);
}
}
}
Write a driver that spawns 50 threads, each performing random transfers between 10 accounts. Run it. It will deadlock, sometimes in 100ms, sometimes in 10s.
Requirements¶
Reproduce the deadlock reliably (either instrument with
Thread.yieldor run for at most 30 seconds).Capture a
jstackdump at the moment of deadlock and save it to the repo.Annotate the dump: which two threads, which two monitor addresses, which two
Accountinstances.Propose two fixes:
Global lock ordering by
id.tryLockwith timeout on aReentrantLock, retry with random backoff.
Implement the ordering fix (it’s the more idiomatic one for this problem).
Write a jcstress test verifying no invariant violation under high concurrency (total balance across all accounts is invariant).
Deliverable: Post-Mortem¶
Write POST_MORTEM.md in the repo with these sections:
What happened — symptoms, first alert, timeline
Root cause — the lock ordering bug explained in ≤ 5 sentences
Evidence — the annotated
jstackdumpFix — what you changed and why
Alternative fixes considered — the
tryLockapproach, its trade-offsPrevention — how to catch this in code review going forward (lock-ordering conventions, tools like
-XX:+PrintConcurrentLocks, chaos tests)Verification — the jcstress test output showing no violations after the fix
Acceptance Criteria¶
The bug reproduces within 30 seconds on your machine, at least 5 out of 5 runs.
The
jstackdump clearly shows the cycle.The fix removes the deadlock (10,000 transfers run to completion in < 5 seconds).
Total balance is invariant across all transfers (prove it in the test).
Post-mortem reads like something you’d file at a real company — not a homework write-up.
What This Tests¶
Everything. Debugging under pressure, reading thread dumps, choosing between remediation strategies, and writing clearly about failure. This project is what you’d hand to an study partner as evidence you can survive a production incident.
Choosing Your Projects¶
Minimum: Complete A and C. Ship them. Recommended: Complete all three, in this order: A → C → B. A is a warm-up, C teaches you the tools, B applies them at scale.
If you finish all three, write about them. Not just READMEs — short blog posts. Publish them (or draft them and don’t publish; either way, writing forces clarity).
⚠️ What Most People Get Wrong¶
They write projects that “work” — the happy path completes. But study partners don’t care that your rate limiter grants a permit; they care whether it never over-grants under 10k concurrent producers. That’s the difference between “I built a rate limiter” and “I built a rate limiter I trust.” Every one of these projects has a stress-test acceptance criterion for exactly this reason. Do not skip them.
Return to README.md · Previous: 06_debugging_concurrency.md