In this article
  1. Before you start
  2. Why interviewers ask this
  3. The refactor take-home from first principles
  4. The Staff-level answer
  5. The rubric
  6. Three variants of this question
  7. Common mistakes
  8. Follow-up questions
  9. Sources

Before you start

This page is one part of the wider picture of interview loops, rounds and who scores what; here the focus is one take-home format.

Why interviewers ask this

Reading someone else's messy code and improving it safely is the skill this assignment isolates. Candidates report versions of it: a messy RAG app to restructure while keeping the exact application programming interface (API) endpoints, removing global mutable state and making the code testable without running services; a deliberately poor Wordle solver to review; and a working but badly built report-generation agent to improve. These are reported by candidates, not confirmed by the companies.

A weak answer rewrites the code, changes endpoints it liked better and hands in no evidence. A strong answer keeps the public contract fixed, shows that behavior is unchanged and explains its choices. The same practical, build-it style also appears in forward deployed engineer rounds. To see how labs structure their wider processes, read how the Anthropic interview process works and OpenAI interview questions and process.

The refactor take-home from first principles

Observable behavior
Observable behavior is everything a caller of the program can see from outside: endpoint paths, methods, status codes, response fields and their values. Internals such as function names and file layout are not part of it.
Golden-master test
A golden-master test, also called a characterization test or approval test, is a test that records what the current code outputs for a fixed set of inputs and later checks that the output is still identical. It does not judge whether the output is right, only whether it changed, which makes it the safety net for a refactor.
Global mutable state
Global mutable state is a set of data stored at module level that any function can read and change while the program runs, such as a shared client, index or cache. It makes behavior depend on hidden history, so tests interfere with each other and code cannot be reused.
Fake
A fake is a small, working stand-in for an outside service, such as an embedding service or a language model, that returns fixed or simple answers in memory. Tests that use fakes run the same way every time and need no network or running service.

In plain words

Picture a restaurant kitchen being reorganized while the doors stay open. Customers must still get the same dishes. Only the layout behind the counter changes.

The analogy breaks in one place. Software can be run old and new side by side on the same orders, and the outputs compared exactly.

Refactoring is a change to the internal structure of software that makes it easier to understand and cheaper to modify without changing its observable behavior, in Fowler's definition (2004). Its verb form means applying a series of such changes. If the behavior changes, the work is not refactoring, as Nicolas Carlo's summary of the book puts it.

The numbers below come from an illustrative toy app, not a real company's task. It has four endpoints, five globals (a client, an index, a cache, a top-k setting and a last-query variable) and three recorded cases per endpoint.1

Step 1: Lock the behavior before you move any code

Locking behavior means writing golden-master tests before any structural change. Call each endpoint with fixed inputs, store the exact responses, and make a test that compares future responses with the stored ones. Outside services must be replaced while you record, because model and embedding outputs are not deterministic and a live recording would not repeat. Before any interface exists, patch each client at the place where the code imports it (in Python, unittest.mock.patch), or replay recorded Hypertext Transfer Protocol (HTTP) responses. Also record the calls made to those stand-ins and the contents of the vector store afterwards, because a check on responses alone misses side effects such as what ingest writes.

Input: the running app and a list of requests. Output: stored responses and a test that replays them. The toy app gets three cases per endpoint, 12 in total.1 Include odd cases, such as an empty question, because a refactor must keep quirks too. Because a refactoring must not change observable behavior, a bug present today stays in the recording, and fixing it is a behavior change that belongs in its own commit. You now have a tripwire that fires the moment any step changes behavior. Without it, every later claim of "same behavior" is only a hope.

Step 2: Replace global mutable state with objects passed in

Global mutable state is replaced by dependency injection: dependency injection is the practice of giving a function or class the things it needs as parameters instead of letting it reach for globals. Start by listing every global and every function that reads it. In the toy app, five globals are read nine times across four functions.1

Input: functions that touch globals. Output: functions whose needs are visible in their signatures. After the change the toy app has 0 globals, and the same nine dependencies appear as explicit parameters, so nothing hidden is left.1 Build the objects once at startup and pass them down. Here top_k counts as a global because the query handler reassigns it at runtime; a setting that is set once and never changes can stay a module-level constant. Rerun the golden-master tests after each small move. What you now know: the code's real dependencies are written down, which is the precondition for swapping any of them out.

Step 3: Put each outside service behind an interface and use a fake

An outside service gets a small interface: one narrow set of methods the app needs, such as embed a text, search the index or complete a prompt. The real client implements it for production and a fake implements it for tests. A RAG app usually has three such services: an embedding model, a vector store and a large language model (LLM).

