Migrating Legacy Languages: Modernizing COBOL and Perl with Claude
AI generated
Claude
>_
Claude AI · Legacy Migration · COBOL & Perl
Migrating Legacy Languages: Modernizing COBOL and Perl with Claude
Why a gradual strangler fig migration backed by AI gets you further than a big bang rewrite

Many companies still run business critical core systems in COBOL, Perl, or other languages whose last complete documentation dates back decades, with the original authors long gone from the company. A full ground up rewrite sounds tempting on paper, but in practice it regularly fails because of underestimated business logic that grew over years. This article shows how to use Claude deliberately to first understand such systems, extract their rules, and then replace them in a controlled way, module by module, instead of in one single risky step.

14 min read COBOL Perl Legacy Migration Strangler Fig

1. Why big bang rewrites of legacy systems fail

A complete rebuild of a decades old COBOL or Perl system often looks like the cleanest path on paper: drop the old baggage, start fresh with a modern architecture, and avoid any compromise with historical design decisions. In practice, the same pattern shows up again and again: during the months or even years long rebuild phase, the old system keeps running in parallel and keeps getting extended with new requirements, so the rebuild never really finishes because the target keeps shifting.

The real reason for the failure runs deeper though: these systems carry business logic that grew over decades and is documented nowhere in full, only derivable from the code's actual behavior, things like special rules for certain customer groups, historically grown rounding rules, or exception handling for long forgotten edge cases. A rewrite team that doesn't know these rules simply won't reproduce them in the rebuild, something that only surfaces months after go live in the form of hard to trace complaints from business departments.

2. Building understanding without documentation: Claude as code archaeologist

The first sensible step is not writing new code but systematically understanding what already exists. Claude works well for this because it can read large COBOL or Perl files in one pass and spot patterns a human reader could easily miss while going through the code linearly, things like hidden dependencies between far apart parts of the program or implicit state changes via global variables. It's important not to ask Claude for a pure translation but to ask specifically about the business purpose of individual sections.

In practice an iterative approach works best: Claude is first given a single module along with its call context and returns a structured plain language summary of the program logic, followed by targeted follow up questions about unclear spots. These follow up questions are often more revealing than the first summary itself, because they mark exactly the places where the code is ambiguous or inconsistent and a human subject matter expert should be pulled in for clarification, instead of accepting an unverified guess.


# Have Claude understand a single COBOL module deliberately, not translate blindly
claude "Read PROG-BILLING.cbl in full. Summarize the business purpose of \
  every paragraph section in plain language. Explicitly flag every spot \
  where the intent isn't clear from the code alone, and list open \
  questions for a subject matter expert separately. Don't translate yet.

3. Extracting and verifying business logic from undocumented code

Once a rough understanding is in place, the real work is extraction: turning business rules buried in procedural code into a language independent form that business departments can actually read. Claude can, for example, derive a decision table from a nested COBOL EVALUATE block that lists the same cases in plain language. What matters is that someone from the business side who knows the historical background then reviews these extracted rules, because Claude can only report what's actually in the code, not judge whether a rule is still wanted from a business standpoint or a forgotten edge case from the nineties.

This verification isn't an optional nice to have step, it's the actual core of the migration, because this is exactly where the correctness of the new system gets decided. A proven approach is presenting the extracted rule together with a concrete sample record showing how the rule acts on real input data, instead of describing it only in the abstract. That makes it quick to see whether the rule was understood correctly before it gets translated into new code.

4. Planning a strangler fig migration step by step instead of big bang

The strangler fig pattern, named after the fig plant that slowly envelops a host tree and eventually replaces it, transfers exactly that principle to legacy systems: instead of replacing the old system wholesale, a routing layer is placed in front of it that forwards requests to either the old or the new system. Module by module, functionality moves into the new system while the rest keeps running through the old one, until eventually nothing calls the original COBOL or Perl code anymore and it can be safely switched off.

Claude works well as a partner for deciding the order in which modules get migrated, using criteria like how often a module changes, how tightly it's coupled to other modules, and the business risk of a mistake. Modules with low coupling and manageable risk make good first candidates, because they let the whole process, from rule extraction to verification, get exercised with limited damage in case of error, before the truly critical core modules are tackled.


