Facing the Legacy Code Problem

Legacy systems are everywhere, and many are written in languages that predate the web, the smartphone, and arguably good software engineering practices. A large portion of critical infrastructure—payroll, banking, government systems—still runs on COBOL. The problem isn't that these systems are broken; it's that they are increasingly difficult to integrate with modern stacks, and the pool of engineers who can maintain them is shrinking.

Modernization efforts hit familiar walls: accumulated technical debt, "temporary" fixes that became permanent, and hardcoded logic that no one dares to touch. These systems often need to integrate with newer APIs but were never designed for it. Data formats and storage methods are outdated, and migrating them without corruption is delicate. Cost and skilled resource constraints add pressure, while organizational resistance to change ("if it works, don't touch it") makes progress even harder. Underneath it all, the code often has performance ceilings and security vulnerabilities that predate modern threat models—and every small change risks breaking an unseen dependency in testing.

Copilot as a Modernization Tool

GitHub Copilot can ease the burden of working with unfamiliar, older languages. Using AI tools in the IDE reduces the need to constantly context-switch to search engines or forums for answers. Instead of breaking flow to look up COBOL syntax or legacy framework quirks, you can ask Copilot directly in your editor and stay focused on the code at hand.

You don't need a special enterprise license to use these features; all the following capabilities are available across Copilot tiers, including the free tier. The practical workflow for tackling a legacy codebase involves using slash commands, chat participants, and chat variables to scope requests precisely.

Using Slash Commands for Targeted Actions

Slash commands in Copilot Chat are shortcuts for specific development tasks. Highlight the relevant code and invoke the command to perform the action directly in your editor. The most useful commands for legacy code work include:

  • /explain – walks through how the code in your active editor works.
  • /tests – generates unit tests for the selected code.
  • /fixTestFailure – identifies and fixes failing tests.
  • /fix – finds and resolves general problems in the selected code.

Scoping with Chat Participants and Variables

Copilot Chat treats different parts of your environment as distinct "participants." While it can infer which one you need, specifying a participant can sharpen the AI's focus. The @workspace participant is effective for questions about your whole codebase. Other useful participants include @vscode for IDE commands, @terminal for shell debugging, and @azure for Azure-related help.

Chat variables add precise context to a prompt. You can reference a specific file with #file, the current repository state with #git, or the visible editor content with #editor. Other built-ins include #selection, #terminalLastCommand, and #terminalSelection.

For questions that require external knowledge, you can involve GitHub-specific skills by including @github in your prompt. Copilot picks an appropriate skill based on the question. For example, you could ask, "What is the latest LTS of Node.js?" with the #web variable added to trigger a web search.

A Practical Strategy for Migration

  • Start small – Do not attempt to refactor an entire monolith at once. Target individual functions or modules first. This builds momentum and confidence early.
  • Write tests firstGenerate unit tests that validate the current behavior before you change anything. This is a baseline "safety net" that catches accidental regressions during the rewrite.
  • Use version control – When refactoring, always work on a separate branch. Isolate your changes and use pull requests to review them before merging to the main codebase.
  • Review everything – Treat Copilot's output as a suggestion, not a final answer. Human review ensures the code meets your organization's security, architecture, and style requirements.

Migrating COBOL to another language is not a single command—it takes a structured approach of understanding the existing logic. Running an explanation on a large module, generating tests against that behavior, converting small pieces, and then using Copilot to handle repetitive translation patterns is one way to keep the process manageable.

For example, a generated /tests command ensures the legacy business logic is understood and codified before you begin rewriting, and your prompts can bring in the correct file context with #file. The AI may then suggest an implementation in the target language that matches the scope you have defined through this process.

From COBOL to Node.js: A guided Copilot workflow

The example that follows walks through modernizing a small COBOL account management system into a Node.js application. Every step works with any tier of GitHub Copilot, including the Free version. The model used here is OpenAI’s GPT-4o; Free tier users can also choose Anthropic’s Claude 3.5 Sonnet. The full demo repository is available on GitHub and can be forked for hands-on practice.

The repository contains three files—data.cob, main.cob, and operations.cob—that handle crediting, debiting, balance viewing, and program exit. Since Copilot runs on generative AI, outputs can vary between sessions even with identical prompts.

