Rebooting Legacy Systems: Modernising COBOL Without Breaking Compliance
Bringing old code into the future safely

Contents
<p>Somewhere in a bank right now, a COBOL program written before the person maintaining it was born is settling a payment. It has run every night for forty years, it has never been fully understood by anyone currently employed, and it processes a volume of money that makes the idea of “just rewriting it” the sort of joke that gets people fired. COBOL turns out to be one of the most quietly durable technologies ever built: code compiled in the 1970s still runs unmodified on IBM’s current z16 mainframe, because IBM has held backward compatibility across every hardware generation since. That durability is exactly the trap. The systems work, so no one touches them, so the knowledge of how they work leaks away one retirement at a time, until you are left with a black box that mints money and terrifies auditors.</p>
<p>I have never run a mainframe — this is not a homelab job, and anyone who tells you they modernised a core banking ledger over a weekend is selling something. But I have inherited enough undocumented systems in my own infrastructure to recognise the shape of the problem, and the failure mode is universal: the moment you decide the safest thing to do is replace the scary old system with a shiny new one, you have made the most dangerous choice available. Let me explain why the “big rewrite” keeps killing these projects, and what actually works instead.</p>
<h2 id="why-cobol-refuses-to-die">Why COBOL refuses to die</h2><div class="ad-unit ad-in-article" aria-label="Advertisement">
<span class="ad-label">Advertisement</span>
<ins class="adsbygoogle" style="display:block;text-align:center"
data-ad-client="ca-pub-3726833845844946"
data-ad-slot="3291553914"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
<script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
</div>
<p>COBOL — the Common Business-Oriented Language — was designed in 1959 by a committee whose most famous member was Grace Hopper, and it was built for exactly one thing: reading, processing, and writing enormous quantities of business records with fixed-point decimal arithmetic that never rounds your fractions of a cent into oblivion. That last detail matters more than it sounds. Floating-point maths, the default in most modern languages, cannot represent <code>0.10</code> exactly; COBOL’s packed decimal can. For money, that is not a nicety, it is the whole job.</p>
<p>Sixty-odd years later, estimates put hundreds of billions of lines of COBOL still in production across banking, insurance, payroll, and government. It handles a large share of the world’s daily card transactions and a great deal of its benefits and tax processing. This is not legacy in the sense of “abandoned”. It is legacy in the sense of “load-bearing”. You are not modernising a museum piece; you are performing surgery on something with a pulse.</p>
<h2 id="the-rewrite-that-keeps-failing">The rewrite that keeps failing</h2>
<p>The instinct of every new technical leader who meets a COBOL system is the same: this is old, I am modern, I will replace it. The graveyard of failed modernisation programmes is full of exactly this ambition. The reason is straightforward once you see it. Those decades of COBOL do not just encode business logic; they encode <em>undocumented</em> business logic — every regulatory edge case, every “we handle Belgian VAT differently on Tuesdays”, every fix applied in a 3am incident twenty years ago and never written down. The code is the specification. Rewrite from a fresh understanding and you will faithfully reproduce your understanding, which is not the same as the system’s behaviour, and the gap between them is where the compliance breach lives.</p>
<p>McKinsey’s 2023 “Rewired” report put a number on it: organisations that modernise <em>incrementally</em> recover their investment roughly three times faster than those attempting full rewrites, and reach equivalent technical-debt reduction inside two years. The big-bang rewrite is not just riskier; it is slower and more expensive at achieving the thing it promised. This is the same lesson I have learned the hard way with far less consequential systems — the urge to throw it all out and start clean almost always costs more than the disciplined, boring path of changing one piece at a time. I make the same argument about the pressure to migrate off working infrastructure in <a href="/story/the-saas-trap-how-per-seat-pricing-pushes-you-toward-self-hosting/">the SaaS trap</a>: the working system you understand is worth more than the elegant one you have imagined.</p>
<h2 id="assess-before-you-touch-anything">Assess before you touch anything</h2><div class="ad-unit ad-in-article" aria-label="Advertisement">
<span class="ad-label">Advertisement</span>
<ins class="adsbygoogle" style="display:block;text-align:center"
data-ad-client="ca-pub-3726833845844946"
data-ad-slot="3291553914"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
<script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
</div>
<p>The first phase is not coding. It is archaeology. You cannot safely change a system whose behaviour you cannot describe, and in most COBOL shops the people who could describe it have retired.</p>
<p>Start by making the invisible visible. Static-analysis and source-mapping tools built for COBOL can trace data flows, chart which programs call which, and surface dead code and the exact points where the batch schedule fans out. The goal is a map: what feeds what, which modules touch regulated data, and where the genuinely load-bearing logic sits versus the accreted cruft. Only once you have that map can you rank modules by risk and by how cleanly they can be isolated — because isolation is the whole game.</p>
<p>Crucially, do the compliance review <em>before</em> you alter a single line of production. Anything touching financial reporting, personal data, or regulated calculations lives inside a change-control regime that mandates documented, reversible, audited changes. Discovering that after you have started is how a modernisation project becomes an incident.</p>
<h2 id="the-strangler-fig-how-it-actually-works">The strangler fig: how it actually works</h2>
<p>The pattern that survives contact with reality is the <strong>strangler fig</strong>, named after the tropical plant that grows around a host tree, gradually enveloping it until it can stand on its own and the original rots harmlessly away inside. In software terms: you build new functionality alongside the old system and redirect traffic to it one slice at a time, leaving everything you have not touched exactly as it was.</p>
<p>The mechanics start with an interface. You put an API layer in front of the mainframe so modern systems can talk to COBOL modules without knowing or caring what is inside them. The legacy code keeps doing its job, unchanged, as a backend behind a clean façade:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"> Modern clients (web, mobile, partner APIs)
</span></span><span class="line"><span class="cl"> │
</span></span><span class="line"><span class="cl"> ▼
</span></span><span class="line"><span class="cl"> ┌───────────────────────┐
</span></span><span class="line"><span class="cl"> │ API gateway / façade │ ← new code, versioned, tested
</span></span><span class="line"><span class="cl"> └───────────┬───────────┘
</span></span><span class="line"><span class="cl"> ┌───────────┴───────────┐
</span></span><span class="line"><span class="cl"> ▼ ▼
</span></span><span class="line"><span class="cl"> New service COBOL module
</span></span><span class="line"><span class="cl"> (reimplemented) (unchanged, via wrapper)
</span></span></code></pre></td></tr></table>
</div>
</div><p>Then, module by module, you reimplement a slice behind that façade — a reporting job, a batch step, one calculation — and flip the gateway to route that slice to the new implementation. The rest of the system does not notice. If the new slice misbehaves, you flip the route back. Nothing about a strangler-fig migration requires a heroic cutover night, which is precisely why it works and the big rewrite does not.</p>
<h2 id="compliance-is-a-testing-problem-in-disguise">Compliance is a testing problem in disguise</h2>
<p>Here is the part that separates a modernisation that passes audit from one that ends up in a regulator’s report: you have to <em>prove</em> the new module does exactly what the old one did, before it goes live, for every case that matters. The technique is a comparison harness. Run identical inputs through both the legacy module and its replacement and assert the outputs are byte-for-byte identical — including the edge cases, the negative numbers, the leap-year boundaries, the rounding behaviour you did not know existed.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Golden-master regression: replay real historical inputs through both</span>
</span></span><span class="line"><span class="cl"><span class="c1"># implementations and diff every field of the output. Any difference fails.</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="k">case</span> in test-cases/*.in<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"> <span class="nv">name</span><span class="o">=</span><span class="k">$(</span>basename <span class="s2">"</span><span class="nv">$case</span><span class="s2">"</span> .in<span class="k">)</span>
</span></span><span class="line"><span class="cl"> ./run-legacy-cobol <span class="s2">"</span><span class="nv">$case</span><span class="s2">"</span> > <span class="s2">"out/legacy/</span><span class="nv">$name</span><span class="s2">.out"</span>
</span></span><span class="line"><span class="cl"> ./run-new-service <span class="s2">"</span><span class="nv">$case</span><span class="s2">"</span> > <span class="s2">"out/new/</span><span class="nv">$name</span><span class="s2">.out"</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> ! diff -q <span class="s2">"out/legacy/</span><span class="nv">$name</span><span class="s2">.out"</span> <span class="s2">"out/new/</span><span class="nv">$name</span><span class="s2">.out"</span> >/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl"> <span class="nb">echo</span> <span class="s2">"MISMATCH: </span><span class="nv">$name</span><span class="s2">"</span>
</span></span><span class="line"><span class="cl"> diff <span class="s2">"out/legacy/</span><span class="nv">$name</span><span class="s2">.out"</span> <span class="s2">"out/new/</span><span class="nv">$name</span><span class="s2">.out"</span>
</span></span><span class="line"><span class="cl"> <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Feed it real historical transaction data, not synthetic examples — the whole point is to catch the behaviour nobody documented. Every mismatch is either a bug in your replacement or an undocumented rule you have just rediscovered, and both are gold. This “golden master” approach turns compliance from a paperwork exercise into an executable check, and it gives you the evidence trail auditors actually want: proof, per module, that behaviour was preserved.</p>
<h2 id="gotchas-and-troubleshooting">Gotchas and troubleshooting</h2>
<p><strong>Character encoding will ambush you.</strong> Mainframes store text in EBCDIC, not ASCII or UTF-8. When you bridge COBOL data out to a modern service, a mishandled code-page conversion silently corrupts anything non-numeric — names with accents, currency symbols, packed fields misread as text. If your comparison harness shows mismatches only on records with certain characters, suspect EBCDIC translation before you suspect your logic.</p>
<p><strong>Packed decimal is not a <code>float</code>.</strong> Reimplement a COBOL monetary calculation in a language that defaults to floating point and you <em>will</em> eventually be off by a cent on some transaction, and in regulated finance a systematic one-cent error is a reportable defect, not a rounding quirk. Use a proper decimal type (<code>BigDecimal</code>, <code>decimal</code>, an arbitrary-precision library) and mirror COBOL’s rounding mode exactly.</p>
<p><strong>Batch windows are load-bearing.</strong> These systems run on a nightly schedule where job B assumes job A finished first. Reroute one job to a new service and you can quietly break an ordering dependency that only manifests at month-end. Map the batch dependency graph before you touch the schedule, and monitor the whole chain, not just the piece you changed. Overhauling monitoring on this kind of system is its own project — the same discipline I describe for humbler cron pipelines in <a href="/story/healthchecksio-self-hosted-making-sure-your-cron-jobs-actually-ran/">Healthchecks.io: making sure your cron jobs actually ran</a> applies at mainframe scale, just with far higher stakes.</p>
<p><strong>Knowledge is the scarcest resource.</strong> The pool of COBOL veterans shrinks every year. If your migration depends on one person who understands the payroll module, that person is a single point of failure worse than any hardware. Pair juniors with them, record the sessions, document as you go — treat institutional knowledge as data to be backed up before it is lost, because unlike the code, it does not run unmodified on a z16.</p>
<h2 id="is-it-worth-it-and-who-is-this-for">Is it worth it, and who is this for?</h2>
<p>If you are a hobbyist reading this out of curiosity, the honest answer is that you will almost certainly never touch a line of production COBOL, and that is fine — but the <em>pattern</em> is worth stealing. The strangler fig and the golden-master regression harness are exactly how you should approach any inherited, undocumented, load-bearing system, mainframe or not. I have used both on my own kit with nothing more exotic than an old PHP monolith.</p>
<p>If you are actually responsible for one of these systems, the verdict is uncomfortable but clear. The modernisation cannot be avoided forever — the expertise is retiring and the integration pressure is real — but it also cannot be rushed, and the fantasy of a clean rewrite is the thing most likely to get you breached, fined, or fired. Go slowly. Map before you cut. Wrap before you replace. Prove equivalence before you cut over, one module at a time, and keep the auditors alongside you the whole way. It is a marathon measured in years, not a sprint measured in sprints, and the organisations that respect that are the ones whose payments still settle correctly at 3am. The reward is not glory. It is that nothing breaks, which in this line of work is the highest praise there is.</p>
Advertisement
Related Content
Advertisement




