Code review judgment: blockers before nits

Java & Spring Boot code review bible (staff)

The Lattice bible for Java/Spring Boot code review and staff bug-bash interviews: Spring Security, JPA/transactions, WebClient/RestTemplate networking, Kafka/async outbox & DLQ, API idempotency — with smell/fix samples, severity ladder, and timed drills.

What staff-level code review grades

01Money / truth

Billing · flags · coupons

02Safety

Locks · races · interrupts

03Security

IDOR · SQLi · secrets

04Scale

N+1 · fanout · cache

05Ops

Idempotency · DLQ · retries

06Shape

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)

0–5Orient

Entry · data flow · side effects

5–15Hunt money/auth

Billing · IDOR · locks

15–35Scale & ops

N+1 · stampede · retries

35–45Shape

Contracts · tests · nits

45–50Summarize

Blockers → high → nits

  1. 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).
  2. 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.
  3. Minute 15–35 — load & ops. N+1, sequential fanout, unbounded pools, cache stampede/TTL, timeouts/retries, DLQ, logging secrets.
  4. Minute 35–45 — design & maintainability. God methods, inconsistent null/throw, leaky SDK fields, missing tests for the risky path. Keep these after blockers.
  5. 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.

  1. Money / truth: double/float cash, string ==, false vs null flags, coupon/tax literals, wrong cache key dimensions.
  2. Safety / exclusivity: check-then-set locks, unsafe unlock, missing single-flight, TOCTOU on files.
  3. Security: IDOR, SQLi, path traversal, secrets in repo, unsigned webhooks, shell/RCE, model-supplied userId.
  4. Reliability: no idempotency, swallowed markPaid, receipt on critical path, empty tool errors, dropped DLQ jobs.
  5. Scale / resources: N+1, sequential independent I/O, unbounded pools/batch, forever cache, PBKDF2-per-item, per-call HttpClient, huge bodies.
  6. 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

01Security

Authz · secrets · Actuator

02Data

TX · N+1 · pagination

03Network

Timeouts · CB · SSRF

04Kafka

Ack · outbox · DLQ

05API

Validation · errors

06Java

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.

  1. Drill A (20 min) — Checkout: Hunt double money, coupon ==, null vs throw, god checkout(), dead params. Target ≥3 blockers/highs.
  2. Drill B (20 min) — Redis lock worker: NX PX, token-safe release, busy-spin, interrupt, DLQ. Target all five.
  3. Drill C (25 min) — Feed/cache: Sequential fanout, unbounded batch, region in key, TTL/LRU, single-flight, HttpClient reuse, body cap, PBKDF2 scorer.
  4. Drill D (20 min) — Documents API: IDOR on get/update/delete, SQLi, secrets, pagination clamp, shared requireOwner.
  5. Drill E (25 min) — Webhook + Kafka: Signature verify, event type, idempotency, markPaid, receipt off path; producer idempotence + process-then-commit.
  6. Drill F (25 min) — Agent loop: Iteration budget, result-by-id, path traversal, shell sandbox, empty error string, model-supplied userId.
  7. Drill G (15 min) — Closing muscle: From any prior drill, deliver only the 60-second ranked summary aloud.
  8. 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)

ABlast radius

Money · data · dual-run

BMechanism

Why it fails

CFix shape

Atomic · pure · shared

DPriority

Blockers first

ESystemic

Root-cause framing

FClose

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?

Practice next

← Lattice