Input: code that calls clients directly. Output: code that calls an interface. Patching module internals, as in Step 1, is brittle, so after this step the fakes are passed in. Without patching or fakes, a test run of the toy app would make 12 external calls: ingest 3, query 6, health 3 and documents 0.1 With injected fakes the count is 0. Across five refactoring steps, each followed by a full golden-master run, that is 60 live calls avoided, against 0.1 The assignment's wording, testable without running services, is met when the suite runs with no network.

Fakes cut external calls per test run from twelve to zeroGrouped bars of external calls in one golden-master run of an illustrative toy app: live services need 3 calls for ingest, 6 for query, 3 for health and 0 for documents, while fakes need none.External calls per run (calls)02468ingestqueryhealthdocumentsEndpointLive servicesWith fakes36300000Diagram: Interview Clozo, CC BY 4.0 ·interview.clozo.ai/blog/the-refactor-take-home.html
Fakes cut the external calls of one test run from twelve to zero, so the tests run anywhere.CC BY 4.0 — source: What is the refactor take-home, and how do you do it well?
Text description of the diagram
  • A live test run makes 12 external calls: ingest 3, query 6, health 3 and documents 0.
  • With fakes the count is 0.

Step 4: Move logic into layers and keep the routes thin

A thin route does three jobs only: parse the request, call one function and shape the response. The logic lives in plain functions or classes that take their dependencies as arguments and know nothing about the web framework.

Input: route handlers that mix parsing, logic and calls. Output: routes of a few lines and a separate layer that is easy to test. The paths, methods, status codes and JavaScript Object Notation (JSON) fields of the four toy endpoints stay exactly as they were, so endpoints before and after stay at 4.1 Move one piece at a time and make one commit per move, so a failing golden-master run points at one change. Resist renaming public things: a better name for an endpoint is a behavior change. What you now know: the structure can be reviewed layer by layer.

Step 5: Hand in notes that say what changed and what did not

Notes are a short written record that lets a reviewer trust the work without rereading every line. State the contract you preserved, the structure you moved to, how to run the tests, and why each dependency became a parameter.

List what you left alone on purpose. Bugs belong here. A bug fix changes behavior, so it is not part of the refactor: log each bug with a proposed fix and offer to make it as a separate change. Finish with the next change you would make if you had more time. Input: your commits and test results. Output: one page a busy reviewer reads in a few minutes. Reviewers cannot see the reasoning you did not write down.

A safe refactor locks behavior first and explains itself lastFlow of five steps: lock behavior with golden-master tests, remove global state, wrap outside services behind interfaces, thin the routes, and write notes that say what changed and what did not.Lockbehaviorgolden-mastertestsRemoveglobalspass objects inWrapservicesinterfaces andfakesThin routeslogic in layersWrite noteschanged andnot changedDiagram: Interview Clozo, CC BY 4.0 ·interview.clozo.ai/blog/the-refactor-take-home.html

Each step is small and ends with the same behavior check, so the contract never drifts.CC BY 4.0 — source: What is the refactor take-home, and how do you do it well?
Step-by-step description
  1. Record current outputs as tests.
  2. Pass objects in instead.
  3. Interfaces and fakes.
  4. Logic moves to layers.
  5. State what changed and what not.
  • Golden-master tests lock behavior before any code moves.
  • Global mutable state is replaced by objects passed in.
  • Outside services go behind interfaces and tests use fakes.
  • Notes say what changed and what did not.

Knowing it is half the job. Saying it is the other half.

Interview Clozo is an AI interview copilot for real interview rounds: it hears the question and puts a strong answer on your screen, and you say it in your own words. See the features or how it works.

Start Free Trial →

The Staff-level answer

Interviewer asks

You get a working but messy app and must refactor it. How do you approach it?

A strong answer

I treat the exact paths, methods and response shapes as a contract I may not change. Before touching anything I record the current outputs for a set of inputs as golden-master tests, quirks included, with fakes standing in for the embedding service, vector store and model. Then I work in small steps and rerun those tests after each one: replace globals with objects passed in, put each outside service behind an interface, and keep routes thin. If I find a bug, I log it and fix it in a separate commit after the refactor. My notes list what changed, what I left alone, how to run the tests and the next change I would make.

What makes it Staff-level is that the answer names the evidence of unchanged behavior, the order of the work and the scope boundary, and it ends in communication rather than code.

The rubric

