Clean Code, Refactoring, and Maintainability Questions
Writing code that other people can read, change, and keep alive over time: naming, function and module decomposition, avoiding duplication, readability, disciplined use of language idioms and design patterns, and recognizing code smells, extending into working effectively in large, aging, or unfamiliar codebases through safe incremental change, refactoring under test coverage, and managing technical debt. Covers both authoring professional-grade code beyond mere correctness and improving code you cannot rewrite without breaking it. Spans the coding-round quality signal and the seniority signal of leaving a codebase healthier than you found it.
Your app uses jQuery for DOM manipulation and event handling, and leadership wants an incremental migration to React without a full rewrite. Propose an approach that lets old and new code coexist safely while you migrate page by page or component by component.
Sample Answer
Direct answer. Let React own a well-bounded slice of the page at a time (mount it into a container div alongside untouched jQuery-managed regions), migrate page-by-page or component-by-component starting with the most isolated pieces, and avoid having both systems fight over the SAME DOM nodes.
The coexistence strategy
- Pick a boundary where React can own a subtree cleanly: mount a React root into a specific container element for one feature/widget, leaving the surrounding page under jQuery's control -- React and jQuery can coexist as long as they don't both try to manage the SAME DOM nodes simultaneously.
- Start with isolated, self-contained widgets (a single form, a modal, a dropdown) that don't need deep two-way data flow with the surrounding jQuery-managed page -- these are the safest first conversions because the interop surface is small.
- Define an explicit communication contract for any interaction that must cross the boundary (jQuery code triggering a custom DOM event that a React component listens for, or a small shared state object both sides read from) -- ad hoc, undocumented coupling between the two systems is where this pattern typically goes wrong.
- Avoid migrating a widget that jQuery code elsewhere directly manipulates via selectors (
$('#some-react-owned-element').hide()) until that external manipulation is either removed or routed through the same explicit contract -- jQuery reaching into React's owned DOM behind React's back causes React's virtual DOM to diverge from actual DOM state, producing confusing, hard-to-debug rendering bugs. - Migrate progressively, page by page, converting the highest-value or highest-churn pages first (where ongoing jQuery maintenance costs the most), leaving stable, rarely-touched pages in jQuery until there's a concrete reason to convert them.
Why avoid a full rewrite
A stop-the-world React rewrite means the whole application is simultaneously in flux, with no way to ship incremental value or get real user feedback on individual converted pieces until the whole rewrite finishes -- the coexistence approach lets each converted piece ship and prove itself independently, with a much smaller blast radius if something's subtly wrong.
Trade-offs and pitfalls
- Running both jQuery and React means shipping BOTH libraries' bundle weight during the transition -- budget for a temporarily larger bundle size and plan to drop jQuery entirely once the migration completes, rather than treating the interim bloat as permanent.
- The most common failure mode is jQuery code OUTSIDE the React-owned region reaching INTO it via selectors -- audit for this specifically before converting a widget, since it's easy to miss a stray
$('.my-widget').someJqueryPlugin()call buried in an unrelated file.
You want to introduce mandatory linting and a consistent style guide across a codebase (or many repos) that has never had one, without drowning teams in noisy diffs or blocking urgent work. Describe your rollout plan: scope, sequencing, and how you handle legacy code that fails the new rules on day one.
Sample Answer
Direct answer. Roll out in WARN-then-ENFORCE stages, scoped to new/changed code first rather than the whole legacy codebase at once, so teams get advance visibility without a flood of unrelated diffs blocking urgent work on day one.
A staged rollout plan
- Warn-only phase: enable the linter/formatter in CI as a non-blocking report for a few weeks, so teams see what WOULD fail without anything actually blocking merges yet -- this surfaces the scale of existing violations before anyone is forced to fix them under pressure.
- Scope enforcement to the DIFF, not the whole file: a common and effective rule is 'new/changed lines must pass; pre-existing violations in untouched lines are grandfathered' -- this stops the bleeding immediately without requiring a giant one-time cleanup of the entire legacy codebase.
- Auto-fix what can be auto-fixed: run the formatter across the whole repo ONCE, in a single, isolated, reviewed PR (ideally with
git blame --ignore-revsupport configured so this mass-reformat doesn't pollute blame history for actual logic changes), separating pure formatting noise from real code changes forever after. - Enforce blocking status on new code once the warn-only phase has run its course and teams have had time to adjust their workflows (editor integration, pre-commit hooks).
- Track and periodically pay down legacy violations as a separate, lower-priority backlog, ideally opportunistically (fix violations in a file when you're already touching it for another reason) rather than a dedicated sweep that competes with feature work.
Avoiding the two failure modes
- Breaking builds for many teams: the diff-scoped enforcement plus a WARN period before ENFORCE avoids a big-bang day where hundreds of pre-existing violations suddenly block everyone's unrelated PRs.
- Silent non-adoption: making it non-blocking forever (never moving past WARN) means violations just accumulate with a report nobody reads -- commit to a concrete date when warn becomes enforce, communicated in advance.
Trade-offs and pitfalls
- The single, whole-repo auto-format commit is disruptive to any in-flight branches at that moment (merge conflicts on every file touched) -- schedule it for a quiet period and communicate it clearly, and configure blame-ignore-revs immediately so it doesn't permanently obscure history.
- 'New/changed lines only' enforcement can be gamed by refactoring a file just enough to dodge triggering full-file linting, or conversely can feel unfair when a small logic change in an old, unlinted file suddenly triggers a wall of unrelated formatting fixes -- a common refinement is enforcing only on lines actually touched by the diff, not the whole file the diff happens to be in.
Here is a pull request that passes its tests but is hard for a teammate to follow. Review it as you would in a real code review: what would you flag, in what order of priority, and how would you phrase the feedback so it's actionable rather than just critical?
Sample Answer
Direct answer. Prioritize by risk and reader impact, not by discovery order: flag correctness/contract risks first, then readability/maintainability issues, and phrase everything as specific, actionable suggestions rather than blanket criticism.
Reading this PR (complex conditionals, unclear names, mixed concerns)
Given a PR that passes tests but is hard to follow, my review priority order would be:
- First pass -- understand intent: read the PR description and the diff once fully before commenting on anything, so feedback is grounded in what the change is trying to do, not a line-by-line reaction.
- Correctness-adjacent risk: complex conditionals are exactly where subtle logic bugs hide even when tests pass (tests often don't cover every branch combination) -- flag the specific branches I'm not confident are covered, and ask for a test or a walk-through, not just 'this is confusing.'
- Readability, with a concrete alternative: rather than 'this is hard to follow,' name the specific pattern and propose a fix: 'This nested conditional handling five states could be a guard-clause chain or a lookup table -- want me to sketch what that would look like?' Offering the alternative respects the author's time more than a bare complaint.
- Naming: flag the specific unclear names with suggested replacements (
flag2->is_priority_customer), not a general 'names could be better.' - Mixed concerns: if the function is doing more than one job, suggest the specific split (by responsibility, as in decomposing a god function) rather than a vague 'this does too much.'
Phrasing that stays actionable, not just critical
Compare 'this is messy' (unhelpful, no path forward) to 'this branch handles three customer tiers with three near-identical blocks -- could we extract a lookup table keyed by tier so adding a fourth tier doesn't need a new branch?' The second version names the specific issue, explains the future cost, and proposes a concrete fix the author can evaluate quickly.
Handling 'it passes tests, so what's the problem'
Acknowledge explicitly that correctness isn't in question -- frame the ask as being about the NEXT change: 'This works today; my concern is that the next person adding a case here will have to untangle this same complexity, possibly under time pressure. Worth a quick cleanup now while the logic is fresh in your head?'
Trade-offs and pitfalls
- Don't block a functionally-correct PR indefinitely over style preferences that don't rise to a real maintainability risk -- distinguish 'this will genuinely bite the next person' from 'I would have written it differently.'
- If the author pushes back that the current shape was a deliberate trade-off (e.g., optimized for a specific performance need), be ready to update your own recommendation rather than insisting on the abstraction regardless of their reasoning.
Design a progressive-enhancement strategy for a public-facing app that must function with JavaScript disabled or on a poor connection. Specify which features must work purely server side, how you would structure server-rendered markup and forms, how you would progressively enhance with client-side JavaScript (for example partial hydration or islands), the SEO implications, and the tests you would add to verify graceful degradation.
Sample Answer
Direct answer
Progressive enhancement means the core functionality of a page works from server-rendered HTML alone, with no client-side JavaScript required, and JavaScript is layered on top afterward to improve the experience for browsers and connections that can support it; the design principle is to build the baseline first and treat JavaScript as an enhancement, never as a requirement for the page to function at all.
Structured elaboration
What must work server-side. Any core user action, most importantly form submission, must work as a standard HTML form POST to a server endpoint that returns a full page (or a redirect), with no dependency on client-side JavaScript intercepting the submit event. Navigation must work through real <a href> links that the server can resolve, not exclusively through client-side routing that requires JavaScript to have loaded and executed first.
Structuring server-rendered markup and forms. Forms use native HTML validation attributes (required, type="email") as a first line of defense that works with zero JavaScript, and the server independently re-validates everything on submission regardless, since client-side validation of any kind, HTML-native or JavaScript, can always be bypassed. The markup itself should be semantically structured (real <form>, <button type="submit">, real headings) so it is both accessible and immediately usable without any enhancement layer.
Progressively enhancing with client-side JavaScript. Once the page has loaded and JavaScript has executed, it can intercept the same form's submit event to do an AJAX submission instead, show inline validation before the user submits, or swap in a richer, partially-hydrated interactive component (the islands or partial-hydration pattern) around a specific piece of the page, without ever removing the underlying working form as a fallback if the JavaScript fails to load or execute for any reason.
SEO implications. Search engine crawlers historically executed JavaScript unreliably or not at all, and even where a crawler can execute JavaScript, server-rendered content is indexed faster and more reliably; a page whose core content only appears after a client-side JavaScript render risks being indexed as empty or with a significant delay, which is a second, independent reason (beyond resilience to JS failures) to make sure meaningful content is present in the initial server response.
Testing graceful degradation. A specific, repeatable test disables JavaScript entirely (most browser testing tools and frameworks support this directly) and re-runs the core user flows (can you still submit the form, can you still navigate to another page); this should be a standing check in the test suite, not a one-time manual verification, since a future change can easily reintroduce a JavaScript dependency for something that used to work without it.
Worked example
A newsletter signup form: the server renders a real <form method="POST" action="/subscribe"> containing an <input type="email" required> and a submit button. With JavaScript disabled entirely, submitting this form performs a full page POST to /subscribe, the server validates the email server-side, and returns either a success page or a re-rendered form with an inline error message, and the user has successfully subscribed with zero JavaScript involved. With JavaScript enabled, a script intercepts the same form's submit event, performs the same request via fetch instead of a full page navigation, and swaps in a small success message without a full page reload, a nicer experience layered on top of, not replacing, the working baseline. If the JavaScript bundle fails to load (a content delivery network (CDN) outage, an ad-blocker interference, a slow connection that times out the script fetch), the form still works exactly as it did in the no-JS case, since the interception was purely additive.
Trade-offs and pitfalls
Progressive enhancement takes genuinely more implementation effort than building a JavaScript-only single-page interaction, since the team is effectively building and maintaining two paths (the server-rendered baseline and the JavaScript enhancement) rather than one; this cost is worth paying for core flows (checkout, signup, anything revenue- or conversion-critical) and is often not worth paying for a genuinely optional, decorative interactive widget where a JavaScript-only implementation is a reasonable, deliberate trade-off. The most common mistake is building the JavaScript-enhanced version first and then treating the no-JS fallback as an afterthought bolted on at the end, which reliably produces a fallback that technically exists but was never really tested and quietly breaks the first time the enhanced version's markup changes.
Explain the Single Responsibility Principle. What does 'responsibility' mean precisely (a reason to change, not merely 'does one thing'), and how do you recognize an SRP violation in a class or module you're reading for the first time?
Sample Answer
Direct answer. Single Responsibility Principle: a class or function should have exactly one reason to change. 'Responsibility' means an axis of change owned by a specific actor or concern, not literally 'does one thing' in a narrow procedural sense.
What 'a reason to change' actually means
A class that both formats a report AND persists it to disk has two reasons to change: the business wants a different report layout, or the ops team wants a different storage backend. Those changes come from different stakeholders on different timelines. When they're tangled in one class, a storage change risks breaking formatting and vice versa, and two people can't safely work on the two concerns in parallel.
Recognizing a violation on first read
- The class name is vague or conjunctive:
UserManager,OrderProcessor,ReportHelperAndSaver-- names with 'and' or generic suffixes likeManager/Handler/Utilare a strong tell. - Methods on the class naturally group into unrelated clusters that never call each other (e.g., half the methods touch a database, half touch an HTTP client, and neither group references the other).
- You can't describe the class in one sentence without using 'and'.
- Changing behavior for one caller's needs forces you to touch code that a completely different caller depends on.
Worked example
A UserService that validates input, applies business rules, persists to the database, AND sends a welcome email has (at least) four responsibilities. Splitting it into a UserValidator, UserRules, a repository, and a WelcomeEmailSender means: a change to email copy touches only WelcomeEmailSender; a change to the persistence layer (say, swapping ORMs) touches only the repository; and each piece can be unit-tested in isolation without mocking the other three.
Trade-offs and pitfalls
- SRP is not 'one method per class' -- that's over-splitting and creates its own maintenance cost (you now have to trace behavior across ten tiny classes instead of one). The right granularity is 'one reason to change,' not 'one line of code.'
- Don't force a split just to satisfy a rule when the two concerns are ALWAYS going to change together in practice; if they share a single reason to change, keeping them together is the correct call, not a violation.
- SRP applies at multiple altitudes: a single function, a class, and a service/module. The 'reason to change' framing works at all three, but the actors change (a function's actor might be 'the caller's contract'; a service's actor might be 'a whole team').
Unlock Full Question Bank
Get access to all 23 Clean Code, Refactoring, and Maintainability interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.