<?php
declare(strict_types=1);

// Routing layer in the strangler fig pattern: feature flag per module
final class BillingRouter
{
    public function __construct(
        private readonly LegacyCobolBridge $legacy,
        private readonly ModernBillingService $modern,
        private readonly FeatureFlags $flags,
    ) {
    }

    public function calculateInvoice(CustomerId $customerId): Invoice
    {
        if ($this->flags->isEnabled('billing.invoice.modern', $customerId)) {
            return $this->modern->calculateInvoice($customerId);
        }

        return $this->legacy->calculateInvoiceViaCobol($customerId);
    }
}

5. Generating characterization tests before the migration

Before touching a single line of legacy code, you need a safety net that captures the actual, not the assumed, behavior of the old system. That's exactly what characterization tests do: instead of checking whether the code is business correct, they simply document what it currently outputs for given inputs, quirks and historical baggage included. Claude can systematically derive test cases from a Perl module that run through various input combinations and lock in the observed result as the expected value.

These tests then run against the new module and immediately reveal if the migration introduced a behavior change, regardless of whether that change was intentional or a translation mistake. It's important to clarify every deviation explicitly: is it a bug in the old system that's deliberately not being reproduced anymore, or an actual defect in the new implementation? That distinction can only be made with business knowledge, not purely technically.


# Legacy Perl function with implicit context, a candidate for characterization tests
sub calculate_discount {
    my @items = @_;
    my $total = 0;
    $total += $_->{price} * $_->{qty} for @items;
    return $total > 500 ? $total * 0.9 : $total;
}

# Claude generates test cases from this that lock in current behavior
# before anything about the function gets changed:
# calculate_discount({price=>100, qty=>6}) == 540  (discount threshold exactly crossed)

6. COBOL data type pitfalls: COMP-3, rounding, and scale

COBOL programs frequently use packed decimal numbers via the COMP-3 clause for monetary amounts, and their rounding and scaling behavior doesn't map one to one onto modern floating point or decimal types without care. A PIC 9(7)V99 COMP-3 definition implicitly fixes two decimal places and a specific rounding behavior for arithmetic operations that can differ depending on compiler settings. If this nuance gets overlooked during migration, cent level discrepancies creep in that stay unnoticed on individual postings but add up to noticeable balance sheet differences across millions of transactions.

Claude can be asked deliberately to document, for every money relevant variable in the legacy code, the exact COMP-3 format along with its rounding behavior, and to propose a matching modern decimal type with identical precision, instead of defaulting to floating point numbers, which are fundamentally unsuitable for monetary calculations. It's then worth running a targeted test with boundary values like an amount of exactly 0.005 to check whether the new system produces the same result as the COBOL original under commercial rounding.

7. Perl specific pitfalls: context, implicit variables, and regex

Perl code from the nineties and early two thousands often relies on implicit mechanisms the language deliberately offers as convenience features, which become a trap for successors: the implicit variable $_, context dependent function behavior via wantarray depending on scalar or list context, and complex regular expressions with unclear back references. A developer reading such code for the first time easily misses that a function returns a completely different result depending on the calling context, which leads to silently incorrect behavior when translated into a language without this concept.

Claude can flag exactly these implicit spots before any translation happens, for example noting that a certain function returns a number in scalar context but an entire array in list context, and that both call sites in the code need to be identified. For complex regex patterns it's also worth explicitly asking Claude to break the pattern down into named, commented sub expressions, so the actual business intent behind the pattern becomes traceable instead of carrying over a cryptic string unchanged and unexamined.


# Context dependent behavior, an easy to miss Perl trap
sub get_customer_ids {
    my @ids = (101, 205, 309);
    return wantarray ? @ids : scalar(@ids);
}

# In list context: returns (101, 205, 309)
my @all = get_customer_ids();
# In scalar context: returns 3 (the count!), not the first id

8. Verification through parallel run and diff testing