Strong answers differ from weak ones mainly in proof of unchanged behavior and in scope discipline, not in how much code moves. This is a self-check, not any company's scoring sheet.
DimensionWeak answerStrong answerStaff answer
BehaviorEyeballs the app after changesReplays recorded requests and compares responsesRecords quirks too and explains why bugs are preserved
StructureMoves files aroundThin routes, logic in separate functionsDependencies visible in signatures, layers reviewable one by one
TestabilityTests need live servicesFakes replace outside servicesInterfaces are narrow and the suite runs with no network
ScopeRewrites and fixes bugs in one passKeeps endpoints fixedLogs bugs separately and proposes fixes after the refactor
CommunicationNo notesShort list of changesNotes state contract, trade-offs, test command and next step

Three variants of this question

Here's this mediocre Wordle solver, what is your feedback in review?

Candidates report a review version of the task. Start with behavior: what must the solver still output? Then give feedback in order of risk: correctness, hidden state, structure and test gaps, with one concrete suggestion for each. One concrete review point: separate the guess strategy from reading input and printing output, so the strategy can be tested on a fixed word list without a terminal.

You are given a working but poor report-generation agent; what do you improve?

Reported by candidates for a spreadsheet-to-Markdown agent. Keep what it does for users, then replace tools that dump raw data into prompts with aggregation done in code, and test the pieces with a fake model.

Read a snippet with a planted problem and say what is wrong

Reported for a code-review round: find the issues, rank them, explain the concept behind each, then apply the fixes. Say what you would not change in this pass, since scope discipline reads as judgment.

Common mistakes

  • "I rewrote it from scratch so it is cleaner." (wrong) A rewrite replaces the code, so it can change behavior in ways nobody checked. (right) "I changed the structure in small steps and proved with recorded tests that behavior stayed the same."
  • "I fixed a few bugs while I was in there." (wrong) Fixing a bug is not refactoring, and mixing the two hides which change broke what. (right) "I logged the bug, finished the refactor, then fixed it in its own commit."
  • "I renamed an endpoint because the new name is clearer." (wrong) The assignment fixes the exact endpoints, so this changes the contract. (right) "Public paths are frozen; I improved names only inside the code."
  • "The tests call the real services, so they are more trustworthy." (wrong) They are slow, may fail for outside reasons and cannot run for the reviewer. (right) "Fakes make the suite repeatable, and one separate check can exercise the real services."

Follow-up questions

Should I fix bugs I find while refactoring?

Not in the same pass. A bug fix is a behavior change, and Carlo's summary of the book puts it plainly: if the behavior changes, the work is not refactoring. Refactor first, then fix the bug in its own commit, and write each bug in your notes with a proposed fix.

How do you make a RAG app testable without running services?

Put the embedding model, vector store and language model behind small interfaces and pass them into the logic. Tests then pass fakes that return fixed vectors, documents and text. For more questions of this kind, see interview questions and answers for artificial intelligence (AI) engineers.

What do I hand in with the code?

Hand in the code, the tests, the exact command to run them and a one-page note: contract preserved, structure chosen, bugs logged but not fixed, and next steps. The order of that note matters less than its presence.

Can I use an AI assistant on a take-home?

Follow the written rules of the assignment, and ask the recruiter if they are silent. Whatever tools you use, you must be able to explain every change. A related page covers how an AI-assisted coding interview works and is graded.

What if the app had twice as many endpoints?

The method does not change, but the cost of live runs grows: with the same three cases per endpoint, a live run of the toy app would make 24 external calls instead of 12, and with fakes it stays at 0.1

How long should I spend on a take-home, and how do I scope it?

Follow the time limit in the instructions, and ask the recruiter if there is none. Time-box the work: lock behavior first, then do as many of the steps as fit, in order. State in your notes what you skipped and the next change you would make, because a stated boundary reads as judgment.

Key takeaways
  • The refactor take-home tests whether you can improve messy code without changing what users see.
  • Lock behavior with golden-master tests before moving any code.
  • Replace global mutable state with objects passed in, and put outside services behind interfaces with fakes.
  • Keep exact endpoints, refactor first and fix bugs in a separate change.
  • Hand in notes saying what changed and what you left alone.

Sources

  1. Definition Of Refactoring (bliki) — martinfowler.com (Martin Fowler); accessed 2026-10-26, 1 Sep 2004
  2. The key points of Refactoring - Change Messy Software Without Breaking It — Understand Legacy Code (Nicolas Carlo); the page shows no publication date, so the date given is the read date, 26 Oct 2026