What staff-level code review grades
Billing · flags · coupons
Locks · races · interrupts
IDOR · SQLi · secrets
N+1 · fanout · cache
Idempotency · DLQ · retries
Contracts · pure cores
These guidelines combine planted interview defects (checkout, locks, leaderboards, documents, feeds/cache, flags, webhooks, agents) with public practice from Google’s code review guide, Spring Boot security checklists, distributed-systems review habits, and Kafka producer/consumer norms.
Staff principles — with snippets
Lead with money/security — not structure
Order checkout(String userId, String code, boolean isGift, boolean express) {
// … 80 lines: load, validate, price, tax, ship, save, email …
double total = subtotal * 1.08; // money bug
if (code == "SAVE10") total *= 0.9; // == bug
return db.save(total);
}
Name the failure mode
void onWebhook(Event e) {
payments.charge(e.orderId, e.amount);
}
One systemic fix > pile of locals
Document get(long id) { return db.find(id); }
Document update(long id, Body b) { db.update(id, b); return db.find(id); }
void delete(long id) { db.delete(id); }
// three local “add owner check” comments vs one design comment
Ask when intent is unclear
double calculateDiscount(String code, double subtotal, boolean isGift, boolean express) {
if ("SAVE10".equals(code) && subtotal > 0) return subtotal * 0.1;
return 0; // isGift / express never read
}
Single-flight / pure pricing / shared guard — fix shapes
// Systemic fix shapes to propose
Money price(Cart cart); // pure pricing
void requireOwner(Req req, Doc doc); // shared ownership
inflight.computeIfAbsent(key, k -> load); // single-flight cache
Comment priority order — with snippets
Blocker examples
Wrong money (double)
double tax = total * 0.08; // BLOCKER — mis-bills
Lock race / double-charge / RCE
if (redis.get(key) == null) redis.set(key, token); // BLOCKER race
charge(event); // every webhook delivery — BLOCKER without idempotency
new ProcessBuilder("/bin/sh", "-c", modelCmd).start(); // BLOCKER RCE
Cache stampede / forever map / unbounded batch
cache.put(key, feed); // never evicts — BLOCKER leak + stale
// N concurrent misses all loader.load(key) — BLOCKER stampede
userIds.forEach(id -> supplyAsync(() -> buildFeed(id), cachedPool)); // BLOCKER herd
High examples
if (now - fetchedAt < ttlMs) refresh(); // HIGH — inverted TTL
for (User u : users) db.findOrdersByUser(u.id); // HIGH N+1
HttpClient c = HttpClient.newHttpClient(); // HIGH per call
if (user == null) return null; else throw ...; // HIGH inconsistent contract
Medium examples
Order checkout(...) { /* load, price, save, email — god method */ }
while (!acquire()) { /* spin — missing backoff */ }
public Map<String, Boolean> cache; // leaky abstraction
Nit examples
if (subtotal > 5000) ... // NIT — name FREE_SHIPPING_THRESHOLD_CENTS
String env = "prod"; // NIT — Environment enum
// extract helper for readability after blockers
How to run a staff code-review interview (45–60 min)
Entry · data flow · side effects
Billing · IDOR · locks
N+1 · stampede · retries
Contracts · tests · nits
Blockers → high → nits
- Minute 0–5 — map the system. Find the public entry (controller/handler/main). Trace one happy path aloud: inputs → storage → outbound calls → response. List side effects (charge, email, cache write, lock).
- Minute 5–15 — correctness & safety first. Money math, authz/IDOR, SQL injection, check-then-set locks, idempotency on webhooks. Say each finding as: symptom → failure mode → fix shape → severity.
- Minute 15–35 — load & ops. N+1, sequential fanout, unbounded pools, cache stampede/TTL, timeouts/retries, DLQ, logging secrets.
- Minute 35–45 — design & maintainability. God methods, inconsistent null/throw, leaky SDK fields, missing tests for the risky path. Keep these after blockers.
- Last 5 — close. “Three blockers: … Two highs: … Nits: … I’d merge only after blockers.” Ask one clarifying question if intent was ambiguous.
The finding template — worked example
Snippet
class RedisLock {
boolean acquire(String key, String token, long ttlMs) {
if (jedis.get(key) == null) { // (1) location
jedis.set(key, token); // separate round-trip
jedis.pexpire(key, ttlMs);
return true;
}
return false;
}
void release(String key) {
jedis.del(key); // no token compare
}
}
// Fix shape
String ok = jedis.set(key, token, SetParams.setParams().nx().px(ttlMs));
return ok != null;
// release: Lua if get==token then del
Second worked example — IDOR → systemic guard
Document get(long id) { return db.findById(id); } // no owner check
void delete(long id) { db.delete(id); } // no owner check
Failure-mode taxonomy (hunt in this order)
Memorize this ladder. In the room, walk it top-down against whatever snippet they give you.
- Money / truth: double/float cash, string
==, false vs null flags, coupon/tax literals, wrong cache key dimensions. - Safety / exclusivity: check-then-set locks, unsafe unlock, missing single-flight, TOCTOU on files.
- Security: IDOR, SQLi, path traversal, secrets in repo, unsigned webhooks, shell/RCE, model-supplied userId.
- Reliability: no idempotency, swallowed markPaid, receipt on critical path, empty tool errors, dropped DLQ jobs.
- Scale / resources: N+1, sequential independent I/O, unbounded pools/batch, forever cache, PBKDF2-per-item, per-call HttpClient, huge bodies.
- Contracts / shape: null vs throw mix, positional booleans, public mutable fields, god methods — after the above.
One-liner snippet per rung
// Money/truth
double tax = total * 0.08;
if (code == "SAVE10") ...
return (value != null && value) ? value : def; // false collapsed
// Safety
if (get(k)==null) set(k,t);
jedis.del(k);
// Security
db.findById(id); // no owner
query("... id=" + id);
/bin/sh -c modelCmd;
// Reliability
charge(e); // no dedupe
catch (Exception e) { return ""; } // tool errors
// Scale
for (s : sources) fetch(s);
new HttpClient() per call;
cache.put forever;
// Contracts/shape (last)
checkout does everything; public Map cache;
Hire bar — what interviewers write after you leave
- Did I find ≥1 money/correctness bug if money exists in the snippet?
- Did I find ≥1 authz/injection/secret issue if there’s an HTTP surface?
- Did I find ≥1 race/idempotency/timeout issue if there’s concurrency or webhooks?
- Did I find ≥1 scale/resource issue if there’s fanout, cache, or batch APIs?
- Did I give a ranked closing summary?
What a lean-no-hire review sounds like vs strong
Company formats you’ll see
Verbal bank — high-signal phrases
Mapped to a snippet
// “Failure mode is double-charge / blast radius is money”
charge(event); // no wasProcessed(event.id)
// “Check-then-act — needs one atomic op”
if (get(key)==null) set(key, token);
// “At-least-once → need idempotency”
onWebhook → charge every delivery
// “Pure function so we can unit-test without fakes”
Money price(Cart c);
// “Systemic fix is shared ownership guard”
requireOwner(req, doc);
// “Severity blocker — wouldn’t merge”
// “Is this unfinished intent or dead code?”
Money & numeric correctness
Never compute money in double
Smell
// BROKEN — double money
double tax = total * 0.08;
double discount = subtotal * 0.1;
order.setTotal(total + tax - discount);
email.send(order.getTotal()); // may print 19.999999999998
Fix
// FIX — integer cents + explicit rounding at the boundary
long taxCents = Math.round(totalCents * TAX_RATE_BPS / 10_000.0);
// or BigDecimal with RoundingMode.HALF_UP at the API boundary
long payable = totalCents + taxCents - discountCents;
order.setTotalCents(payable);
Coupon / string checks use equals, not ==
Smell
// BROKEN
if (code == "SAVE10") return subtotal * 0.1;
Fix
// FIX — literal first so null code is safe
if ("SAVE10".equals(code)) return rateOf(code) * subtotal;
Don’t collapse false with missing (feature flags)
Smell
// BROKEN — false and missing look the same
return (value != null && value) ? value : defaultValue;
Fix
// FIX — distinguish null from false
return value == null ? defaultValue : value;
Name money literals (nit)
Smell
if (subtotal > 5000) shipping = 0;
else shipping = 1500;
double tax = total * 0.08;
Fix
private static final long FREE_SHIPPING_THRESHOLD_CENTS = 5_000;
private static final long STANDARD_SHIPPING_CENTS = 1_500;
private static final double TAX_RATE = 0.08;
Structure, contracts & readability
checkout() must not do everything
Smell
Order checkout(userId) {
User u = db.user(userId);
Cart c = db.cart(userId);
// validate… subtotal… coupon… tax… shipping…
Order o = db.save(...);
mailer.send(o);
return o;
}
Fix
Money price(Cart cart) { /* pure: subtotal, coupon, tax, ship */ }
Order checkout(userId) {
User u = requireUser(userId);
Cart c = requireCart(userId);
Money priced = price(c);
Order o = db.save(u, priced);
mailer.send(o); // side effect after persist
return o;
}
One failure contract — don’t mix null and throws
Smell
if (user == null) return null;
if (cart == null) return null;
if (cart.isEmpty()) throw new IllegalStateException("empty");
Fix
User user = userRepo.find(id).orElseThrow(() -> new NotFound("user"));
Cart cart = cartRepo.find(id).orElseThrow(() -> new NotFound("cart"));
if (cart.isEmpty()) throw new InvalidCart("empty");
Guard clauses beat six-level nesting
Smell
if (user != null) {
if (cart != null) {
if (!cart.isEmpty()) {
// happy path buried
}
}
}
Fix
if (user == null) throw new NotFound("user");
if (cart == null) throw new NotFound("cart");
if (cart.isEmpty()) throw new InvalidCart("empty");
// happy path at column 0
Replace coupon if-chains with a table
Smell
if ("SAVE10".equals(code) && subtotal > 0) return subtotal * 0.10;
if ("SAVE20".equals(code) && subtotal > 0) return subtotal * 0.20;
if ("SAVE30".equals(code) && subtotal > 0) return subtotal * 0.30;
Fix
private static final Map<String, Double> RATES = Map.of(
"SAVE10", 0.10, "SAVE20", 0.20, "SAVE30", 0.30);
Double rate = RATES.get(code);
if (rate == null || subtotal <= 0) return 0;
return subtotal * rate;
Locks, workers & interruption
Lock acquire must be atomic (SET NX PX)
Smell
// BROKEN
if (redis.get(key) == null) {
redis.set(key, token, ttl);
return true;
}
return false;
Fix
// FIX — single atomic op
String ok = redis.set(key, token, SetParams.setParams().nx().px(ttlMs));
return ok != null; // null => someone else holds it
Release only if you still own the token
Smell
// BROKEN
redis.del(key);
Fix
// FIX — atomic compare-and-delete (Lua)
// if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
Don’t busy-spin waiting for a lock
Smell
while (!acquire()) {
// spin
}
Fix
while (!acquire()) {
try {
Thread.sleep(backoffMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
backoffMs = Math.min(max, backoffMs * 2);
}
Don’t swallow InterruptedException in a broad catch
Smell
try {
doJob();
} catch (Exception e) {
attempts++;
}
Fix
try {
doJob();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (JobException e) {
log.warn("job failed", e);
attempts++;
}
Failed jobs must not vanish after retries
Smell
} catch (Exception e) {
attempt++;
}
// loop ends — lock released — silence
Fix
} catch (Exception e) {
log.error("job {} failed attempt {}", jobId, attempt, e);
if (attempt >= maxAttempts) {
deadLetter.enqueue(jobId, e);
break;
}
attempt++;
}
Performance & ranking
Don’t load the whole users table to rank
Smell
List<User> users = db.findAllUsers();
for (User u : users) {
List<Order> orders = db.findOrdersByUser(u.getId()); // N+1
...
}
Fix
-- Prefer DB-side aggregation
SELECT user_id, SUM(amount_cents) AS spent
FROM orders
GROUP BY user_id
ORDER BY spent DESC
LIMIT 100;
N+1: batch by foreign key
Smell
for (Order o : orders) {
items.addAll(db.findItemsByOrder(o.getId()));
}
Fix
Map<Long, List<Item>> byOrder = db.findItemsByOrderIds(orderIds);
for (Order o : orders) {
items.addAll(byOrder.getOrDefault(o.getId(), List.of()));
}
Don’t cast long differences to int for sort
Smell
users.sort((a, b) -> (int) (b.getSpent() - a.getSpent()));
Fix
users.sort((a, b) -> Long.compare(b.getSpent(), a.getSpent()));
Rank from sorted position — not O(n²) rescans
Smell
for (Row r : sorted) {
int rank = 1;
for (Row o : sorted) if (o.spent > r.spent) rank++;
r.rank = rank;
}
Fix
for (int i = 0; i < sorted.size(); i++) {
// handle ties explicitly if needed
sorted.get(i).rank = i + 1;
}
HashMap iteration is not a stable tie-break
Smell
for (var e : counts.entrySet()) {
if (e.getValue() > bestCount) { best = e.getKey(); bestCount = e.getValue(); }
}
Fix
// Explicit tie-break: higher count, then smaller product id
best = counts.entrySet().stream()
.max(Comparator
.comparingLong(Map.Entry<String, Long>::getValue)
.thenComparing(Map.Entry::getKey, Comparator.reverseOrder()))
.map(Map.Entry::getKey)
.orElse(null);
Feed fanout, cache & request-path cost
Independent upstreams must fan out concurrently
Smell
List<Item> fanOut(List<Source> sources) {
List<Item> all = new ArrayList<>();
for (Source s : sources) {
all.addAll(fetchFromSource(s)); // sequential
}
return all;
}
Fix
List<CompletableFuture<List<Item>>> futures = sources.stream()
.map(s -> CompletableFuture.supplyAsync(() -> fetchFromSource(s), upstreamPool))
.toList();
return futures.stream().map(CompletableFuture::join).flatMap(List::stream).toList();
Bound batch fanout — no unbounded cached thread pool
Smell
for (String userId : userIds) { // unbounded
futures.add(CompletableFuture.supplyAsync(
() -> buildFeed(userId), Executors.newCachedThreadPool()));
}
Fix
if (userIds.size() > MAX_BATCH) throw new BadRequest("batch too large");
ExecutorService pool = Executors.newFixedThreadPool(MAX_IN_FLIGHT_USERS);
try {
List<CompletableFuture<Feed>> futures = userIds.stream()
.map(id -> CompletableFuture.supplyAsync(() -> buildFeed(id), pool))
.toList();
return futures.stream().map(CompletableFuture::join).toList();
} finally {
pool.shutdown();
}
Cache keys must include every input that changes the result
Smell
String cacheKey(String userId, String region) {
return "feed:" + userId; // region unused — cross-region bleed
}
Fix
String cacheKey(String userId, String region) {
return "feed:" + userId + ":" + region;
}
Don’t replace cache-miss null with an empty list
Smell
List<Item> getCached(String key) {
List<Item> v = map.get(key);
return v == null ? List.of() : v; // miss looks like empty hit
}
Fix
Optional<List<Item>> getCached(String key) {
return Optional.ofNullable(map.get(key));
}
// getOrLoad: getCached(key).orElseGet(() -> loadAndStore(key));
In-memory cache needs TTL + bounded eviction
Smell
cache.put(key, new Entry(feed, System.currentTimeMillis()));
// nothing ever evicts; storedAt unread
Fix
// e.g. Caffeine: maximumSize + expireAfterWrite
LoadingCache<String, Feed> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(5))
.build(this::loadFeed);
Single-flight loads — prevent cache stampedes
Smell
Feed getOrLoad(String key) {
Feed hit = cache.get(key);
if (hit != null) return hit;
Feed loaded = loader.load(key); // every miss does full work
cache.put(key, loaded);
return loaded;
}
Fix
ConcurrentHashMap<String, CompletableFuture<Feed>> inflight = ...;
Feed getOrLoad(String key) {
Feed hit = cache.get(key);
if (hit != null) return hit;
CompletableFuture<Feed> fut = inflight.computeIfAbsent(key, k ->
CompletableFuture.supplyAsync(() -> loader.load(k))
.whenComplete((v, e) -> {
if (v != null) cache.put(k, v);
inflight.remove(k);
}));
return fut.join();
}
No PBKDF2 (or other slow KDF) on the request-path scorer
Smell
byte[] affinityHash(String itemId) {
return pbkdf2(itemId, salt, 100_000); // per item, sync
}
Fix
long affinityScore(String itemId, String userId) {
return murmur3(userId + ":" + itemId); // cheap, non-crypto
}
Reuse one HttpClient — don’t new it per call
Smell
List<Item> fetchFromSource(Source s) {
HttpClient client = HttpClient.newHttpClient(); // per call
return client.send(...);
}
Fix
private final HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2))
.build();
List<Item> fetchFromSource(Source s) {
return http.send(...);
}
Cap upstream response body size
Smell
HttpResponse<String> res = http.send(req, BodyHandlers.ofString());
Fix
HttpResponse<String> res = http.send(req, BodyHandlers.fromLineSubscriber(
new BoundedStringSubscriber(MAX_BODY_BYTES)));
// or read with InputStream and abort when count > MAX
Security & authorization
IDOR: every read/update/delete checks ownership
Smell
Document doc = db.findById(id);
return doc; // no owner check
Fix
Document doc = db.findById(id);
requireOwner(req.getUserId(), doc);
return doc;
Parameterize SQL — never concatenate ids
Smell
db.query("SELECT * FROM docs WHERE id = '" + id + "'");
Fix
db.query("SELECT * FROM docs WHERE id = ?", id);
Authorize before mutate
Smell
db.update(id, body);
if (!owns(req, id)) return 403;
Fix
Document doc = db.findById(id);
requireOwner(req.getUserId(), doc);
db.update(id, body);
Secrets never live in source
Smell
static final String SESSION_SECRET = "super-secret-value";
static final String ANTHROPIC_KEY = "sk-ant-...";
Fix
String secret = Objects.requireNonNull(System.getenv("SESSION_SECRET"), "SESSION_SECRET");
Don’t log Authorization headers
Smell
log.info("incoming {}", req.getHeader("authorization"));
Fix
log.info("incoming user={} path={}", req.getUserId(), req.getPath());
Clamp pagination limits
Smell
int limit = Integer.parseInt(params.get("limit"));
Fix
int limit = parseLimit(params.get("limit"), /*default*/20, /*max*/100);
Webhooks, payments & reliability
Verify provider signature before acting
Smell
Event event = parse(body);
charge(event); // signature ignored
Fix
if (!verifyWebhookSignature(body, signatureHeader)) {
return 401;
}
Event event = parse(body);
handle(event);
Branch on event type — don’t charge every delivery
Smell
// type parsed then ignored
chargeCustomer(event);
Fix
switch (event.getType()) {
case "payment_intent.succeeded" -> settle(event);
default -> { /* 200-ack and ignore */ }
}
Idempotency: at-least-once delivery will double-charge
Smell
charge(event); // every delivery
Fix
if (db.events.wasProcessed(event.getId())) return true;
charge(event, /*idempotencyKey*/ event.getId());
db.events.markProcessed(event.getId());
Null-check findById before dereference
Smell
Order order = db.orders.findById(event.orderId);
charge(order.getCustomerId(), order.getAmountCents());
Fix
Order order = db.orders.findById(event.orderId);
if (order == null) {
log.warn("unknown order {}", event.orderId);
return true; // ack — don't infinite-retry
}
Don’t report success if markPaid failed
Smell
try { db.markPaid(orderId); }
catch (Exception e) { log.error(e); }
sendReceipt(order);
return true;
Fix
db.markPaid(orderId); // let failure fail the webhook for retry/reconcile
sendReceiptBestEffort(order);
return true;
Receipt email off the critical path
Smell
charge();
sendReceipt(); // throws → webhook fails → provider retries → charge again
Fix
charge();
markPaid();
try { sendReceipt(); } catch (Exception e) { log.warn("receipt", e); }
Timeouts, retryable vs not, backoff, keep the cause
Smell
for (int i = 0; i < 5; i++) {
try { return http.send(req); }
catch (Exception e) { last = e; }
}
throw new RuntimeException("payment failed");
Fix
HttpRequest req = HttpRequest.newBuilder(uri).timeout(Duration.ofSeconds(5)).build();
for (int i = 0; i < 5; i++) {
try {
HttpResponse<String> res = http.send(req, BodyHandlers.ofString());
if (res.statusCode() < 500 && res.statusCode() != 429) {
if (res.statusCode() >= 400) throw new NonRetryable(res);
return res;
}
} catch (NonRetryable e) { throw e; }
catch (Exception e) { last = e; sleepWithJitter(i); }
}
throw new RuntimeException("payment failed", last);
SDK & cache design
Don’t expose a public mutable cache field
Smell
public Map<String, Boolean> cache = new HashMap<>();
Fix
private final Map<String, Boolean> cache = new ConcurrentHashMap<>();
// expose getFlag / invalidate only
Staleness comparison is easy to invert
Smell
if (now - fetchedAt < ttlMs) refresh(); // backwards
Fix
if (now - fetchedAt > ttlMs) refresh();
refresh() must drop keys removed upstream
Smell
for (var e : remote.entrySet()) cache.put(e.getKey(), e.getValue());
Fix
Map<String, Boolean> next = remote.fetchAll();
cache.keySet().retainAll(next.keySet());
cache.putAll(next);
Empty catch in refresh hides outages
Smell
try { refresh(); } catch (Exception e) { /* empty */ }
Fix
try { refresh(); }
catch (IOException | JsonException e) {
log.error("flag refresh failed", e);
}
Avoid two positional booleans on public APIs
Smell
getFlag("checkout_v2", false, true);
Fix
getFlag("checkout_v2", FlagOptions.defaults()
.defaultValue(false)
.refreshIfStale(true));
One missing-key contract across the client
Smell
boolean getFlag(String k, boolean d) { ... return d; }
JsonNode getConfig(String k) { if (missing) throw ...; }
Fix
// Same absence policy on both — e.g. both Optional or both defaulted
Optional<Boolean> getFlag(String k);
Optional<JsonNode> getConfig(String k);
Agent loops & tool safety
Bound the agent loop
Smell
while (true) {
Message m = model.complete(history);
if (m.toolCalls.isEmpty()) break;
history.addAll(execute(m.toolCalls));
}
Fix
for (int turn = 0; turn < MAX_TURNS; turn++) {
Message m = model.complete(history);
if (m.toolCalls.isEmpty()) return m;
history.addAll(execute(m.toolCalls));
}
return gaveUpAfter(MAX_TURNS);
Match tool results by id, not array index
Smell
List<Result> results = executeFiltered(calls);
for (int i = 0; i < calls.size(); i++) {
attach(calls.get(i).id, results.get(i)); // drifts after filter
}
Fix
Map<String, Result> byId = executeReturningIds(calls);
for (ToolCall c : calls) {
attach(c.id, byId.get(c.id));
}
System prompt goes in the system field
Smell
messages.add(0, userMessage(systemPrompt));
Fix
client.complete(SystemParam.of(systemPrompt), messages);
Contain file paths — no traversal
Smell
Path p = Paths.get(workspaceRoot(), inputPath);
Files.readString(p);
Fix
Path root = Path.of(workspaceRoot()).toRealPath();
Path p = root.resolve(inputPath).normalize().toRealPath();
if (!p.startsWith(root)) throw new SecurityException("path escapes workspace");
Never /bin/sh -c a raw model string
Smell
new ProcessBuilder("/bin/sh", "-c", modelCommand).start();
Fix
// Run inside locked-down sandbox + allowlist / approval gate
sandbox.runAllowlisted(modelCommand);
Cap tool output and manage history
Smell
return Files.readString(path); // entire file into context
Fix
String out = Files.readString(path);
if (out.length() > MAX) return out.substring(0, MAX) + "\n…[truncated]";
return out;
Don’t swallow tool errors as an empty string
Smell
String runOne(ToolCall call) {
try {
return HANDLERS.get(call.name).run(call.input);
} catch (Exception e) {
return ""; // model sees "nothing happened"
}
}
Fix
String runOne(ToolCall call) {
try {
return HANDLERS.get(call.name).run(call.input);
} catch (Exception e) {
log.warn("tool {} failed: {}", call.name, e.toString());
return "Error: " + e.getMessage(); // model can recover
}
}
Never trust the model for authorization identity
Smell
@Tool
Order search(String userId, String query) { // userId from model
return db.find(userId, query);
}
Fix
Order search(AuthUser user, SearchArgs args) { // user from session
return db.find(user.id(), args.query());
}
Treat tool/web/file content as untrusted (prompt injection)
Smell
history.add(userMessage(downloadedPageHtml)); // treated as instructions
runWhateverToolsTheModelAsks();
Fix
history.add(toolResult(sanitize(downloadedPageHtml))); // data, not commands
if (isIrreversible(tool)) requireHumanApproval(tool);
validateArgs(schema, tool.args); // allowlist + types
Google review standard & Java hygiene
Prefer Optional / clear absence over nullable returns in new APIs
Smell
User find(String id) { return map.get(id); } // null = miss
Fix
Optional<User> find(String id) { return Optional.ofNullable(map.get(id)); }
Don’t mute exceptions or return magic values
Smell
try { return parse(json); }
catch (Exception e) { return null; }
Fix
try { return parse(json); }
catch (JsonProcessingException e) {
throw new InvalidPayload("order json", e);
}
Close resources with try-with-resources
Smell
InputStream in = Files.newInputStream(path);
return new String(in.readAllBytes()); // leak on exception paths
Fix
try (InputStream in = Files.newInputStream(path)) {
return new String(in.readAllBytes());
}
Java depth interviewers expect at staff
equals/hashCode must agree; never mutate a key in a map
Smell
class UserId {
int id;
public boolean equals(Object o) { return id == ((UserId) o).id; }
// missing hashCode — HashMap breaks
}
Fix
record UserId(int id) {} // equals/hashCode correct; immutable
Don’t publish mutable internal state
Smell
public List<Item> items() { return items; } // mutable escape
Fix
public List<Item> items() { return List.copyOf(items); }
Thread interruption is a control signal
Smell
} catch (InterruptedException e) {
// ignore
}
Fix
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw e; // or break out of the worker loop
}
Spring Boot review checklist
Actuator exposure is a credential exfil risk
Smell
# BROKEN for prod
management.endpoints.web.exposure.include=*
Fix
management.endpoints.web.exposure.include=health,info,prometheus
management.server.port=8081
# + SecurityFilterChain requiring ADMIN on /actuator/**
Validate DTOs — don’t bind entities at the edge
Smell
@PostMapping("/orders")
Order create(@RequestBody Order entity) { // no @Valid, persistence type at edge
return repo.save(entity);
}
Fix
@PostMapping("/orders")
OrderResponse create(@Valid @RequestBody CreateOrderRequest req) {
return service.create(req);
}
ConfigurationProperties over magic @Value strings
Smell
@Value("${feed.upstream.timeout-ms}") long timeout;
Fix
@ConfigurationProperties(prefix = "feed.upstream")
public record UpstreamProps(@Min(1) long timeoutMs, @NotBlank String baseUrl) {}
Spring Boot — extra staff checks
Transactional boundaries: no remote I/O inside @Transactional
Smell
@Transactional
void checkout(Order o) {
repo.save(o);
payments.charge(o); // remote call inside TX
mail.send(o);
}
Fix
@Transactional
void checkout(Order o) {
repo.save(o);
outbox.enqueue(OrderPaid.of(o));
}
// separate processor: charge + mail with idempotency
Open-in-view / lazy loads on the request thread
Smell
return repo.findById(id); // entity graph escapes to Jackson
Fix
return repo.findSummaryById(id); // DTO/projection
Method security + ownership, not role-only
Smell
@PreAuthorize("isAuthenticated()")
Document get(Long id) { return repo.findById(id).orElseThrow(); }
Fix
@PreAuthorize("@docs.owner(authentication, #id)")
Document get(Long id) { return repo.findById(id).orElseThrow(); }
Java & Spring Boot review bible — how to use this
Authz · secrets · Actuator
TX · N+1 · pagination
Timeouts · CB · SSRF
Ack · outbox · DLQ
Validation · errors
Equals · resources
Spring Security bible (with samples)
SecurityFilterChain must not end in permitAll
Smell
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().permitAll()); // BLOCKER
Fix
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health", "/actuator/info").permitAll()
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated());
Mass assignment / binding sensitive fields
Smell
@PatchMapping("/users/{id}")
User update(@PathVariable long id, @RequestBody User body) {
// client can set body.role = "ADMIN"
return repo.save(body);
}
Fix
@PatchMapping("/users/{id}")
UserResponse update(@PathVariable long id, @Valid @RequestBody UpdateUserRequest req) {
return service.updateProfile(id, req.name(), req.email()); // no role field
}
CSRF: know your API model
Smell
http.csrf(csrf -> csrf.disable()); // with cookie session — BLOCKER
Fix
// Cookie session: keep CSRF (or CookieCsrfTokenRepository)
// Bearer-only API: csrf.disable() OK if no cookie session auth
http.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS));
JWT: pin algorithm and claims
Smell
Jwts.parserBuilder().build()
.parseClaimsJws(token); // weak — no key/alg/iss discipline
Fix
Jwts.parserBuilder()
.requireIssuer("https://auth.example")
.requireAudience("orders-api")
.setSigningKey(publicKey) // asymmetric; reject alg none
.build()
.parseClaimsJws(token);
Log masking for tokens and PII
Smell
log.info("auth={}", request.getHeader("Authorization"));
log.info("user={}", user); // may dump password hash / PII
Fix
log.info("userId={} path={}", userId, path);
// structured logger with field denylist for authorization, password, pan
CORS must list origins in production
Smell
config.addAllowedOriginPattern("*");
config.setAllowCredentials(true);
Fix
config.setAllowedOrigins(List.of("https://app.example.com"));
config.setAllowCredentials(true);
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
Spring Data / JPA bible (with samples)
Pagination is mandatory on list endpoints
Smell
@GetMapping("/orders")
List<Order> list() {
return repo.findAll(); // BLOCKER at scale
}
Fix
@GetMapping("/orders")
Page<OrderSummary> list(@PageableDefault(size = 20) Pageable pageable) {
Pageable safe = PageRequest.of(
pageable.getPageNumber(),
Math.min(pageable.getPageSize(), 100),
pageable.getSort());
return repo.findSummaries(safe);
}
Avoid N+1: join fetch / entity graph / DTO query
Smell
List<Order> orders = repo.findAll();
for (Order o : orders) {
o.getLines().size(); // N+1
}
Fix
@Query("select o from Order o join fetch o.lines where o.customerId = :id")
List<Order> findWithLines(long id);
// or projection interface / DTO constructor expression
readOnly transactions for queries
Smell
@Transactional
public Order get(long id) { return repo.findById(id).orElseThrow(); }
Fix
@Transactional(readOnly = true)
public Order get(long id) { return repo.findById(id).orElseThrow(); }
Optimistic locking for concurrent updates
Smell
account.setBalance(account.getBalance() - amount);
repo.save(account); // lost update under concurrency
Fix
@Entity
class Account {
@Version long version;
// …
}
// or: update account set balance = balance - :amt where id = :id and balance >= :amt
Network & HTTP clients bible (with samples)
WebClient: set connect + response timeouts
Smell
WebClient.create(baseUrl)
.get().uri("/x").retrieve().bodyToMono(String.class)
.block(); // no timeouts
Fix
HttpClient netty = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000)
.responseTimeout(Duration.ofSeconds(5));
WebClient client = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(netty))
.baseUrl(baseUrl)
.build();
RestTemplate: never ship without timeouts
Smell
return new RestTemplate(); // infinite wait risk
Fix
RestTemplate rt = new RestTemplateBuilder()
.setConnectTimeout(Duration.ofSeconds(2))
.setReadTimeout(Duration.ofSeconds(5))
.build();
Retry only idempotent + transient failures
Smell
for (int i = 0; i < 5; i++) {
rest.postForEntity(url, chargeRequest, Void.class); // duplicate money
}
Fix
@Retry(name = "payments", ignoreExceptions = HttpClientErrorException.class)
@CircuitBreaker(name = "payments")
public void charge(IdempotentCharge req) {
client.post(req); // Idempotency-Key header required
}
SSRF: allowlist outbound hosts
Smell
String url = request.getUrl(); // user controlled
return webClient.get().uri(url).retrieve().bodyToMono(String.class);
Fix
URI uri = URI.create(request.getUrl());
if (!ALLOWED_HOSTS.contains(uri.getHost())) {
throw new BadRequest("host not allowed");
}
return webClient.get().uri(uri).retrieve().bodyToMono(String.class);
Propagate correlation / trace headers
Smell
webClient.get().uri("/deps/x").retrieve()...
Fix
webClient.get()
.uri("/deps/x")
.header("traceparent", TraceContext.current())
.header("X-Request-Id", MDC.get("cid"))
.retrieve()...
TLS and hostname verification stay on
Smell
sslContext.init(null, trustAllCerts, new SecureRandom()); // BLOCKER
Fix
// Use platform trust store / pinned certs; never trust-all in shared modules
Async & Kafka bible (with samples)
Spring Kafka: ack mode and error handler
Smell
@KafkaListener(topics = "orders")
void onMessage(OrderEvent e) {
billing.charge(e); // if this throws forever, partition stalls / dupes depending on config
}
Fix
@KafkaListener(topics = "orders")
void onMessage(OrderEvent e, Acknowledgment ack) {
billing.chargeIdempotent(e);
ack.acknowledge();
}
// Container factory: DefaultErrorHandler with DeadLetterPublishingRecoverer + fixed backoffs
Idempotent consumer with business key
Smell
void onMessage(OrderEvent e) {
repo.save(Order.from(e)); // duplicate insert on redelivery
}
Fix
void onMessage(OrderEvent e) {
if (!processed.markIfNew(e.eventId())) return;
repo.upsert(Order.from(e));
}
Transactional outbox with Spring
Smell
@Transactional
void place(Order o) {
orderRepo.save(o);
kafka.send("orders", OrderCreated.of(o)); // dual-write
}
Fix
@Transactional
void place(Order o) {
orderRepo.save(o);
outboxRepo.save(OutboxMessage.of("orders", OrderCreated.of(o)));
}
// @Scheduled / Debezium publisher drains outbox
Producer config in Spring Boot
Smell
spring.kafka.producer.acks=1
# enable.idempotence unset
Fix
spring.kafka.producer.acks=all
spring.kafka.producer.properties.enable.idempotence=true
spring.kafka.producer.properties.min.insync.replicas=2
@Async executor must be bounded
Smell
@EnableAsync
// no AsyncConfigurer — unbounded growth risk
Fix
@Bean
TaskExecutor appExecutor() {
ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor();
ex.setCorePoolSize(8);
ex.setMaxPoolSize(16);
ex.setQueueCapacity(500);
ex.setRejectedExecutionHandler(new CallerRunsPolicy());
ex.initialize();
return ex;
}
Don’t @Transactional across Kafka send + remote HTTP
Smell
@Transactional
void settle(Order o) {
repo.markPaid(o.id());
kafka.send(...);
http.notifyPartner(...); // still in TX
}
Fix
@Transactional
void settle(Order o) {
repo.markPaid(o.id());
outbox.save(...);
}
// after commit: publisher + async notify with idempotency
Spring API & observability bible (with samples)
@Valid + ProblemDetail errors
Smell
@PostMapping
Order create(@RequestBody Order o) { return service.create(o); }
Fix
@PostMapping
ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest req) {
return ResponseEntity.status(201).body(service.create(req));
}
@ExceptionHandler
ProblemDetail onValidation(MethodArgumentNotValidException ex) { ... }
Idempotency-Key on create/charge endpoints
Smell
@PostMapping("/charges")
ChargeResponse charge(@RequestBody ChargeRequest req) {
return payments.charge(req);
}
Fix
@PostMapping("/charges")
ChargeResponse charge(
@RequestHeader("Idempotency-Key") String key,
@Valid @RequestBody ChargeRequest req) {
return payments.chargeOnce(key, req);
}
Micrometer timers on outbound dependencies
Smell
partnerClient.fetch(id);
Fix
return Timer.builder("http.client.partner")
.tag("method", "GET")
.register(meterRegistry)
.record(() -> partnerClient.fetch(id));
Microservice & distributed-systems review
Every outbound call needs a finite timeout
Smell
RestClient.get().uri(url).retrieve().body(String.class); // default may block too long
Fix
RestClient.builder()
.requestFactory(factoryWithConnectAndReadTimeouts(200, 800))
.build();
Retries without idempotency double side effects
Smell
for (int i = 0; i < 3; i++) {
payments.charge(orderId, amount); // duplicate charge on retry
}
Fix
payments.charge(orderId, amount, IdempotencyKey.of(requestId));
Propagate deadlines — don’t let downstream retries exceed the parent budget
Smell
// each hop retries independently with full budget
Fix
Duration budget = remainingDeadline(ctx);
callDownstream(ctx.withDeadline(budget.minus(safetyMargin)));
Microservices — extra staff checks
Correlation and baggage on every hop
Smell
log.info("paid order {}", orderId); // no request id
Fix
log.info("paid order {} correlationId={}", orderId, MDC.get("cid"));
// HTTP: outgoing header; Kafka: headers.add("cid", ...)
Backpressure and load shedding beat infinite queues
Smell
executor = new ThreadPoolExecutor(..., new LinkedBlockingQueue<>()); // unbounded
Fix
executor = new ThreadPoolExecutor(
n, n, 60, SECONDS, new ArrayBlockingQueue<>(1000),
new ThreadPoolExecutor.AbortPolicy());
Multi-tenant / auth scopes on service-to-service calls
Smell
docs.get(docId); // uses god-mode service account
Fix
docs.get(docId, TenantContext.required()); // enforced downstream
Kafka producer & consumer review
Idempotent producer for anything you retry
Smell
props.put("acks", "1");
props.put("retries", 5);
// enable.idempotence unset / false → duplicate risk
Fix
props.put("enable.idempotence", "true"); // forces acks=all, safe retries
props.put("acks", "all");
Commit offsets only after successful processing
Smell
consumer.poll(timeout);
for (record : records) {
// auto-commit may already have advanced
db.save(record);
}
Fix
consumer.poll(timeout);
for (record : records) {
db.saveIdempotent(record); // upsert / dedupe by event id
}
consumer.commitSync();
acks=all needs min.insync.replicas
Smell
# topic RF=3 but min.insync.replicas left at 1
Fix
# broker/topic
min.insync.replicas=2
# producer acks=all
Consumer poison pill handling
Smell
while (true) {
process(record); // throws forever — partition stuck
}
Fix
try {
process(record);
} catch (Exception e) {
if (attemptsExhausted(record)) {
dlq.publish(record, e);
} else throw e; // retry / seek policy
}
commit(record);
Kafka — extra staff checks
Transactional outbox beats dual-write
Smell
repo.save(order);
producer.send(OrderCreated.of(order)); // crash → lost event or ghost event
Fix
@Transactional
void place(Order order) {
repo.save(order);
outbox.save(OrderCreated.of(order));
}
Rebalance-safe consumers: don’t do heavy work without pause/commit strategy
Smell
for (record : records) {
Thread.sleep(120_000); // exceeds max.poll.interval → rebalance storm
process(record);
}
Fix
for (record : records) {
processIdempotent(record); // fast or async with careful commit
}
consumer.commitSync();
Compacted topics vs infinite log — know retention semantics
Smell
// treating compacted changelog as a task queue
Fix
// task queue: delete retention + consumer groups
// latest-state per key: compact + tombstones
Practice drills (crack the round)
Do these timed. After each, write a ranked finding list without looking at this post, then grade yourself.
- Drill A (20 min) — Checkout: Hunt double money, coupon
==, null vs throw, godcheckout(), dead params. Target ≥3 blockers/highs. - Drill B (20 min) — Redis lock worker: NX PX, token-safe release, busy-spin, interrupt, DLQ. Target all five.
- Drill C (25 min) — Feed/cache: Sequential fanout, unbounded batch, region in key, TTL/LRU, single-flight, HttpClient reuse, body cap, PBKDF2 scorer.
- Drill D (20 min) — Documents API: IDOR on get/update/delete, SQLi, secrets, pagination clamp, shared
requireOwner. - Drill E (25 min) — Webhook + Kafka: Signature verify, event type, idempotency, markPaid, receipt off path; producer idempotence + process-then-commit.
- Drill F (25 min) — Agent loop: Iteration budget, result-by-id, path traversal, shell sandbox, empty error string, model-supplied userId.
- Drill G (15 min) — Closing muscle: From any prior drill, deliver only the 60-second ranked summary aloud.
- Drill H (30 min) — Spring PR bible: SecurityFilterChain + IDOR, @Transactional around HTTP, WebClient timeouts, @KafkaListener ack/DLQ, Idempotency-Key on POST. Write a ranked review with ≥4 blockers/highs.
Low-signal comments — with snippets
“Use Objects.isEmpty” when null||isEmpty is already correct
Code under review
if (signature == null || signature.isEmpty()) {
return false;
}
// Real finding — wire verification before charge
if (!verifyWebhookSignature(rawBody, signatureHeader)) {
return 401;
}
handle(parse(rawBody));
“Move HmacSHA256 to config” when verifySignature is never called
Code under review
Mac mac = Mac.getInstance("HmacSHA256");
boolean ok = MessageDigest.isEqual(mac.doFinal(body), sig);
// …but handleWebhook never calls this helper
void handleWebhook(byte[] body) {
Event e = parse(body);
charge(e); // unsigned body trusted
}
if (!verifyWebhookSignature(body, header)) return 401;
charge(parse(body));
“Name the %02x format string” with no correctness impact
Code under review
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
Null-checking HttpExchange / getRequestBody when the API guarantees them
Code under review
void handle(HttpExchange exchange) throws IOException {
InputStream in = exchange.getRequestBody();
byte[] body = in.readAllBytes();
// …
}
Generic “use Jackson” without naming missing signature verify / idempotency
Code under review
Event e = new Event();
e.id = json.substring(json.indexOf(""id":") + 5); // fragile parse
charge(e); // no signature check, no dedupe on e.id
if (!verify(body, sig)) return 401;
Event e = mapper.readValue(body, Event.class);
if (db.wasProcessed(e.id)) return 200;
charge(e);
db.markProcessed(e.id);
“Extract a private method” without calling out HashMap tie nondeterminism
Code under review
String findTopProduct(Map<String, Long> counts) {
String best = null;
long bestCount = -1;
for (var e : counts.entrySet()) { // HashMap — arbitrary order
if (e.getValue() > bestCount) {
best = e.getKey();
bestCount = e.getValue();
}
}
return best; // ties → nondeterministic winner
}
return counts.entrySet().stream()
.max(Comparator.comparingLong(Map.Entry<String, Long>::getValue)
.thenComparing(Map.Entry::getKey))
.map(Map.Entry::getKey)
.orElse(null);
“Return empty list instead of null” for cache get — breaks miss signal
Code under review
List<Item> getCached(String key) {
return map.get(key); // null = miss
}
Feed getOrLoad(String key) {
List<Item> hit = getCached(key);
if (hit == null) {
hit = loader.load(key);
map.put(key, hit);
}
return new Feed(hit);
}
Optional<List<Item>> getCached(String key) {
return Optional.ofNullable(map.get(key));
}
Feed getOrLoad(String key) {
return new Feed(getCached(key).orElseGet(() -> loadAndStore(key)));
}
Generic “validate userId/region” while fanout/stampede/HttpClient stay uncaught
Code under review
Feed buildFeed(String userId, String region) {
List<Item> all = new ArrayList<>();
for (Source s : sourcesFor(userId)) {
HttpClient c = HttpClient.newHttpClient(); // per call
all.addAll(fetch(c, s, region)); // sequential
}
// getOrLoad has no single-flight — stampede on cold key
return rank(all);
}
Leading with findAllSources batching while cache/fanout bombs stay uncaught
Code under review
List<Source> all = db.findAllSources(); // fair note, lower priority
List<Source> followed = all.stream().filter(s -> follows.contains(s.id)).toList();
for (Source s : followed) fetch(s); // sequential + per-call client + forever cache above
Staff scorecard (use before you submit / leave the room)
Money · data · dual-run
Why it fails
Atomic · pure · shared
Blockers first
Root-cause framing
Ranked summary
- Did I name at least one way this loses money, leaks data, dual-runs work, or OOMs / stampedes under load?
- Did I propose a fix shape (atomic op, shared guard, pure function, single-flight, bounded pool)?
- Did I separate blocker / high / nit?
- Did I ask one clarifying question where intent might be unfinished?
- Did I deliver a 60-second ranked summary?
- Would an interviewer see judgment separating production risks from background noise?