Modernizing a Legacy 8583 Engine Without Breaking It - Or Your Career


When last we left our ISO 8583 transaction processing engine, we’d materially improved the developer experience, the observability, and even our (and our team’s) understanding of the code base. We’d started looking at the logs and doing some trend analysis, and the basics were good - sub 50 milliseconds latency on most transactions.

But not all transactions.

There’s a process, without getting TOO into the weeds, where the legacy application could be asked to ‘dial home’ - specifically to send details about the current transaction to one of our partners, externally. And that partner could have bespoke business logic, based on things like the Merchant ID or Merchant Category Code, and effectively give themselves a ‘veto’ on the transaction. It sounds a little intense and Big Brother-y, but the examples are pretty mundane - someone using a commercial card for a property management company probably shouldn’t be buying movie tickets, for example - or imagine if an Alcoholics Anonymous branded card could be used to pay for a round at the bar.

There’s a whole other conversation to be had, and maybe I will at some point in the future, about the existance of this pathway. In short, my humble opinion is that it was added for totally valid reasons (partners who wanted to maintain their own ledger balances, for example), but grew into ‘business logic that doesn’t exist inside the primary application’. Not because it necessarily made sense, and not because it was the ‘right’ way to abstract out that business logic (making external partners build their OWN systems to handle it) - but mostly because the process of modifying the existing authorization application was a nightmare, and terrifying. So rather than handling it ‘in house’ by modifying the business logic of the authorization application, it was ‘easier’ (read, developmentally) to ‘make’ partners figure this stuff out for themselves. We’d give them the pieces, but they had to handle the logic.

Astute readers will probably have noticed the tradeoff, though - we were making our development life cycle easier, and reducing code changes to the legacy codebase.

But we were also introducing a TON of potential latencies to our poor single-threaded, ‘hot path only’ application. Namely - we are completely at the mercy of network latency, but also, of the latency of our partner’s systems, as well. There’s a two second timeout - if we haven’t heard a response from a partner within two seconds, we ‘stand in’ for the partner and act on the transaction ‘ourselves’, based on ledger balances of record in our system.

So - if you have a system, where runaway latency compounds and can eventually lead to busting your Service Level Objective - what do you do?

Attack the long tail. Keep the proven, low latency path - but find a way to offload the slowest path. Preferably in a way that’s asynchronous.

It felt like an obvious first slice at a Fig Strangler. If you’re not familiar with the pattern, reader, it goes something like this: replacing a monolith with a monolith is an errand in folly. You’re trying to incorporate some number of years of bugfixes, optimizations, improvements, and lessons learned (to say nothing of features!) into a ‘brand new’ application.

It’s hard. And especially if you’re trying to do all that at once, in a way where you have a hard cutover, and oftentimes you don’t have a ‘safe rollback’ option?

It’s a recipe for disaster. You’re going sky diving, but you’ve only tried your parachute while you’re standing in the hangar - never after throwing yourself out of an airplane.

Fig Stranger is named after, well, the Fig Strangler plant. It does sort of what you’d expect, from the name. It

I added sideband defer and enrichment emissions without changing the main processing path. In other words, the system started telling the outside world what it was about to do, and why, before I asked it to stop doing that work itself.

That was the right order.

Around the legacy engine, I started building Go components:

  • a lightweight listener that could receive and persist shadow events
  • broker-like control surfaces for deferral and later broker release
  • mirrored TCP traffic capture
  • correlation logic between mirrored wire traffic and legacy enrichment sidebands
  • and eventually a worker path that could process a deferred slice of traffic and return a final ISO response back through the broker path

This is where the work became especially interesting technically.

At first, the new worker path looked almost correct but was not actually owning the response. The broker was receiving defer signals, the Go side was receiving enrichment, and mirrored traffic was being captured, but correlation semantics were slightly off because one side was hashing framed ISO and the other was hashing unframed payloads. That kind of bug is deeply typical of integration work on old protocols: the architecture is sound, the implementation is close, and one subtle normalization mismatch means everything looks wired but the wrong component still wins the race.

Fixing that, notably, did not require more changes to the legacy code. I normalized the new Go services instead. That decision mattered to me. I trust greenfield Go more than I trust opportunistic edits to a brittle VB.NET codebase, and I want the new perimeter to be tolerant of legacy quirks rather than demanding the old process become cleaner before it can be surrounded.

Once the normalization was corrected, I got the proof I was after: a deferred 1121 path where mirrored traffic, legacy enrichment, Go-side processing, and broker release all aligned, and where the broker capture could explicitly tell me that the final response source was the broker path rather than the legacy engine.

That was the first moment where the modernization stopped being theoretical.

Why This Was Possible At All

A candid version of this story needs to include one more important point: the pace and quality of this work were only possible because I had a genuinely capable model helping me.

I do not mean that in the empty “AI wrote my code for me” sense. I mean something more practical and more valuable.

What made the model useful here was not just code generation. It was its ability to hold a large amount of architectural context in working memory and help connect several layers of the problem at once:

  • what the legacy VB.NET code was actually doing
  • what the database and stored procedures implied about business behavior
  • what a safe replay harness needed in order to validate that behavior
  • what observability primitives had to exist before I could trust any strangler work
  • and where it was wiser to absorb complexity into new Go components rather than push more risk into the old application

That connective tissue is hard, and it is usually where modernization efforts lose coherence.

I knew I needed guardrails and safety. I knew I needed a database baseline, a replay harness, a mockable external-auth path, structured capture, and a brokered deferred architecture. What the model was particularly good at was helping bridge the distance between that instinct and the concrete implementation details in front of me: which files to inspect, which branches were actually coupled to ledger behavior, where correlation was subtly wrong, which documentation needed to exist before code, and which local reproductions were worth prioritizing.

In short, it materially improved my ability to move quickly without moving recklessly.

The Biggest Win So Far

The biggest win is not that I now have some Go services.

The biggest win is that the legacy engine is no longer opaque.

There is now:

  • a reproducible local database baseline
  • seeded artifacts for deterministic edge-case coverage
  • a real replay and SQL-assertion harness
  • external-auth mocking for scenario-driven validation
  • structured capture and correlation around the legacy path
  • the first proven deferred broker/worker slice around the old engine

That changes the emotional shape of the system as much as the technical shape.

Before, changing the application meant operating mostly on caution and tribal knowledge. Now, changes can be grounded in replay, seeded data, observable behavior, and documented contracts. That does not remove risk. Nothing removes risk from a long-lived payments engine. But it makes the risk legible, and legible risk is something an engineer can manage.

What Comes Next

The story is not over. It has barely started.

The deferred 11xx path is a proof, not a final state. There is still mutation ownership to move. There is more external-auth parity work to do. The 12xx and reversal-related branches remain more complex and, in many ways, more urgent from a latency perspective. There are still places where the legacy engine owns behavior the new path only understands conceptually.

But this is the part that matters: there is now a path.

That path did not begin with a rewrite. It began with local reproducibility, deterministic testing, structured observability, and enough technical honesty to admit that the old engine needed to be understood before it could be replaced.

If there is a lesson here, it is that modernization of a critical legacy system is usually not about the new service first. It is about building the conditions under which the new service can be trusted.

In my case, that meant building the tooling and the visibility that should have existed years ago, discovering real bugs along the way, and proving that even a fifteen-year-old single-threaded 8583 engine can be surrounded, observed, and gradually relieved of responsibility without being detonated in place.