Skip to main content
Building with AI (Vibe Coding)

⏱ About 20 min20 XP

When to Refactor

Every project accumulates imperfection. Early decisions made with incomplete information become constraints that are awkward to work around. Code written quickly to test an idea becomes permanent. Components that were small and simple grow into something tangled. The instinct when facing this kind of accumulated complexity is to keep adding — to patch around the problem rather than address it. Refactoring is the deliberate alternative: restructuring existing code to make it cleaner, clearer, or more maintainable without changing its external behavior.

What Refactoring Is and Is Not

Refactoring (definition): the process of changing the internal structure of code — how it is organized, named, or decomposed — without changing its observable external behavior. A refactored function does the same thing it did before; it is just structured so that it is easier to understand, test, or modify. Refactoring is not: - Adding a new feature (that changes behavior) - Fixing a bug (that changes incorrect behavior to correct behavior) - Optimizing for speed (that may change internal behavior without changing external results, but the goal is performance, not clarity) The distinction matters because refactoring carries a specific risk: the external behavior must remain identical. A refactor that accidentally changes behavior has introduced a regression. Testing before and after a refactor is therefore mandatory. Common signals that refactoring is warranted: 1. Duplication: the same logic appears in three or more places. When this logic needs to change, it must be changed in each place — and missing one is a bug. 2. Length: a single function has grown to fifty or more lines and does several conceptually distinct things. Long functions are hard to understand, hard to test, and hard for the AI to modify correctly. 3. Names that lie: a variable called 'data' that actually holds a list of expense objects, or a function called 'processStuff' that validates input, writes to the database, and sends a notification. Misleading names cause errors when someone (or the AI) reads the code and infers behavior from the name. 4. Fragility: every time you change one part of the code, something else breaks unexpectedly. This is a sign that components are too tightly coupled — that they depend on each other's internal details rather than their published interfaces.

Refactoring Preserves Behavior

The definition of refactoring is precise: external behavior does not change. If you add a feature while refactoring, you have made two changes simultaneously — and lost the ability to verify each independently. Discipline here means: refactor first (confirm behavior is preserved), then add the feature in a separate loop.

Leading the AI through a refactor requires a different kind of prompt than a feature prompt. A feature prompt describes new behavior. A refactor prompt describes the structural change without changing behavior. A well-structured refactor prompt for the expense tracker might read: 'I want to refactor the POST /expenses route. Currently it performs three tasks inline: it validates the request body, it writes to the database, and it formats the response. I want to extract these into three separate helper functions: validateExpenseInput(body) which returns { valid: boolean, errors: string[] }, insertExpense(validatedData) which returns the new record, and formatExpenseResponse(record) which returns the JSON object. The route function itself should call these three helpers in sequence and handle errors from each. The observable behavior — accepted inputs, returned responses, error codes — must remain identical. Do not add any new validation rules or change any existing ones.' Note the specificity: the names of the new functions, their signatures, what each returns, and an explicit statement that observable behavior must not change. This specificity gives the AI what it needs to refactor correctly and gives you a clear standard against which to review the output.

Complete the definition of refactoring.

Refactoring changes the structure of code without changing its behavior.

Knowing When Not to Refactor

Refactoring has a cost: it takes time, it introduces risk of regression, and it defers feature work. Three situations where refactoring is not the right choice: 1. The code works and will not change: if a component is stable, correct, and unlikely to be modified, its internal messiness has no practical impact. Refactoring stable code is activity without benefit. 2. You do not understand the code well enough yet: a refactor requires precise knowledge of what the code currently does. If you are reading code you did not write (or code the AI generated that you have not fully reviewed), refactoring it before understanding it is a recipe for regression. 3. A deadline requires shipping a feature: refactoring and feature work compete for the same time. A conscious decision to defer a refactor is legitimate — write a comment or a note in your project brief recording the debt, and return to it when the deadline pressure is relieved. Unrecorded technical debt accumulates invisibly; recorded debt can be scheduled.

AI-Driven Refactoring Requires Extra Review

An AI asked to 'clean up this code' may rename variables, restructure control flow, combine or split functions, and add comments — any of which may inadvertently change behavior. Refactor prompts must be more precise than feature prompts, not less. Always specify exactly what structural change you want and explicitly state that observable behavior must not change. Then read the diff carefully.

A developer extracts a validation function from a route handler. After the refactor, the same inputs that were accepted before are still accepted, the same inputs that were rejected before are still rejected, and error messages are identical. This is an example of:

Why is it important to refactor and add features in separate loops rather than simultaneously?

Refactor Planning Exercise

  1. Read the following description of a function and identify the refactoring opportunities. Then write a refactor prompt for the AI.
  2. Function description: The function is called 'submit'. It receives a form submission object. It checks that the amount field is a number greater than zero. It checks that the category field is one of: Food, Transport, Housing, Entertainment, Other. If either check fails, it logs the error to the console and returns null. If both pass, it constructs a new object with the fields: id (a randomly generated string), amount (the submitted amount), category (the submitted category), date (today's date formatted as YYYY-MM-DD), note (the submitted note, or an empty string if not provided). It then calls the database module's insert function with this object. If the insert succeeds, it returns the new record. If the insert fails, it logs the error and returns null.
  3. Tasks:
  4. 1. List every refactoring signal you see in this function (duplication, length, names that lie, fragility).
  5. 2. Propose a set of helper functions to extract, with a name and one-sentence description for each.
  6. 3. Write a complete refactor prompt for the AI that specifies the structural change, the helper function names and signatures, and an explicit statement that observable behavior must not change.