Step 1: Compile and run the legacy program

Two options exist for getting a COBOL compiler: install GnuCOBOL locally or work in a GitHub Codespaces environment. On macOS, GnuCOBOL installs via Homebrew:

brew install gnucobol 

For Codespaces or Ubuntu Linux, use the apt-based install command:

sudo apt-get update && \
sudo apt-get install gnucobol

Once the compiler is in place, build the executable by compiling the source files and linking them:

cobc -x main.cob operations.cob data.cob -o accountsystem

Step 2: Ask Copilot to explain the code

With the program compiling, the next step is understanding what the files actually do. Copilot Chat handles this efficiently. A prompt that combines the /explain slash command, the #file chat variable to point at specific files, and a plain-language request yields a detailed explanation:

/explain #file:main.cob #file:operations.cob #file:data.cob Can you please create a high level overview of the app. It will be great if you can explain each file in detail and how they are linked.

The response covers the logic of each file, the relationships between them, and the purpose of each section:

Sure! Here's a high-level overview of the COBOL application, along with detailed explanations of each file and how they are linked together.
High-Level Overview
This COBOL application simulates an account management system. It allows users to view the current balance, credit the account, debit the account, and exit the application. The application is divided into three main COBOL source files: main.cob, operations.cob, and data.cob.

Step 3: Visualize data flow with @workspace

Understanding the interactions between files is easier with a diagram. The @workspace command lets Copilot search the codebase, plan edits, and explain higher-level architecture. Forcing the output into Mermaid syntax produces a rendered sequence diagram:

@workspace can you create a sequence diagram of the app showing the data flow of the app. Please create this in mermaid format so that I can render this in a markdown file.

The generated Markdown file renders as a Mermaid sequence diagram tracing how data moves through the application:

A Copilot-generated data flow diagram for a COBOL application, illustrating interactions between four components: User, MainProgram, Operations, and SubProgram. The diagram maps how the user initiates the application, selects options, and how data flows between the main program, operations module, and subprogram. It details processes such as balance inquiry, credit operations, and debit operations, showing the sequence of calls and returns among the COBOL files while updating the final balance. This diagram is featured in a GitHub blog about modernizing legacy COBOL code with GitHub Copilot.

Step 4: Build a test plan

The repository currently has no tests. A second @workspace prompt generates a structured test plan:

@workspace The current Cobol app has no tests. Can you please create a test plan of current business logic that I can use to validate with business stakeholders about the current implementation.

Later I would like to use this test plan to create unit and integration tests in a node.js app. I am in the middle of transforming the current Cobol app to a node.js app.

The test plan should include the following:

1. Test Case ID
2. Test Case Description
3. Pre-conditions
4. Test Steps
5. Expected Result
6. Actual Result
7. Status (Pass/Fail)
8. Comments

Please create the test plan in a markdown table format. The test plan should cover all the business logic in the current Cobol app.

Depending on the response, one additional prompt may be needed to have the plan output as a Markdown file for insertion into the project:

Convert this to markdown syntax to insert as a new file

Step 5: Translate COBOL to Node.js

Conversion begins by creating a new Node.js project directory:

mkdir node-accounting-app
cd node-accounting-app

Each COBOL file is converted iteratively. The main.cob conversion uses a prompt that gives context, specifies clear directions, and requests the exact outcome:

Can you convert code in #file:main.cob to node.js?

Copilot returns a complete Node.js implementation for review:

Sure! Here is the equivalent Node.js code for the 

main.cob

 file. This code will simulate the same account management system functionality.

### COBOL Code: 

main.cob

