That Time I Learned Banks Still Code Like It's 1975
A journey into the hellscape of fixed-width ASCII files, COBOL, and why your modern web app needs to speak mainframe

Got a strange email today. Client has some “local soft” running, it’s supposed to get their latest financial data from our server. The email says their invoice integration is broken. The system can’t read our data. “The file must be ASCII with fixed spacing.”
ASCII. Fixed spacing. I stare at my screen. It’s 2026. We have AI that can write poetry and drive cars. And somewhere, a banking system is demanding I format data like it’s being punched into cards and fed into a room-sized computer.
What the hell is a fixed-width file?
You know how normal people use CSV? Or JSON? Formats where you separate fields with commas or structure data with brackets? Where “John Doe” can be 9 characters in one row and “Kovalev Alexander Sergeevich” can be 28 in the next?
Fixed-width files don’t do that. Every field starts at an exact character position and has an exact length. Always.
[127 spaces] 12345000000000000002000000000000050That’s not a mistake. The item code lives at characters 128-147, 20 characters wide, no more, no less. If your item code is only 5 digits, you pad it with 15 spaces. If it’s 21 digits? Tough shit, you’re truncating.
The spec comes as two PDFs in Hebrew. One defines the header structure:
- Position 1: file type (1 character)
- Position 19-43: customer name (25 characters)
- Position 45-69: address (25 characters)
- Position 112-126: total amount (15 characters)
The other defines line items:
- Position 128-147: item code (20 characters)
- Position 149-163: quantity (15 characters)
- Position 165-179: price (15 characters)
Notice something? The header fields start at position 19. The line fields start at position 128. They’re not supposed to share a line, that’s just how the format works. Which means 127 characters of nothing before (or after) your actual data.
10 20 30 40 50 60 70 80 ----+----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8 0·················Kovalev Alexander Sergeev·רחוב הרצל 5···············תל אביב···
25/25 columns · chopped:ich
Why would anyone do this?
I had to look this up because I genuinely couldn’t believe it. This format comes from IBM mainframes in the 1960s-1980s. Banks, insurance companies, government systems—they built everything on these machines. Systems running COBOL, processing data in fixed-width formats because:
- Speed - The computer knows character 128 is always the item code. No parsing needed. Jump directly to position.
- Binary safety - You can’t use delimiters when your data might contain those delimiters. Fixed positions eliminate the problem.
- COBOL expects it - Millions of lines of business logic written in COBOL that would cost billions to rewrite.
When the data is large (Giga/Terra-bytes), fixed width format files can be MUCH more efficient.
Since each record and field has fixed sizes, you can simply seek to the (for example) n-millionth row and read a couple of records from there. You can also memory map the whole file into memory and get rather efficient and easy random access to everything.
They will never upgrade. Why would they? The system works. It’s processed trillions of dollars in transactions. It’s stable. It’s tested. Replacing it would be a multi-billion dollar project with massive risk.
So instead, every modern web developer who needs to integrate with these systems has to time-travel back to 1975 and learn to speak mainframe.
The code I inherited
$data .= $year . str_pad($order_id, 6, "0", STR_PAD_LEFT) . '|' . $order->get_date_created()->format('d/m/y') . '|' . mb_str_pad($order->get_formatted_billing_full_name(), 25, " ", STR_PAD_LEFT) . '|' . mb_str_pad($order->get_billing_address_1(), 25, " ", STR_PAD_LEFT) // ... continues for 12 more fieldsSee those pipe characters? That’s the problem. This creates a delimited file:
26240001|19/01/26|יוסי כהן|רחוב הרצל 5|...But the mainframe wants:
0יוסי כהן רחוב הרצל 5 תל אביב 1234567...No pipes. No delimiters. Just exact positions. Always.
The error message
The system sent back an error file. In Hebrew. Encoded in Windows-1255 (another relic from the past). When decoded:
תקלה ASCII במספר שורה בקריאת: 1שדה מספר 64 אינו תקין: 26 |00000000039Translation: “Field 64 is broken. You sent: 26 |00000000039”
Field 64 should be a 15-digit number: 000000000000026
We sent: 26 |00000000039 (wrong length, has a pipe symbol, not zero-padded)
The fix
Remove all delimiters. Pad everything to exact lengths. Position fields at exact character positions.
function formatHeaderRecord($order) { return str_pad('0', 1) // pos 1: file type . str_repeat(' ', 18) // pos 2-18: empty . mb_str_pad($order->billing_name, 25, ' ') // pos 19-43: name . mb_str_pad($order->billing_address, 25, ' ') // pos 45-69: address . mb_str_pad($order->billing_city, 15, ' ') // pos 71-85: city . str_repeat(' ', 26) // pos 86-111: empty . str_pad(formatNumber($order->total), 15, '0', STR_PAD_LEFT); // pos 112-126: total}
function formatLineRecord($item) { return str_repeat(' ', 127) // pos 1-127: empty . str_pad($item->sku, 20, ' ') // pos 128-147: item code . str_pad(formatNumber($item->quantity), 15, '0', STR_PAD_LEFT) // pos 149-163: qty . str_pad(formatNumber($item->price), 15, '0', STR_PAD_LEFT); // pos 165-179: price}127 spaces. Then your data. Because some programmer in 1975 decided that’s where the item code goes, and nobody’s been allowed to change it since.
What I learned
Your code lasts. That bullshit function I’m writing today? Tomorrow I’ll forget about it. In a year it might be rewritten in rust. But if I write it well enough, it just might outlive me. Some developer in 2077 will stumble upon my WooCommerce integration, cursing my name, wondering why field 64 starts at position 112.
The COBOL programmer who wrote this banking system in 1975 probably thought they were just solving today’s problem. Write a spec, process some invoices, go home. Fifty years later, their code is processing trillions of dollars. Banks merged, acquired, went bankrupt—the code kept running. Programmers retired, died—the code kept running.
Keep that in mind the next time someone pitches “rebuilding the legacy”. Especially now, when every new GPT wrapper makes code feel cheaper than ever and a rewrite looks like a weekend job. It is tempting. Just remember:
most code lasts for a reason.
And while I’m on my soapbox, here’s the one thing worth salvaging from this mess: this is what good code is. Not clever, not flexible, not modern. Just clear, documented, and specific enough that someone in 2075 can still understand what the hell field 64 is supposed to contain.
So yeah, I’ll write my fixed-width formatter. I’ll pad my strings. I’ll count character positions like some monk copying manuscripts. And I’ll leave a short readme (yes, Claude, short), because maybe, just maybe, if I write it well enough, some poor developer in 2077 will thank me.
Now if you’ll excuse me, I have 127 spaces to pad.
Useful links
Excel date mismatch and off-by-one errors, another legacy file format that bit me
https://www.easydatatransform.com/help/latest/windows/html/index.html?fixed_width_format.html https://stackoverflow.com/questions/7666780/why-are-fixed-width-file-formats-still-in-use
Comments
Comments live in GitHub Discussions (via giscus), so you need a GitHub account to join in.