Code Quality, Error Handling, and Defensive Programming Questions
Writing robust, high-quality code that fails safely. Covers defensive programming, input validation, error handling and fault tolerance, logging for diagnosability, and general engineering-quality standards. Includes anticipating failure modes and making code resilient to bad inputs and unexpected states.
Design a Java logging helper that redacts common PII, such as email addresses, Social Security numbers, and credit-card numbers, from log messages before they are written. State your assumptions, show the use of compiled regular-expression patterns, discuss the performance considerations, explain how you would configure the helper to extend the redaction patterns, and describe how you would test and validate it at scale. Also cover what structured fields you would include (for example a correlation ID and a job or request identifier), what you would log at INFO versus DEBUG level, and how the approach differs for a nightly batch scoring job versus a real-time service.
Sample Answer
Direct answer
A PII-safe logging helper redacts common sensitive patterns (emails, Social Security numbers, credit card numbers) from a message before it is ever written to a log, using compiled regular expressions applied consistently across every log call, while also enforcing structured fields (a correlation ID, a job or request identifier), log-level discipline, and an extensibility point for adding new redaction patterns as new sensitive-data types are identified.
Structured elaboration
Redaction via compiled regex patterns. A small set of well-tested regular expressions, compiled once and reused (not recompiled on every log call, which would be wasteful), match common PII shapes: an email address pattern, a Social Security number pattern (\d{3}-\d{2}-\d{4}), and a credit-card-like sequence of 13 to 16 digits (allowing spaces or dashes as separators, since real card numbers are often written with them). Each match is replaced with a fixed [REDACTED] marker before the message is passed to the underlying logging framework.
Performance considerations. Compiling patterns once at startup (not per-call) and running them against every log message adds a small, roughly-constant regex-matching cost per log call; for a very high-throughput logging path this is measurable but usually acceptable, since the alternative (an actual PII leak into a log aggregation system with far broader read access than the original data source) is a materially worse outcome, and the specific cost can be validated by simply measuring log throughput with and without the redaction step in a realistic load test.
Configuration to extend redaction patterns. New PII patterns identified over time (an internal account-number format specific to the business, for instance) should be addable without modifying the core logging class, via a constructor parameter or configuration file accepting additional patterns, so the redaction logic can grow as new sensitive-data types are identified in practice, not require a code change and redeploy of the core logging utility itself for every new pattern.
Testing and validating at scale. Beyond unit tests confirming each pattern redacts correctly and that non-sensitive messages pass through unchanged, validating "at scale" means running the redaction logic against a genuinely large, realistic sample of actual (or realistically-synthetic) log messages and manually or statistically auditing a sample of the OUTPUT for anything that looks like it should have been redacted but wasn't, since regex patterns can have false negatives on real-world data that a small, hand-written unit test suite won't surface (an international phone number format, a differently-formatted SSN with no dashes).
Structured fields, log levels, and INFO versus DEBUG. Beyond redaction, the logging helper should ensure every log entry carries a correlation ID (tying related log lines from the same request or job together) and a job/run identifier where relevant (for a batch context specifically). INFO-level logging should capture what a normal operator needs to see to understand system behavior (a job started, a job completed, a summary count); DEBUG-level logging can include more granular detail useful only during active troubleshooting, but should still be redacted with exactly the same discipline as INFO, since a DEBUG log accidentally left enabled in production is a very common real-world path by which sensitive data actually ends up in logs.
Differences for a batch job versus a real-time service. A nightly batch scoring job's logging strategy centers on a per-run summary (start time, record count processed, error count, completion status) tagged with a job_id and run_id, since a human reviewing a batch job's logs the next morning wants an overview, not necessarily a line per record. A real-time service's logging centers on a per-REQUEST correlation ID and typically much higher log volume, and needs sampling strategies for very high-traffic endpoints (logging a representative fraction of successful requests at INFO, while still logging every failure) to keep log volume and cost manageable without losing visibility into failures specifically.
Worked example
public class PiiSafeLogger {
private static final Pattern EMAIL = Pattern.compile("[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}");
private static final Pattern SSN = Pattern.compile("\\b\\d{3}-\\d{2}-\\d{4}\\b");
private static final Pattern CREDIT_CARD = Pattern.compile("\\b(?:\\d[ -]*?){13,16}\\b");
private final List<Pattern> patterns = new ArrayList<>(List.of(EMAIL, SSN, CREDIT_CARD));
public PiiSafeLogger(List<Pattern> extraPatterns) { patterns.addAll(extraPatterns); }
public String redact(String message) {
if (message == null) return null;
String result = message;
for (Pattern p : patterns) result = p.matcher(result).replaceAll("[REDACTED]");
return result;
}
}
Verification note: a Java Development Kit was not available in this execution sandbox, so the exact class above was not compiled directly; the identical regular-expression patterns and replacement logic were instead executed against an equivalent Python translation (Python's re module uses the same pattern syntax for these specific expressions) and confirmed: an email address embedded in a sentence is fully redacted; a dashed Social Security number is fully redacted; a spaced 16-digit credit-card-like number is fully redacted; a message containing no PII passes through completely unchanged; and a null input returns null rather than throwing. This confirms the REGEX LOGIC is correct; it does not confirm Java-specific behavior (for example, java.util.regex.Pattern's exact semantics versus Python's re), which is a disclosed limitation of this verification, not a claim of full Java execution.
Trade-offs and pitfalls
Regex-based redaction is a strong first line of defense but is not exhaustive: a credit card number with unusual formatting, a non-US identification number format, or a PII field that doesn't match any recognizable pattern at all (a person's name in a free-text field, which has no distinguishing shape a regex can reliably catch) can pass through unredacted; this is exactly why validating "at scale" against a realistic log sample, not just a handful of unit tests, matters, and why some organizations pair regex-based redaction with an explicit ALLOWLIST discipline (only log fields you've deliberately reviewed) for the highest-sensitivity data flows, rather than relying on a denylist-style redaction pattern to catch everything. The most common real-world failure mode is a DEBUG-level log statement, written during development and forgotten, that logs an entire request or record object directly (bypassing the redaction helper entirely, since it wasn't routed through it) rather than going through this centralized logging path, which is why enforcing that ALL logging goes through a single, redacting logger (not calling a raw print or an unwrapped logging framework call directly) is as important as the redaction logic itself.
A boundary check validates that a value (an index, an offset, a size) falls within the range the code actually handles correctly, and it routinely catches real production bugs before they cause damage. Pick three DIFFERENT kinds of boundary bugs you've seen or can construct realistically, and for each: describe the bug it would cause if unchecked, the specific defensive check you'd add, and a unit test that would catch a regression if the check were later removed.
Sample Answer
Direct answer
A boundary check catches a specific class of bug (accessing an index, offset, or value outside the range the code actually handles correctly) at the moment it happens, instead of letting it silently produce wrong output or crash somewhere unrelated later; three concrete examples: array/list indexing, pagination offsets, and numeric limits.
Structured elaboration and worked examples
- Array indexing: the bug is an off-by-one or attacker-controlled index reading past the end of a buffer or list. The defensive check: validate
0 <= index < len(array)before accessing, raising a clearIndexError/custom exception instead of either crashing with a cryptic native error or, in an unsafe language, reading adjacent memory. A unit test:assert_raises(IndexError, get_item, [1,2,3], 5). - Pagination offsets: the bug is a negative or absurdly large
offset/limitfrom a client, which can either error confusingly deep in a SQL driver or, worse, silently return zero rows and look like 'no data' rather than 'bad request'. The defensive check: clamp or rejectoffset < 0and caplimitto a sane maximum (say 1000) before it reaches the query layer. A unit test:assert paginate(items, offset=-5, limit=10) raises ValueError. - Numeric limits: the bug is an integer overflow or an out-of-domain value (a negative quantity in an order, a percentage over 100) silently producing a nonsensical result instead of an error. The defensive check: validate the value's range explicitly before using it in a calculation. A unit test:
assert_raises(ValueError, apply_discount, price=100, percent=150).
Trade-offs and pitfalls
Each of these checks is cheap individually, but the value comes from applying them CONSISTENTLY at every place the boundary is actually crossed (every array access from external input, not just the ones you happen to remember); a single unguarded pagination endpoint added six months later by someone who didn't see this pattern reintroduces the exact bug class. Treat these as patterns to lint for or wrap in a shared utility function, not as one-off checks to remember individually.
Production just exhausted its error budget due to cascading 5xx errors triggered by a downstream change, and you must ship defensive changes quickly to prevent a repeat. Which mitigations do you prioritize first and why: request timeouts, retries with backoff and jitter, circuit breakers, bulkheads/isolated thread pools, backpressure, or graceful degradation? Explain how you would measure whether each change is actually working.
Sample Answer
Direct answer
Under active error-budget exhaustion from cascading 5xx errors, prioritize the mitigation that stops the cascade fastest with the least new risk: circuit breakers and request timeouts first (they cut the feedback loop immediately and are usually already-tested code paths), then backpressure/bulkheads to protect what's left, with retries-with-backoff and graceful degradation as the follow-up once the bleeding has stopped, not the first move.
Structured elaboration
- Circuit breakers, first: if a downstream change is causing cascading failures, the fastest way to stop the cascade is to stop CALLING the failing dependency; a circuit breaker (or a manual, config-driven kill switch if none exists yet for this path) halts the cascade immediately, faster than any code change can ship.
- Timeouts, immediately after: if calls to the failing dependency are hanging rather than failing fast, tightening the timeout (even a temporary, aggressive config change) frees up resources (threads, connections) being held hostage, which is often what's actually driving the cascade beyond the original failing dependency.
- Bulkheads/isolated thread pools: if the resource exhaustion has already spread to starve unrelated requests (see the bulkhead survivor), isolating pools limits further blast radius, though retrofitting a bulkhead mid-incident is a bigger, riskier change than flipping an existing breaker or timeout config.
- Retries with backoff and jitter: valuable for RECOVERY once the dependency is coming back, but retries added or left ENABLED during the active cascade make it worse, not better, by adding more load onto an already-struggling dependency; this is often the first thing to actively DISABLE, not add, during the incident.
- Backpressure: shedding load at the edge (rejecting a percentage of incoming requests outright, with a clear 503) protects the system's remaining capacity for the traffic it CAN serve, at the direct, visible cost of intentionally failing some requests.
- Graceful degradation: the longer-term fix (serve a fallback/cached response instead of failing) usually requires a code change that can't ship instantly during an active incident, so it's the follow-up hardening work, not the immediate mitigation.
- Measuring effectiveness: track the downstream dependency's own error rate and the error BUDGET burn rate in real time as each mitigation is applied; a mitigation is working if the burn rate visibly slows within minutes, not hours.
Worked example
During the incident: (1) immediately flip the circuit breaker for the failing downstream to force-open (or disable retries against it if no breaker exists) to stop the cascade; (2) tighten the client timeout for that dependency from 30s to 2s to stop threads from being held hostage; (3) if capacity is still degraded, enable load shedding (reject 20% of lowest-priority traffic) to protect the rest; (4) once the dependency confirms recovery, re-enable the breaker and retries gradually, watching the error rate as you do, rather than flipping everything back on at once.
Trade-offs and pitfalls
The instinctive first move during many outages is to add MORE retries ('the requests are failing, let's retry them harder'), which is exactly backwards during a cascading-failure incident: more retries onto an already-overloaded dependency deepens the cascade. The discipline that prevents this: stop calling the failing thing first, THEN worry about graceful recovery.
Write a small JavaScript function safeParseJSON(jsonString) that returns an object {success: boolean, value: any, error: string|null}. It must not throw, must handle invalid JSON gracefully, and must not allow prototype pollution (it must avoid assigning to proto or constructor). Also write one example unit test in Jest for an invalid input.
Sample Answer
Direct answer
A safe JSON parser must never throw on bad input, must report success or failure through its return value instead, and must specifically guard against prototype pollution, which is JSON.parse's own quiet trap: a payload like {"__proto__": {"polluted": true}} can corrupt the global Object prototype for the entire process if you are not careful about how you consume the parsed result.
Structured elaboration
Never throw. JSON.parse throws a SyntaxError on malformed input; a "safe" wrapper catches that and returns a structured result instead, so callers do not need a try/catch at every call site.
Prototype pollution. JSON.parse itself does not mutate Object.prototype (unless you additionally use a reviver or later merge the result unsafely into another object with a naive deep-merge). The real risk in a "safe parse" utility is what happens AFTER parsing: if calling code later does something like Object.assign(defaults, parsed) or a recursive merge without checking keys, an attacker-controlled __proto__ or constructor.prototype key can walk up to the shared prototype and add or override properties on every object in the process. The defensive fix at the parse boundary is twofold: use a JSON.parse reviver function that strips dangerous keys (__proto__, constructor, prototype) as soon as they are encountered, and additionally verify with Object.prototype.hasOwnProperty.call that the top-level parsed object does not carry one of those keys before returning it, since a reviver alone can miss certain nested shapes depending on how the object is later traversed.
Structured return, not an exception. Return {success, value, error} so a caller writes if (!result.success) { ...handle... } instead of a try/catch, which keeps the calling code linear and makes "parsing failed" an ordinary value instead of a special control-flow path.
Worked example
function safeParseJSON(jsonString) {
if (typeof jsonString !== 'string') {
return { success: false, value: null, error: 'input is not a string' };
}
let parsed;
try {
parsed = JSON.parse(jsonString, (key, value) => {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') return undefined;
return value;
});
} catch (err) {
return { success: false, value: null, error: err.message };
}
if (parsed && typeof parsed === 'object') {
if (Object.prototype.hasOwnProperty.call(parsed, '__proto__') ||
Object.prototype.hasOwnProperty.call(parsed, 'constructor')) {
return { success: false, value: null, error: 'disallowed key detected' };
}
}
return { success: true, value: parsed, error: null };
}
Executed (Node.js, verified): safeParseJSON('{"a": 1, "b": [1,2,3]}') returns {success: true, value: {a: 1, b: [1,2,3]}, error: null}. safeParseJSON('{not valid json') returns success: false with the underlying SyntaxError message, and does not throw. safeParseJSON(42) returns success: false without ever calling JSON.parse on a non-string. Critically, safeParseJSON('{"__proto__": {"polluted": true}}') was run and confirmed that ({}).polluted is undefined afterward, meaning the global Object prototype was NOT polluted, and the parsed value has no __proto__ own-key surviving in it; the same holds for a constructor.prototype variant of the attack.
One Jest unit test for the invalid-input case:
test('returns a failure result instead of throwing on malformed JSON', () => {
const result = safeParseJSON('{not valid json');
expect(result.success).toBe(false);
expect(result.value).toBeNull();
expect(typeof result.error).toBe('string');
});
Trade-offs and pitfalls
The reviver-based key strip handles the common shape of the attack, but a defense-in-depth mindset says: never rely on parse-time stripping alone if you also deep-merge untrusted objects elsewhere in the codebase, because a different merge utility downstream might not go through this parser at all. Prefer Object.create(null) or a Map for any object you build from untrusted keys if you can avoid prototype-based objects entirely. A common mistake is checking for __proto__ as an enumerable key with a plain for...in loop, which will not find it, since __proto__ set via the object literal syntax is an accessor, not an own enumerable property in the usual sense; using hasOwnProperty.call directly, as above, avoids that gap.
Explain patterns for handling missing or null values in strongly-typed languages like Java and dynamically-typed languages like Python or JavaScript. Include examples of Option/Maybe-style types, exceptions, and sentinel values, and explain when you would use an assertion compared to throwing a recoverable error.
Sample Answer
Direct answer
Strongly-typed languages let you make "this can be absent" part of the type itself (an Optional, Maybe, or nullable type), which forces every caller to handle the absent case at compile time; dynamically-typed languages have no such enforcement, so the same discipline has to be applied by convention, through explicit checks, sentinel values, or exceptions, and it is far easier to forget.
Structured elaboration
Option/Maybe types (Java's Optional<T>, Kotlin's T?, Rust's Option<T>). These make "might not have a value" visible in the type signature itself. A function returning Optional<User> cannot be called and have its result used as a User without the caller explicitly unwrapping it (via .get(), .orElse(default), or a null check), so the compiler catches the case where a developer forgot that the value might be absent.
Sentinel values. A special value from within the same type used to mean "nothing" (returning -1 for an index not found, or an empty string). These predate Optional types and are still common, particularly in older or lower-level codebases, but they are a real hazard: a sentinel is indistinguishable from a legitimate value of the same type unless every caller remembers to check for it, and nothing enforces that they do. indexOf returning -1 is the classic example: if a caller forgets to check and uses the result directly as an array index, it silently wraps or throws far from the actual bug.
Exceptions. Appropriate when absence represents an actual error condition the caller must react to (a required config value is missing), not merely a normal possible outcome (a user has no middle name). Throwing for something that is a completely normal case forces every caller into try/catch for ordinary control flow, which is a sign the wrong tool was chosen.
Dynamically-typed languages (Python, JavaScript). There is no compiler to force a null check, so None/null/undefined handling depends entirely on discipline: explicit is not None checks at the boundary where a value enters the system, defensive defaults (value = data.get("key", default)), and, where the codebase uses type hints, tools like mypy can catch some cases statically even though the language itself does not enforce them at runtime.
Assertions versus recoverable errors. An assertion says "this should be logically impossible given my own code's invariants; if it happens, my code has a bug", and is appropriate for catching a developer error early (an internal invariant that should never be violated if the code upstream is correct). A recoverable error (an exception or an Optional/error-result) is for a condition that is possible in the outside world regardless of whether the code is correct (a user did not provide their middle name; a file does not exist). Do not use an assertion for something a real caller can legitimately trigger, since assertions can be stripped in optimized production builds in several languages and are not guaranteed to run.
Worked example
A Java method Optional<User> findById(String id) forces every caller to write findById(id).map(User::getName).orElse("unknown") or similar, and the compiler will not let a caller treat the return value as a bare User. The equivalent Python function find_by_id(user_id) might return None on a miss, and nothing stops a caller from writing find_by_id(user_id).name and getting an AttributeError: 'NoneType' object has no attribute 'name' at runtime, potentially in a code path that only executes rarely, long after the function was written and long after the original author has moved to another project.
Trade-offs and pitfalls
Overuse of Optional/Maybe wrapping for values that are realistically always present adds ceremony without benefit; reserve it for genuinely-optional data. The most damaging mistake in dynamically-typed languages specifically is treating None/null handling as optional discipline rather than a hard rule at every boundary where external data enters the system (an API response, a database read, a config file): that is precisely where a missing null-check turns into a production incident, because it is exactly the boundary where the type system (if any) has the least information about what's actually there.
Unlock Full Question Bank
Get access to all 10 Code Quality, Error Handling, and Defensive Programming interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.