```COBOL
IDENTIFICATION DIVISION.
       PROGRAM-ID. MainProgram.

       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  CONTINUE-FLAG      PIC X VALUE 'Y'.
       01  USER-CHOICE        PIC 9.
       01  FINAL-BALANCE      PIC 9(6)V99 VALUE 1000.00.

       PROCEDURE DIVISION.
           PERFORM UNTIL CONTINUE-FLAG = 'N'
               DISPLAY "1. View Balance"
               DISPLAY "2. Credit Account"
               DISPLAY "3. Debit Account"
               DISPLAY "4. Exit"
               ACCEPT USER-CHOICE

               EVALUATE USER-CHOICE
                   WHEN 1
                       CALL 'Operations' USING 'TOTAL'
                   WHEN 2
                       CALL 'Operations' USING 'CREDIT'
                   WHEN 3
                       CALL 'Operations' USING 'DEBIT'
                   WHEN 4
                       MOVE 'N' TO CONTINUE-FLAG
                       DISPLAY "Exiting the program. Goodbye!"
                   WHEN OTHER
                       DISPLAY "Invalid choice. Please try again."
               END-EVALUATE
           END-PERFORM
           STOP RUN.
```

### Node.js Code: `main.js`
```javascript
const readline = require('readline');

let continueFlag = 'Y';
let finalBalance = 1000.00;

const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

function displayMenu() {
    console.log("1. View Balance");
    console.log("2. Credit Account");
    console.log("3. Debit Account");
    console.log("4. Exit");
}

function handleUserChoice(choice) {
    switch (choice) {
        case '1':
            viewBalance();
            break;
        case '2':
            creditAccount();
            break;
        case '3':
            debitAccount();
            break;
        case '4':
            continueFlag = 'N';
            console.log("Exiting the program. Goodbye!");
            rl.close();
            break;
        default:
            console.log("Invalid choice. Please try again.");
            promptUser();
            break;
    }
}

function viewBalance() {
    console.log(`Current balance: ${finalBalance.toFixed(2)}`);
    promptUser();
}

function creditAccount() {
    rl.question("Enter credit amount: ", (amount) => {
        amount = parseFloat(amount);
        if (!isNaN(amount) && amount > 0) {
            finalBalance += amount;
            console.log(`Amount credited. New balance: ${finalBalance.toFixed(2)}`);
        } else {
            console.log("Invalid amount. Please try again.");
        }
        promptUser();
    });
}

function debitAccount() {
    rl.question("Enter debit amount: ", (amount) => {
        amount = parseFloat(amount);
        if (!isNaN(amount) && amount > 0) {
            if (finalBalance >= amount) {
                finalBalance -= amount;
                console.log(`Amount debited. New balance: ${finalBalance.toFixed(2)}`);
            } else {
                console.log("Insufficient funds for this debit.");
            }
        } else {
            console.log("Invalid amount. Please try again.");
        }
        promptUser();
    });
}

function promptUser() {
    if (continueFlag === 'Y') {
        displayMenu();
        rl.question("Select an option: ", handleUserChoice);
    }
}

promptUser();
```

This Node.js code replicates the functionality of the COBOL main.cob file, providing a simple command-line interface for viewing the balance, crediting the account, debiting the account, and exiting the application.

The generated code still needs human verification—trust but verify always applies. The same prompt pattern works for operations.cob and data.cob. With all files translated, a final prompt asks Copilot to link everything together, initialize npm, and identify the entry point to execute:

link all node.js files to work together in one accounting application and with npm commands to initialize and install and to run the app.

Step 6: Generate unit and integration tests

The final phase adds automated coverage for the newly generated Node.js application. Another @workspace prompt asks for both unit and integration tests:

@workspace I would like to create unit and integration tests cases form the test plan mentioned in #file:TESTPLAN.md file The node.js code is in node-accounting-app folder and I am looking to generate tests for #file:operations.js file. Use a popular testing framework and also provide all the dependencies required to run the tests.

Copilot produces Jest-based test cases covering balance viewing, crediting, debiting, and program exit, along with setup instructions. The suggested test plan should be validated with business stakeholders before implementation. Copilot can also be prompted to identify edge cases or propose additional test scenarios that might have been missed.

Key takeaways for modernization projects

  • Prompt quality drives output quality: Give Copilot clear context, break large tasks into smaller steps, and specify the desired outcome. Better prompts mean more precise suggestions and a smoother workflow.
  • Answers arrive inside the IDE: Copilot eliminates the search-and-scroll cycle of traditional refactoring by bringing solutions directly into the editor, whether migrating COBOL to Node.js or cleaning up outdated logic.
  • The Free tier is genuinely usable: Anyone with a free GitHub account can access Copilot in VS Code without a paid subscription, making these techniques available to all developers.