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.
What items should a code-review checklist contain to enforce production-quality, defensive-programming standards across distributed teams? Draft a prioritized checklist of at least eight review items, and for each one explain why it directly impacts production reliability or operability.
Sample Answer
Direct answer
A code-review checklist for defensive, production-quality standards should be short enough that reviewers actually use it every time, and should focus on the handful of items that correlate most directly with real production incidents: error handling, observability, resource leaks, secrets management, idempotency, and input validation, each with a concrete one-line test a reviewer can actually apply while reading a diff.
Structured elaboration
1. Error handling. Does every external call (network, database, file system) have explicit failure handling, and is there no bare, silent catch-and-ignore? This matters because a swallowed exception is one of the most common root causes of "the system silently stopped working and nobody noticed for days".
2. Observability. Does this change add or preserve logging/metrics for its new failure paths, not just its happy path? A new code path with no visibility into whether it's failing in production is effectively unmonitored the moment it ships.
3. Resource leaks. Are file handles, database connections, and locks acquired in this diff guaranteed to be released even when an exception occurs (via try/finally, a context manager, or the language's equivalent)? This matters because a resource leak in an error path specifically (the path least likely to be exercised in normal testing) is a classic source of a slow production degradation that only appears under sustained load or over a long uptime.
4. Secrets management. Does this diff introduce any hardcoded credential, API key, or token, or log anything that could contain one? This is a fast, mechanical check (often automatable via a pre-commit secret scanner) but still worth a human's attention, since scanners miss secrets embedded in less obvious places like a debug log statement.
5. Idempotency. If this diff adds or touches an operation that could be retried (by a client, a queue redelivery, or an internal retry mechanism), is that operation actually safe to run more than once? This matters because a non-idempotent operation that silently becomes retriable somewhere in the call stack is a duplicate-side-effect bug waiting to happen, often not caught until production traffic patterns exercise the retry path that testing never did.
6+. Input validation and the remaining prioritized items. Does every externally-supplied input reaching this code get validated at a clear boundary, rather than trusted implicitly? Beyond these top items, a fuller checklist includes: test coverage for the new failure paths specifically (not just the happy path), whether any deprecated or discouraged pattern was introduced, and whether the change includes a rollback plan for anything touching a schema or a stateful migration.
Why these six, and why prioritized. Each is chosen because it maps directly to a common, real production-incident root cause, and they are ordered so a reviewer under time pressure who only gets through the first three still caught the highest-impact categories.
Worked example
A pull request adds a new endpoint that calls an internal payments service and writes a record to a local database. Applying the checklist: (1) error handling: the diff has a bare except: pass around the payments call, flagged; (2) observability: no log line exists for the payments-call failure path, flagged; (3) resource leaks: the database connection is correctly used inside a context manager, passes; (4) secrets: no hardcoded credentials found, passes; (5) idempotency: the endpoint is a POST that creates a payment record with no idempotency key, and the client-facing API documentation doesn't mention retry safety, flagged as a real production risk given payments-adjacent code specifically; (6) input validation: the request body is validated via a shared schema, passes. Three of six items are flagged, and the two most severe (the swallowed exception and the missing idempotency key on a payments-adjacent endpoint) block the merge, while the missing log line is a required fix but not necessarily a hard blocker if paired with a fast-follow commitment.
Trade-offs and pitfalls
A checklist with thirty items reliably gets skimmed rather than actually applied under normal review-time pressure; keeping it to the highest-impact handful, with a concrete one-line test for each, is what makes it something a reviewer genuinely runs through on every diff rather than something referenced once and then forgotten. The most common failure mode for a checklist like this is treating it as a one-time training exercise rather than something enforced consistently: without periodic reinforcement (referencing it explicitly in review comments, tracking how often flagged items actually get raised) it tends to fade from active use within a few months of being introduced.
Compare and contrast graceful degradation and fail-fast design approaches for production systems. For each approach, explain a typical use case (for example a customer-facing API versus an internal pipeline), the operational trade-offs, how you would instrument each approach with metrics, logs, and traces, and how you would communicate degraded functionality to clients or downstream systems.
Sample Answer
Direct answer
Fail-fast stops the operation immediately and surfaces the error the moment something is wrong, trading availability for correctness and a clear signal; graceful degradation keeps the system partially functional by falling back to reduced capability, trading some correctness or completeness for continued availability. The right choice depends on whether a wrong or incomplete answer is worse than no answer at all for this specific system.
Structured elaboration
Fail-fast: when correctness matters more than availability. An internal data pipeline computing financial reconciliation numbers should fail fast and loudly the moment its inputs look wrong, because a wrong number that looks plausible and gets used in a report is a much worse outcome than the pipeline simply not running today. Fail-fast systems are also easier to operate: a hard failure with a clear error is diagnosable immediately, whereas a system that silently degrades can mask a real problem for a long time before anyone notices the quality of its output has quietly dropped.
Graceful degradation: when partial availability beats a hard stop. A customer-facing product page that depends on a recommendation service should degrade to a generic, non-personalized set of recommendations if that service is slow or down, rather than showing the customer an error page, because a slightly-worse-but-functional page is a much better outcome for both the customer and the business than a hard failure on a page that otherwise works fine.
Instrumentation differs by approach. A fail-fast system needs strong alerting on the failure itself, since the failure IS the signal: an error rate spike, a specific exception type, or a circuit breaker opening. A gracefully-degrading system needs the opposite kind of visibility: a metric or log line specifically for "we are currently in degraded mode", because the degraded path, by design, does not look like a failure to a simple error-rate dashboard, and a team that isn't specifically tracking degraded-mode usage can be running in a permanently degraded state for months without noticing.
Communicating degraded functionality. For a user-facing system, this usually means a visible but non-alarming UI signal ("Showing popular items while personalized recommendations are unavailable") rather than silence, since silent degradation erodes trust once a user notices the quality difference without being told why. For a downstream service-to-service dependency, this means an explicit field or header in the response indicating degraded mode, so the calling service can make its own informed choice about whether to also degrade or to fail.
Worked example
An internal pipeline vs. a downstream API dependency, side by side: the internal pipeline computing quarterly revenue numbers for a financial report should fail fast and halt if a required upstream table is empty or a row count sanity check fails, alerting the on-call data engineer immediately, because publishing a subtly wrong number in a financial report is far worse than the report being late. The customer-facing recommendation widget on the same company's storefront, dependent on a separate ML service, should instead catch a timeout from that service and immediately serve a cached "most popular this week" list, log a degraded_mode=true metric tagged with the reason, and continue serving the page, because an incomplete page for one widget is a minor UX cost, not a correctness failure the business needs to halt over.
Trade-offs and pitfalls
The most common mistake is applying the wrong default to a whole system uniformly: treating every dependency as fail-fast produces a fragile product where one non-critical service outage takes down an entire page, while treating every dependency as gracefully-degradable risks quietly serving wrong financial or safety-relevant data with no alert ever firing. The decision should be made dependency by dependency, based on whether being wrong is worse than being unavailable for that specific piece of functionality, not applied as a single system-wide policy.
Describe the role of assertions and invariants in maintaining code correctness. When should assertions be used versus throwing exceptions? Provide an example where an assertion detects a developer error early and avoids a costly runtime check in production, and describe how this maps to design-by-contract thinking (preconditions, postconditions, invariants).
Sample Answer
Direct answer
An assertion checks a condition that your own code's logic guarantees should always be true if nothing upstream has a bug; an exception handles a condition that the outside world (a user, a file system, a network) can legitimately produce regardless of whether your code is correct. Assertions catch developer errors early and cheaply; exceptions handle the world being unpredictable.
Structured elaboration
What an assertion is for. An assertion encodes an invariant: a statement that, given correct code, must hold at this point in the program no matter what valid input arrives. If it fails, the bug is in the code that led to this point, not in the input or the environment. Because of this, assertions are cheap to reason about (you never need a recovery path for them, the correct response to a failed assertion is to fix the bug) and, in several languages, can be compiled out entirely in optimized production builds, which is precisely why they must never be relied on for something the outside world can trigger.
What an exception is for. An exception handles a condition that is a normal, expected possibility given a correct program: a file that does not exist, a network call that times out, a user who submits invalid input. These require an actual recovery path (retry, a default, informing the user) because they will happen in production no matter how correct the code is.
Preconditions, postconditions, and invariants (design by contract). A precondition is what a function requires to be true of its inputs to behave correctly; a postcondition is what it guarantees to be true of its output if the precondition held; an invariant is a condition that must hold at every observable point in an object's lifetime. Assertions are the natural implementation mechanism for all three inside a single codebase's own internal logic: asserting a precondition at function entry catches a caller who violated the contract due to a bug in their own code, which is different from validating an input that arrived from outside the trust boundary and might be malformed for entirely legitimate reasons.
The line between the two. The test is not "is this input bad", it's "could this input be bad even if every line of my own code is correct". A negative array length passed internally between two functions you wrote and control, where nothing external can produce that value if your code is right, is an assertion case. A negative quantity field parsed from a JSON request body is an exception (or validation-error) case, because a malicious or buggy client can produce it no matter how correct your server code is.
Worked example
A function withdraw(account, amount) internal to a ledger system might assert assert account.balance >= 0, "invariant violated: account balance went negative" right after debiting, because if the debit logic is correct, the balance should never go negative; if this assertion fires, there's a bug in the debit logic itself, and the fix is to find that bug, not to add a check that reacts gracefully to a negative balance in production. Contrast this with the same function's very first line, which must instead RAISE an exception (not assert) if the caller passes a negative amount: a negative withdrawal amount is exactly the kind of thing an upstream caller (a request handler parsing user input) can produce, whether or not the ledger's own internal logic has any bugs at all, so it needs a real, always-active check, and the right response is to reject the request, not to crash a debug build's assertion and silently no-op in production.
Trade-offs and pitfalls
The most costly mistake is using an assertion to validate something a real caller can trigger: because assertions can be disabled in optimized builds in several languages (C's NDEBUG, Python's -O flag), a check that only exists as an assertion can silently vanish in production, meaning the exact case it was meant to guard against reaches production code entirely unchecked. The opposite mistake, wrapping every internal invariant in a full exception with a try/catch elsewhere in the codebase, adds real performance and readability cost for something that, if your own code is correct, should genuinely never happen and does not need a recovery path at all, only a way to fail loudly and immediately during development and testing.
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.
That is every published Code Quality, Error Handling, and Defensive Programming question for Mobile Developer so far. Browse the other topics in this category, or practice this one interactively.