Even with careful rule extraction and extensive characterization tests, there's a residual risk that a rare edge case in the real production data stream got overlooked. That's why running in parallel has established itself as the last line of defense: the new module runs in the shadow of the old system, processes the same real inputs, but its result is initially only logged, not delivered, while the old system's output stays authoritative.

Claude works well for building a diff script from the two logged result streams that doesn't just report deviations but groups them by pattern, for example all deviations for customers with a certain discount code or all deviations that come out to exactly one cent. This grouping makes systematic errors visible far faster than a plain list of individual differences, because a single incorrectly migrated rule fragment often shows up as dozens of similarly shaped deviations.


# Compare outputs of the old and new system during a shadow run
diff <(sort legacy_output_2026-08-07.csv) <(sort modern_output_2026-08-07.csv) \
  | claude "Group these diff lines by recognizable pattern (customer, \
  discount code, amount size) and estimate the most likely cause per \
  group within the migrated rule."

9. Migration phases at a glance

The following table summarizes the typical phases of a strangler fig migration along with the corresponding Claude use case and the biggest risk per phase.

Phase Claude use case Goal Typical risk
Inventory Read and summarize modules from a business angle Overview without existing documentation Unnoticed wrong assumptions carried forward
Rule extraction Derive decision tables from code Turn business logic into plain language Forgotten edge cases get missed
Building a test net Generate characterization tests Lock in current behavior Accidentally locking in bugs as intended behavior
Introducing a router Design a feature flag structure Enable gradual switch over Routing mistakes hit production traffic
Module migration Translate rules one by one into the target language Replace module by module Rounding and context errors during translation
Parallel run Group diffs between old and new system Surface residual risk before shutdown Rare edge cases in live traffic

Mironsoft

AI-assisted development, agent workflows, and team processes

Using Claude or other AI tools on the team, but without a clear workflow?

We set up AI-assisted development workflows for teams, from CLAUDE.md conventions to subagent strategies to code review processes that combine human oversight with AI speed.

Workflow Setup

Cleanly set up CLAUDE.md, project conventions, and tool permissions for the team.

Agent Strategy

Build subagent and automation workflows for recurring development tasks.

Team Onboarding

Train developers in productive, safe use of AI coding assistants.

10. Summary

Legacy Migration with Claude: The Essentials at a Glance

Core idea

Strangler fig migration replaces legacy systems module by module instead of in one risky step.

Key tool

Claude as code archaeologist, extracting business intent from undocumented COBOL and Perl code.

Biggest risk

Forgotten special rules and rounding nuances that only surface months after go live.

Success criterion

Grouped diff testing during a parallel run reveals deviations before the old system gets switched off.

11. FAQ: Legacy Migration with Claude: The Essentials at a Glance

1Why do big bang rewrites of COBOL systems fail so often?
Because business logic that grew over decades is documented nowhere in full and simply doesn't get reproduced in the rebuild.
2How does Claude help with understanding undocumented legacy code?
It reads modules in full, summarizes the business intent in plain language, and explicitly flags unclear spots for follow up questions.
3What is the strangler fig pattern?
A routing layer forwards requests to either the old or the new system while functionality gets migrated module by module.
4What are characterization tests?
Tests that lock in the actual, not the assumed correct, behavior of the old system as the expected value.
5Which COBOL pitfall affects monetary amounts most often?
The rounding and scaling behavior of COMP-3 fields, which doesn't automatically translate to modern decimal types.
6Which Perl quirk most often causes translation mistakes?
Context dependent function behavior via wantarray, which returns a completely different result depending on calling context.
7What's the purpose of running the systems in parallel before final cutover?
It surfaces rare edge cases in the real production data stream before the old system gets switched off.
8Can Claude decide on its own whether an extracted rule is still wanted from a business standpoint?
No, that always needs review from the business department with historical knowledge.
9Which modules make good first migration candidates?
Modules with low coupling and manageable business risk in case of a mistake.
10How are diffs between old and new system evaluated most efficiently?
Grouped by recognizable pattern instead of as a plain list, since individual bugs often show up as many similarly shaped deviations.