Start with a working understanding

Refactoring means changing the internal structure of your code while leaving its external behavior untouched. The usual targets—simplifying nested conditionals, extracting duplicated logic, renaming vague identifiers, splitting long functions—all require you to know what the code actually does before you touch it. Otherwise you risk “improving” away its intended behavior.

Take a method like this one:

public String getSound(String animal) {
  if (animal == null) {
      System.out.println("Oops! A null animal?");
  } else if (animal.equalsIgnoreCase("Dog")) {
      return "Bark";
  } else if ( animal.equalsIgnoreCase("Cat")) {
      return "Meow";
  } else if ( animal.equalsIgnoreCase("Bird")) {
      return "Tweet";
  }
  return "Unknown";
}

A typical reaction might be “that should be a switch statement,” but that suggestion only makes sense if you know how the existing chain of if statements evaluates—checking each condition in order and falling through to a default Unknown return when nothing matches. As your code spans multiple files and services, the same reasoning gets harder to apply by eye.

Copilot Chat can help you map out the territory before you modify anything. Use it to explain a selection of code, either in plain language or with the /explain slash command. Limit the analysis to what matters by selecting code in your IDE first or by naming specific files with #file. You can also ask for comments to be added to the code as a way of documenting your understanding.

  • Explain what this code does.
  • What is this code doing?
  • Add comments to this code to make it more understandable.

Work through the relevant parts of your codebase with these prompts until you’re confident about the behavior you’re preserving.

Try broad improvement suggestions

When you’re ready to start refactoring, low-stakes changes come first. Open Copilot Chat in your project and ask a general question like “how would you improve this?” The same scoping techniques apply: highlight a section or reference files with #file so Copilot concentrates on the code you care about.

  • How would you improve this?
  • Improve the variable names in this function.
  • #file:pageInit.js, #file:socketConnector.js Offer suggestions to simplify this code.

These open-ended prompts have a tradeoff worth recognizing. Without much context, Copilot explores many possible directions, which can surface options you hadn’t considered. The downside is the proposals may not target the concerns that actually matter to you.

To get more relevant output, build context into your prompt—the same way an engineer benefits from a precise assignment rather than an open-ended “code something.” State what aspect of the code you want addressed.

Define a concrete refactoring plan

Improving code without a goal tends to produce scattered changes. Instead, decide what you want to accomplish first. Do you want to boost readability? Eliminate redundancy? Reduce the cost of future changes? Once you know the objective, you can point Copilot at it directly.

For example, suppose you have several scripts that each contain the same core logic. A sound refactor is to extract that logic into a shared module that the scripts import, so you only maintain it in one place. You can instruct Copilot to hunt for the duplicate regions and consolidate them:

Inspect all my js files for GitHub API calls and create a new class that will manage all the GitHub API calls.

With this added direction, the suggestions you get back will center on the consolidation task rather than generic cleanup. You can also stack requirements—tell Copilot what to keep in mind while it refactors, such as preserving error handling or keeping public APIs stable.

Can you refactor the GitHubController class to:
- remove nested logic structures
- make the code more concise
- while doing this, check if the code is safe and add comments if not

The pattern to internalize: Copilot responds well to specificity. Vague prompts produce diffuse, sometimes unhelpful answers; prompts that describe the desired outcome produce targeted refactoring suggestions. For deeper guidance on prompt design, see the official documentation on prompt engineering for GitHub Copilot.

Refactoring at scale with GitHub Copilot

Refactoring a single file is straightforward. Refactoring a codebase that spans multiple scripts and workflows, each tailored to a specific customer, is another matter entirely. That was the situation we faced when three organizations independently asked for help migrating from other data centers into tens of thousands of GitHub repositories.

We had built two large-scale migration systems, but each was deeply customized for the original requesting company. When a third organization made a similar request, it became clear that the right move was to refactor our code into modular, reusable components that could be shared across organizations rather than continuing to fork and customize. The codebase was complex, so we turned to GitHub Copilot to help with the heavy lifting.

