Faithful translation is not the safe option
The instinct on a mainframe rule migration is to be faithful. Reproduce the outputs exactly, byte for byte, on a year of production input. It sounds like the conservative choice and it is how most of these programs are scoped, because it is the only acceptance criterion a contracting officer or an audit committee can evaluate without understanding the rules. It is also how a program ends up shipping a modern system that quietly contains every mistake the old one made, now expressed in Java, now covered by unit tests that assert the mistake, now much harder to argue with.
That is what laundering means here. A defect in a 40-year-old COBOL program is visibly a legacy artifact. Everyone in the building treats the mainframe as suspect, and when a number looks wrong somebody goes and reads the copybook. Move the same defect into a new service with a clean domain model and a green test suite, and its status changes. It is now the specified behavior of the system of record, pinned by a passing test that some engineer wrote deliberately, and the next person who questions the number gets told that the logic was validated during modernization. The defect did not get fixed and it did not get worse. It got authority.
This matters commercially, not just aesthetically. A rating rule that rounds the wrong direction is worth a few cents per transaction until an auditor multiplies it by eleven years of volume. An eligibility rule that excludes soft-deleted rows because a 1991 query never learned about them is a compliance finding waiting for a plaintiff. The translation project is the last cheap opportunity anyone will have to look at these rules. Once it ships, the cost of touching a rule goes from a conversation to a change request.
What The Rule Set Actually Contains: Typical Split After Classification
Editorial weighting from published modernization program records and practitioner reading — illustrative, not a measured statistic. The last two rows are small and they are the whole reason to do the classification.
Three things that look identical in the source
Open any long-lived rule module and the code gives you no way to tell these apart. An IF that implements a statutory threshold, an IF that patches a bad feed from a system decommissioned in 2003, and an IF a developer added on a Friday to stop a support call all have the same shape. There is no comment convention that survived four decades of maintainers. The distinction exists only outside the code, in provenance.
So the first deliverable of a rule translation is not code. It is a classification of every rule into one of four states. Policy, meaning it traces to an external authority you can name: a section of statute, a rate filing, a contract clause, a published schedule. Practice, meaning the business genuinely intends it and it appears in no document, which is common and legitimate and needs to be written down now. Artifact, meaning it exists to serve a constraint that no longer applies. And defect, meaning the business owner reads it and says that is not what we meant.
The classification is cheap relative to what follows. On a rule set of a few thousand decision points, expect four to ten weeks of two engineers plus meaningful time from one or two people who actually know the business. That is real money, and it is a fraction of a translation program, and it is the only phase where finding a bad rule costs an afternoon instead of a change order.
Pin the behavior first, including the behavior you dislike
Before any classification argument, capture what the system does. Michael Feathers named the technique in Working Effectively with Legacy Code: a characterization test records actual behavior rather than asserting intended behavior. Feed a year of real production input through the current system in a copy of the production environment, and store every output record, every reject, every generated notice, every downstream feed, keyed to its input.
Pin the wrong answers too. A test that captures the rounding defect is not an endorsement of it. It is the evidence that lets you prove, later, that the new system changed exactly that one behavior and nothing else. Programs that quietly correct defects during translation lose the ability to explain any difference, and every diff in the reconciliation report becomes an argument instead of a finding. Pin everything, then change things deliberately, one at a time, with a ticket per change.
The capture also does scope reduction for free. Dead paths show up immediately as branches no input reaches, and on a mature batch system that is routinely five to ten percent of the module count. You still classify them, because a path unreached in a year may be the annual reconciliation nobody ran yet, but most of them are genuinely gone and removing them from scope pays for the capture phase by itself.
The COBOL details that become silent bugs in Java
Some of the worst laundering happens in mechanical translation, where nobody made a decision at all. COBOL COMP-3 packed decimal is fixed-point base-ten arithmetic. Translate a PIC S9(7)V99 COMP-3 field into a double and you have introduced a class of error that will not appear in a smoke test and will appear on a month-end total. Money belongs in BigDecimal with an explicit scale and an explicit RoundingMode, or in a database numeric type, never in binary floating point. This is not a subtle point and it still ships regularly, because the developer writing the mapping was moving fast and a double holds the value fine in the debugger.
Rounding direction is the same story with worse consequences. COBOL ROUNDED without a phrase is round-half-away-from-zero. Java's default in several common paths is half-even. Across millions of transactions the difference is systematic, not random, and it lands on one side of the ledger. Every arithmetic statement in the source needs its rounding mode read and carried explicitly rather than inherited from whatever the new language does by default.
Then there are the encoding and layout details. EBCDIC-to-ASCII conversion mangles the sign nibble on packed fields if it is applied to a whole record rather than field by field. REDEFINES means one storage area holds different layouts depending on a discriminator that lives somewhere else, and a translator that picks the first definition silently reads the wrong field for a subset of records. OCCURS DEPENDING ON makes record length variable. Group moves copy bytes without regard to the receiving field's type. None of these produce an error. All of them produce a number that is wrong for a minority of inputs, which is exactly the profile that survives testing and reaches production.
| Legacy construct | The naive translation | How it fails | What to do instead |
|---|---|---|---|
| COMP-3 packed decimal | double or float | Fractional cents accumulate on totals; reconciliation drifts by month-end, not by transaction | BigDecimal with declared scale, or a database numeric type. Assert scale at the boundary |
| ROUNDED clause | Language default rounding | Half-away-from-zero becomes half-even. Systematic bias in one direction across volume | Read the mode per statement and set RoundingMode explicitly. Never inherit a default |
| REDEFINES | First layout wins | Wrong field read for whatever record type the discriminator selects. Right for most rows | Model the discriminator explicitly as a tagged union. Fail loudly on an unknown tag |
| Two-digit or Julian dates | Parse with a century window | The window is a rule nobody wrote down. Wrong for the tails, correct in every test | Find the pivot in the source, classify it, then store full dates with the pivot documented |
| Filler and reused fields | Ignored as padding | A field marked FILLER carries meaning added in 1998 and never renamed | Check FILLER against real data before dropping it. Nonzero bytes mean somebody used it |
| GO TO into a paragraph | Straight-line method extraction | Control flow that reenters mid-block is dropped; the rule only fires on some paths | Build the control flow graph, then rewrite from behavior rather than from statement order |
Ask the question that separates policy from accident
The classification interview has one useful question and it is not "is this rule correct." Asked that way, a business owner defends the system, because the system is what they have been running for twenty years. The question that works is: if we were writing this today, with no existing system, would you write this rule? That reframes the rule as a proposal rather than a fact, and the answers get honest fast.
Follow it with provenance. What authorizes this? A statute section, a filed rate, a contract clause, a board decision with a date. If somebody can name the authority, the rule is policy and you translate it exactly and cite the authority in the new code. If nobody can, you have found either practice worth writing down or an artifact worth deleting, and the difference is usually obvious within one conversation.
Record the answers where the new system lives, not in a document that dies at go-live. A rule carrying a provenance field is a rule the next engineer can evaluate without archaeology. This is the same discipline financial institutions have applied to model documentation under the Federal Reserve's SR 11-7 guidance for over a decade, and the reason it survived is that it works: a decision with a named owner, a named authority and a date is a decision somebody can revisit. A decision embedded in code is a decision nobody will touch.
Make the rules data, not control flow
The translated rule set should not be a pile of nested conditionals in a service class, because that reproduces the original problem in a newer language. Rules that were hard to find in COBOL are equally hard to find in a 900-line Java method. Represent them as data: decision tables, a rule registry, or DMN if the organization can carry the tooling.
The reason is not elegance. It is that data can be queried, and control flow cannot. When an auditor asks which rules apply to a given product line, a table answers in a query and a codebase answers in a two-week code read. When a rate changes, a table changes in one row with an effective date, and code changes in a release. When somebody asks how many rules have no traceable authority, a provenance column answers it and a codebase does not.
Keep the boundaries honest. Rules that are genuinely algorithmic, such as an amortization calculation, belong in code with tests rather than forced into a table. The split to aim for is that anything a business person might change belongs in data, and anything only an engineer would change belongs in code. Getting that line right is most of the design work in the new engine.
Run both systems and classify every difference
Parallel run is standard practice and it is usually done badly. Teams feed the same input to both systems, count the mismatches, and chase the number toward zero. A dashboard reporting a 0.4 percent difference rate tells nobody what to do. Store the diffs as full samples, input and both outputs, and classify each one into exactly three buckets.
New system is wrong: fix it. Old system is wrong and we are changing the behavior on purpose: that diff has a ticket, an approval, and a dated decision, and it stays in the report permanently as an expected difference rather than being suppressed. Old system is wrong and we are keeping the behavior anyway, usually because customers or downstream systems depend on it: that also gets written down, because an intentional bug is a legitimate engineering decision and an undocumented one is a liability.
Set the acceptance gate in terms of unexplained diffs, not total diffs. Total diffs going to zero means you translated everything faithfully, defects included, which is the failure this article is about. Unexplained diffs going to zero means every remaining difference is a decision somebody made with their name on it. Those are opposite outcomes and only one of them is worth shipping. Run the parallel long enough to include the slow cycles, which means at least one month-end close and, on anything annual, the annual run. Three days of agreement proves the common path and nothing else.
Cutover Gates: What Has To Be True Before The New Engine Is Authoritative
Write the numbers down before the schedule pressure arrives. A gate negotiated the week of cutover is not a gate.
What the automated translators do and do not give you
Commercial COBOL-to-Java converters and the newer LLM-assisted translation tools are genuinely useful and they solve the easier half of the problem. They handle syntax, data division mapping, control flow restructuring and the bulk mechanical work that would otherwise consume months. On a large codebase they are worth the license.
What they produce is a faithful translation, which is precisely the thing this article argues is insufficient. A converter cannot know that the threshold on line 4,190 is a statutory figure and the one on line 4,205 is a workaround for a feed that stopped existing. It has no access to provenance, because provenance was never in the source. Run the converter, then do the classification against its output rather than against the COBOL if that is easier to read. The tool changes the cost of the mechanical work. It does not change the amount of judgment required, and a program that treats converter output as the finished system has simply automated the laundering.
The LLM-assisted variants add a specific new failure to watch for: they produce plausible code for constructs they did not fully parse, and the output reads correctly. A hand-written mistranslation usually looks wrong to a reviewer. A generated one usually does not. Differential testing against the characterization corpus is the control that catches this, and it needs to be in place before the generated code is reviewed, not after.
Who has to be in the room
This work fails on staffing more often than on technique. It needs three kinds of people at the same time and most organizations can field two. Someone who can read the legacy source fluently, including the constructs nobody writes anymore. Someone who owns the business outcome and has actual authority to say a rule is wrong. And engineers who will build the new engine and have to live with the classification decisions.
The failure mode is running these sequentially. A team reads the COBOL for two months, writes a specification, hands it to the business for review, and the business reviews a document rather than a decision. Nobody says a rule is wrong when reviewing a 200-page specification, because the cost of raising a question is high and the specification arrives already looking finished. Run them together in working sessions against real rules and real outputs, a few hours a week, over the classification period. The questions get asked because the format invites them.
Where the legacy expertise has retired, and it often has, the characterization corpus substitutes for a good deal of it. You can recover much of what a rule does from inputs and outputs without anyone who remembers writing it. What the corpus cannot recover is why, and that is exactly the gap the classification interview with the business owner is there to fill.
What this costs and what it buys
On a rule set of two to five thousand decision points, expect the characterization capture to run six to twelve weeks including environment work, the classification to run four to ten weeks overlapping it, and the parallel run to occupy three to six months of calendar without occupying much labor. Against a translation program measured in years, the added classification work is commonly ten to fifteen percent of program cost.
What it buys is specific. A rule set where every rule has a named authority or an explicit decision that it has none. A permanent record of every behavior change made during migration, with dates and approvals, which is the artifact that answers an audit question in an hour instead of a quarter. Scope reduction from the dead paths found during capture, which frequently covers a meaningful share of the classification cost. And the defects, found while they are still cheap to argue about.
The alternative is not cheaper. It defers the same work to whoever asks the first hard question about a number after go-live, at which point the answer requires reading a new codebase to recover reasoning that was available for the asking during the migration and got thrown away.
Bottom line
A mainframe rule set is not a specification. It is a sedimentary record of four decades of decisions, patches, and constraints, and the code preserves no distinction between them. Translating it faithfully is a defensible engineering choice only if somebody has first determined which parts deserve fidelity. Capture the behavior, classify every rule by provenance, translate the arithmetic with explicit types and explicit rounding, represent the rules as queryable data, and run in parallel until the only remaining differences are ones a named person decided to make. Do that and the new system is better than the old one in the way that matters, which is that its behavior is known. Skip it and you have paid for a rewrite that bought you a faster copy of what you already had.
Frequently asked questions
It is the most measurable one, which is why it gets chosen. It is safe only for the rules that are correct. For the rest it guarantees the defect ships, now backed by a passing test. The better criterion is zero unexplained differences, with every intentional difference carrying a dated approval.
The characterization corpus recovers what a rule does from real inputs and outputs without needing the original author. What it cannot recover is the authority behind it, so the classification interview shifts to current business owners and to document search: rate filings, statute sections, contract clauses, board minutes. A rule with no findable authority is a decision to make now, not a mystery to preserve.
They remove much of the mechanical cost and none of the judgment. A converter has no access to provenance because provenance was never in the source, so it faithfully carries defects forward. Use the tool, then classify against its output. LLM-assisted translation adds a specific risk: mistranslations that read as correct code, which differential testing against the corpus is what catches.
Monetary arithmetic moved from COMP-3 packed decimal into binary floating point, combined with an unexamined change in rounding direction. Neither produces an error. Both produce systematic drift that appears at month-end totals rather than in per-transaction tests.
Long enough to cover the slow cycles, which means at least one month-end close and, for any system with annual processing, one annual run. Three days of agreement proves the common path works and says nothing about the paths that fire twelve times a year.