Coding Interview Questions
Implement a circuit breaker (CLOSED, OPEN, HALF_OPEN) with a fallback.
Count-based sliding window of the last 4 calls, opens at 50% failures, waits 1000 ms, then allows 2 trial calls. A fake clock keeps the output deterministic.
Example
Payment is down, the breaker opens, a failed trial re-opens it, and successful trials close it.
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.function.Supplier;
public class Main {
static long now = 0;
static boolean paymentUp = true;
public static void main(String[] args) {
CircuitBreaker cb = new CircuitBreaker(4, 0.5, 1000, 2);
Supplier<String> payment = () -> {
if (!paymentUp) throw new RuntimeException("timeout");
return "PAID";
};
Supplier<String> fallback = () -> "PAYMENT_PENDING";
String plan = "UUDDDWDWUUU"; // U = payment up, D = payment down, W = wait 1.5 s
int call = 0;
for (char step : plan.toCharArray()) {
if (step == 'W') { now += 1500; System.out.println("... 1500 ms later"); continue; }
paymentUp = step == 'U';
String result = cb.call(payment, fallback);
System.out.printf("call %2d -> %-31s state=%s%n", ++call, result, cb.state);
}
}
}
enum State { CLOSED, OPEN, HALF_OPEN }
class CircuitBreaker {
final int windowSize, trialCalls; final double threshold; final long waitMs;
final Deque<Boolean> window = new ArrayDeque<>();
State state = State.CLOSED;
long openedAt; int trialSuccesses;
CircuitBreaker(int windowSize, double threshold, long waitMs, int trialCalls) {
this.windowSize = windowSize; this.threshold = threshold; this.waitMs = waitMs; this.trialCalls = trialCalls;
}
String call(Supplier<String> action, Supplier<String> fallback) {
if (state == State.OPEN) {
if (Main.now - openedAt < waitMs) return fallback.get() + " (rejected fast)";
state = State.HALF_OPEN; trialSuccesses = 0;
}
try {
String r = action.get();
if (state == State.HALF_OPEN) {
if (++trialSuccesses == trialCalls) { state = State.CLOSED; window.clear(); }
} else record(true);
return r;
} catch (RuntimeException e) {
if (state == State.HALF_OPEN) trip(); else record(false);
return fallback.get() + " (" + e.getMessage() + ")";
}
}
private void record(boolean ok) {
window.addLast(ok);
if (window.size() > windowSize) window.removeFirst();
long failures = window.stream().filter(b -> !b).count();
if (window.size() == windowSize && failures >= threshold * windowSize) trip();
}
private void trip() { state = State.OPEN; openedAt = Main.now; }
}Output
call 1 -> PAID state=CLOSED call 2 -> PAID state=CLOSED call 3 -> PAYMENT_PENDING (timeout) state=CLOSED call 4 -> PAYMENT_PENDING (timeout) state=OPEN call 5 -> PAYMENT_PENDING (rejected fast) state=OPEN ... 1500 ms later call 6 -> PAYMENT_PENDING (timeout) state=OPEN ... 1500 ms later call 7 -> PAID state=HALF_OPEN call 8 -> PAID state=CLOSED call 9 -> PAID state=CLOSED
Tip: Talking points: why a minimum number of calls before tripping, why HALF_OPEN limits trial calls, and that real breakers (Resilience4j) also count slow calls as failures.
Implement a token bucket rate limiter per user.
Example
Each user gets a burst of 5 requests and 2 new tokens per second. Time is passed in, so the output is deterministic.
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
RateLimiter limiter = new RateLimiter(5, 2.0);
Object[][] requests = {
{0L, "asha"}, {0L, "asha"}, {0L, "asha"}, {0L, "asha"}, {0L, "asha"},
{0L, "asha"}, {0L, "ravi"}, {500L, "asha"}, {600L, "asha"}, {3000L, "asha"}
};
for (Object[] r : requests) {
long t = (Long) r[0];
String user = (String) r[1];
boolean ok = limiter.tryAcquire(user, t);
System.out.printf("t=%4d ms %-4s -> %s%n", t, user, ok ? "200 OK" : "429 Too Many Requests");
}
}
}
class RateLimiter {
private final int capacity;
private final double tokensPerMs;
private final Map<String, double[]> buckets = new HashMap<>(); // user -> {tokens, lastRefillMs}
RateLimiter(int capacity, double tokensPerSecond) {
this.capacity = capacity;
this.tokensPerMs = tokensPerSecond / 1000.0;
}
synchronized boolean tryAcquire(String key, long nowMs) {
double[] b = buckets.computeIfAbsent(key, k -> new double[]{capacity, nowMs});
b[0] = Math.min(capacity, b[0] + (nowMs - b[1]) * tokensPerMs); // refill
b[1] = nowMs;
if (b[0] >= 1) { b[0] -= 1; return true; }
return false;
}
}Output
t= 0 ms asha -> 200 OK t= 0 ms asha -> 200 OK t= 0 ms asha -> 200 OK t= 0 ms asha -> 200 OK t= 0 ms asha -> 200 OK t= 0 ms asha -> 429 Too Many Requests t= 0 ms ravi -> 200 OK t= 500 ms asha -> 200 OK t= 600 ms asha -> 429 Too Many Requests t=3000 ms asha -> 200 OK
At t=0 asha uses her 5 tokens; the 6th request is rejected while ravi has his own bucket. 500 ms refills one token; at 600 ms only 0.2 tokens have accumulated. In production the bucket lives in Redis (atomic Lua script) so all gateway instances share it.
Implement an idempotency store so a double-clicked "Pay" charges only once.
Example
Five concurrent requests with the same key: one charges, the other four get the stored result.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
public static void main(String[] args) throws Exception {
PaymentService service = new PaymentService();
List<Future<String>> futures = new ArrayList<>();
try (ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 5; i++) {
futures.add(pool.submit(() -> service.pay("key-7f3a", "ORD-101", 1499)));
}
}
List<String> results = new ArrayList<>();
for (Future<String> f : futures) results.add(f.get());
results.sort(null);
results.forEach(System.out::println);
System.out.println(service.pay("key-9c21", "ORD-102", 799));
System.out.println("Charges sent to provider: " + service.providerCharges.get());
}
}
class PaymentService {
private final ConcurrentHashMap<String, CompletableFuture<String>> store = new ConcurrentHashMap<>();
final AtomicInteger providerCharges = new AtomicInteger();
String pay(String idempotencyKey, String orderId, int amount) {
CompletableFuture<String> mine = new CompletableFuture<>();
CompletableFuture<String> existing = store.putIfAbsent(idempotencyKey, mine); // atomic "insert if new"
if (existing != null) return existing.join() + " (replayed)";
try {
String receipt = chargeProvider(orderId, amount);
mine.complete(receipt);
return receipt;
} catch (RuntimeException e) {
store.remove(idempotencyKey, mine); // failed: allow a retry with the same key
mine.completeExceptionally(e);
throw e;
}
}
private String chargeProvider(String orderId, int amount) {
try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return "PAID " + orderId + " Rs " + amount + " txn=TXN-" + providerCharges.incrementAndGet();
}
}Output
PAID ORD-101 Rs 1499 txn=TXN-1 PAID ORD-101 Rs 1499 txn=TXN-1 (replayed) PAID ORD-101 Rs 1499 txn=TXN-1 (replayed) PAID ORD-101 Rs 1499 txn=TXN-1 (replayed) PAID ORD-101 Rs 1499 txn=TXN-1 (replayed) PAID ORD-102 Rs 799 txn=TXN-2 Charges sent to provider: 2
Note: In a real service the store is a DB table with a unique index on the key (or Redis SET key NX EX 86400), so it works across instances and restarts.
Implement consistent hashing with virtual nodes. Why is it better than hash % N?
Example
Adding a 4th cache node moves only about a quarter of the keys; hash % N moves most of them.
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;
public class Main {
public static void main(String[] args) {
Ring ring = new Ring(100);
for (String n : List.of("cache-A", "cache-B", "cache-C")) ring.add(n);
Map<String, String> before = new HashMap<>();
for (int i = 1; i <= 1000; i++) before.put("user-" + i, ring.nodeFor("user-" + i));
System.out.println("3 nodes: " + count(before.values()));
ring.add("cache-D");
int moved = 0, movedModulo = 0;
List<String> after = new ArrayList<>();
for (String key : before.keySet()) {
String node = ring.nodeFor(key);
after.add(node);
if (!node.equals(before.get(key))) moved++;
long h = Ring.hash(key);
if (Math.floorMod(h, 3) != Math.floorMod(h, 4)) movedModulo++;
}
System.out.println("4 nodes: " + count(after));
System.out.println("Keys moved (consistent hashing): " + moved + " / 1000");
System.out.println("Keys moved (hash % N): " + movedModulo + " / 1000");
}
static Map<String, Integer> count(Collection<String> nodes) {
Map<String, Integer> c = new TreeMap<>();
for (String n : nodes) c.merge(n, 1, Integer::sum);
return c;
}
}
class Ring {
private final TreeMap<Long, String> ring = new TreeMap<>();
private final int virtualNodes;
Ring(int virtualNodes) { this.virtualNodes = virtualNodes; }
void add(String node) {
for (int i = 0; i < virtualNodes; i++) ring.put(hash(node + "#" + i), node);
}
String nodeFor(String key) {
Map.Entry<Long, String> e = ring.ceilingEntry(hash(key)); // first node clockwise
return (e != null ? e : ring.firstEntry()).getValue();
}
static long hash(String s) {
try {
byte[] d = MessageDigest.getInstance("MD5").digest(s.getBytes(StandardCharsets.UTF_8));
long h = 0;
for (int i = 0; i < 8; i++) h = (h << 8) | (d[i] & 0xff);
return h;
} catch (Exception e) { throw new IllegalStateException(e); }
}
}Output
3 nodes: {cache-A=320, cache-B=350, cache-C=330}
4 nodes: {cache-A=220, cache-B=232, cache-C=260, cache-D=288}
Keys moved (consistent hashing): 288 / 1000
Keys moved (hash % N): 744 / 1000Used for sharding caches (Redis Cluster uses fixed hash slots, a related idea), Kafka-like partition assignment, and sticky load balancing. Virtual nodes spread each server around the ring so the load stays even.
Write a retry helper with exponential backoff and jitter.
Example
The downstream call fails twice with 503 and then succeeds. Jitter uses a seeded Random and the sleep is simulated, so the output is repeatable.
import java.util.Random;
import java.util.function.Supplier;
public class Main {
static int attemptsSeen = 0;
public static void main(String[] args) {
Supplier<String> inventoryCall = () -> {
attemptsSeen++;
if (attemptsSeen <= 2) throw new IllegalStateException("503 Service Unavailable");
return "stock reserved";
};
String result = Retry.withBackoff(inventoryCall, 4, 200, 2000, new Random(42));
System.out.println("Result: " + result + " after " + attemptsSeen + " attempts");
}
}
class Retry {
static <T> T withBackoff(Supplier<T> call, int maxAttempts, long baseMs, long maxMs, Random random) {
for (int attempt = 1; ; attempt++) {
try {
return call.get();
} catch (RuntimeException e) {
if (attempt == maxAttempts) throw e;
long cap = Math.min(maxMs, baseMs * (1L << (attempt - 1))); // 200, 400, 800 ...
long delay = cap / 2 + random.nextLong(cap / 2 + 1); // "equal jitter"
System.out.printf("attempt %d failed (%s), retrying in %d ms%n", attempt, e.getMessage(), delay);
// Thread.sleep(delay) in real code
}
}
}
}Output
attempt 1 failed (503 Service Unavailable), retrying in 102 ms attempt 2 failed (503 Service Unavailable), retrying in 306 ms Result: stock reserved after 3 attempts
Jitter spreads retries from many clients over time, so they do not hit the recovering service at the same instant.
Call three services in parallel for a product page, with a timeout fallback for the slow one.
Example
API composition with CompletableFuture: reviews-service is slow, so its part degrades instead of failing the page.
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) {
long start = System.nanoTime();
var product = CompletableFuture.supplyAsync(() -> call("Product{id=42, name=Boat Rockerz 450}", 100));
var price = CompletableFuture.supplyAsync(() -> call("Price{Rs 1499, 50% off}", 150));
var reviews = CompletableFuture.supplyAsync(() -> call("Reviews{4.3 stars, 12k}", 2000))
.completeOnTimeout("Reviews{unavailable}", 300, TimeUnit.MILLISECONDS);
String page = CompletableFuture.allOf(product, price, reviews)
.thenApply(v -> product.join() + " | " + price.join() + " | " + reviews.join())
.join();
long ms = (System.nanoTime() - start) / 1_000_000;
System.out.println(page);
System.out.println("Page built in under 1 s: " + (ms < 1000));
}
static String call(String response, long latencyMs) {
try { Thread.sleep(latencyMs); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return response;
}
}Output
Product{id=42, name=Boat Rockerz 450} | Price{Rs 1499, 50% off} | Reviews{unavailable}
Page built in under 1 s: trueTip: Total latency is the slowest call (bounded by the timeout), not the sum. In Spring, run these with a dedicated executor or virtual threads, not the common pool.