The interviewer hands you a diff for a service that moves money and asks “what's missing?” The senior answer is a checklist you run, not a vibe you have.
This is the depth question in the code-review track. You're handed a pull request for a microservice that handles secure transactions — a payments path, a consent write, a credential exchange — and asked what a senior reviewer would flag. The trap is to comment on style and a missing null check and stop. The signal is that you run the change through a repeatable security-and-correctness checklist, name the classes of risk that would cause a SEV or a breach, and then describe how you'd deliver that feedback so it lands and the diff still ships. What follows is the checklist a senior reviewer runs, plus a wrong-versus-right snippet, plus how to give the feedback.

Whether you have an internalized threat model for code that touches money, secrets, or user data — and the discipline to apply it the same way every time rather than relying on catching something clever in the moment. The interviewer wants to hear that you review for the classes of failure that hurt (a breach, a double-charge, a leaked secret, a data-corruption SEV), that you weight the failure and error paths as heavily as the happy path, and that you can prioritize: what actually blocks the land versus what's a follow-up. They're also listening for whether you'd deliver a security block without shaming the author, because a reviewer who's feared gets routed around.
How to answerGroup your findings the way you'd group them in the review so the author can act on them. Everything above the fold blocks a land; the operational hardening is where you separate must-fix from fast-follow.
| Area | What you look for | Why it’s here |
|---|---|---|
| Tests & failure paths | Unit and integration tests that exercise the error branches, retries, and partial failures — not just the happy path | The bugs that hurt live in the paths no one tested |
| Error handling | No unhandled exceptions; no bare except; errors surfaced, not swallowed; resources released on the failure path | A swallowed error in a money path is a silent data bug |
| Concurrency | Races on shared mutable state; check-then-act on balances; correct locking or an atomic compare-and-set | Two concurrent requests must not both spend the same funds |
| Secrets & credentials | No hardcoded secrets or keys; pull from a secrets manager; least-privilege scopes; rotation possible | A committed secret is a breach waiting in git history |
| AuthN / AuthZ | Caller is authenticated and authorized for this resource; no missing object-level checks (IDOR) | Authn without per-object authz is the classic escalation |
| Input validation & injection | Validate and bound all external input; parameterized queries; no shell string-building; no unsafe deserialization | SQL, command, and deserialization injection all start here |
| Idempotency | Money movement carries an idempotency key; a retry or duplicate delivery cannot double-apply | At-least-once delivery means retries will happen |
| Encryption | TLS in transit; sensitive fields encrypted at rest; correct algorithms, no home-rolled crypto | Transactions and PII must be protected on the wire and on disk |
| PII in logs & audit | No card numbers, tokens, or PII in logs or error messages; a tamper-evident audit trail of who did what | Logs leak, and money movement needs a forensic record |
| Rate limiting & abuse | Per-caller limits, bounded retries with backoff, and abuse/fraud guards on the endpoint | An unbounded money endpoint is a fraud and DoS surface |
| Dependencies | New libraries vetted, pinned, and scanned; no unmaintained or typo-squatted packages | Supply-chain risk enters through a one-line import |
| Safe rollout | Behind a gate/flag; canary or staged ramp; a fast, tested rollback | You want to turn a bad change off in minutes, not redeploy |
The two anti-patterns you flag most often on a transaction path are a hardcoded credential and an unchecked, non-idempotent charge. Here's what you comment on, and the fix you offer alongside the block.
What you'd block# WRONG: hardcoded secret, unchecked exception, no idempotency
API_KEY = "sk_live_9f3a2b7c1d4e" # secret in source → leaks via git history
def charge(user_id, amount):
conn = payments.connect(API_KEY)
# SQL built by string interpolation → injection
row = conn.query("SELECT balance FROM acct WHERE id = " + user_id)
# check-then-act race + no retry guard → double-charge
if row["balance"] >= amount:
conn.debit(user_id, amount) # raises on timeout; nobody catches it
logging.info("charged %s for %s card=%s", user_id, amount, row["card"])
# ^ PII (card) in logs; no audit record; no authz check on user_id
What you'd approve
# RIGHT: secrets manager, handled errors, idempotent, authorized, no PII
def charge(caller, user_id, amount, idempotency_key):
if not authz.can_debit(caller, user_id): # per-object authz
raise PermissionError("not authorized for account")
api_key = secrets_manager.get("payments/api_key") # least-privilege, rotatable
try:
with payments.connect(api_key) as conn:
# parameterized query + atomic conditional debit → no race
result = conn.debit_if_sufficient(
account_id=user_id, amount=amount,
idempotency_key=idempotency_key, # retry-safe
)
except PaymentTimeout as err:
audit.record("charge_timeout", user_id, amount) # forensic trail
raise ChargeFailed("payment provider timeout") from err # handled + chained
audit.record("charge_ok", user_id, amount)
logging.info("charged account", extra={"user_id": user_id, "amount": amount})
# ^ no card / PII in logs; structured context only
return result