Before asking Copilot for anything, we defined a clear set of goals for the refactored code:

  • Modular and reusable: Small, self-contained modules that can be combined for new functionality.
  • Maintainable: Well-organized, well-documented, and easy to extend.
  • Customizable: Flexible enough to meet each organization's unique needs.
  • Following best practices: Consistent, readable, and easy to understand.

Building a shared module

Our first step was to create a common module, gh-migrations, that every script could import. This gave us a single place to put reusable logic and made the later refactoring of individual scripts a matter of swapping calls to the new module. We started with a skeleton and then prompted Copilot to fill in the implementation. As always, we reviewed the generated code carefully before integrating it.

After the core module was in place, we needed to separate the new logic into its own file and update index.js to reference it. Copilot handled that restructuring, and we then repeated the process for each file that contained GitHub API calls. At this stage we deliberately did not refactor the existing code directly; the goal was to create the common module first so we could lean on it later.

Creating reusable entity classes

With the GHApi class in place to handle GitHub API interactions, we moved on to creating reusable classes for the entities in our migration tool. Since the tool manages migrations through GitHub Issues, we needed classes for four distinct entity types:

  • Migration issues: Represent repository migrations from various sources, including configuration and metadata about each repository and how the migration should run.
  • Batch issues: Represent groups of migrations, holding higher-level metadata like custom application IDs and batch status.
  • Team issues: Represent the migration of a group of users, their repositories, and associated permissions.
  • Rewire issues: Represent Azure DevOps pipeline rewire operations that reconfigure pipelines to point to the new GitHub repositories after migration.

We prompted Copilot to generate the first entity class, then followed the same pattern to create BatchIssues, TeamIssues, and RewireIssues, each with their own properties and methods.

Encapsulating state management

Once the entity classes existed, we recognized the need for proper encapsulation. Each class should be responsible for managing its own state and behavior, rather than leaving client code to do that work. This meant giving the classes methods to get and set state, perform actions on the object, and handle GitHub API calls internally. This pattern standardizes how state is protected and keeps the client interface simpler.

Copilot generated the suggested class structure, but reviewing the output revealed a gap: the initialize method wasn't connected to the constructor. We fed that observation back to Copilot as an iterative prompt. The response suggested a factory method approach instead of what we had in mind. It wasn't what we were originally thinking, but it worked well: it kept the constructor clean and focused on initializing the object while gracefully handling different creation and loading scenarios. The save method followed the same philosophy, encapsulating the logic for creating a new issue and improving readability. We applied the same pattern to each of the new entity classes.

Refactoring the client code

With the common module complete, the remaining work was to refactor each client script to use it. This was an iterative process, moving back and forth between the client code and the shared module to ensure everything worked correctly. In our case, because the project was still evolving, we could make sweeping changes to the codebase. For code that is actively in production, smaller incremental changes are more realistic. In either scenario, writing tests to verify the refactored code behaves as expected is essential.

We worked through each script one by one. For instance, refactoring create-team-issues.js followed the same pattern: prompt Copilot with the specific refactoring goal, review its suggestions, and integrate the changes. The process repeated for the remaining scripts.

The key was building the common module before refactoring the client code. That gave Copilot the context it needed to produce consistent suggestions and ensured that the final codebase was something we could reuse and customize for multiple organizations instead of yet another one-off system.

Practical guidance for large refactors

Simple refactors are easy. The difficulty scales quickly when you're dealing with a codebase spread across many files and maintained by a large team. When the task feels overwhelming, the best move is to start with the basics: improve your understanding of the code you're about to change before you change it. The more you know about the starting point, the better positioned you are to design an effective refactoring strategy.

Once that understanding is in place, GitHub Copilot can help translate your plan into code. It's a useful partner for turning intentions into consistent, well-structured implementations.