← Back to blog

Reversing a decision I had argued well: Python to Go

In June I wrote a decision document recommending we stay in Python and revisit a Go migration at twenty customers. It was well argued and I still think the reasoning was correct. In August I rewrote the whole thing in Go anyway. This is what changed, and why "I argued the opposite" is not an argument.

The June assessment

The question was how to package a container that runs inside someone else's cloud tenant while keeping the intellectual property from being trivially extracted. Two options: compile and obfuscate the Python, or rewrite in a compiled language.

The recommendation was Python, and the reasoning held up well:

Conclusion: launch on Python, revisit at twenty customers or on a hard scale-to-zero requirement. Every line of that is defensible and I would write it again with the information I had.

What actually changed

Not the facts. The weights.

The June assessment treated tamper resistance as an engineering preference — something you want, ranked against other things you want. By August it had become part of a commercial claim. The whole sales conversation with a security buyer runs through what this opaque binary is doing in my tenant, and the answer "it is a Python application with an obfuscation layer over it" is materially weaker than "it is a single static binary", not because the second is unbreakable but because the first invites a class of question the second does not.

Image size moved from a footnote to a real operational fact: roughly a gigabyte of Python runtime and dependencies against twenty to thirty megabytes of static binary, pulled into a fresh host in every customer tenant, on every deployment, forever.

And the SDK maturity gap — the strongest technical argument in the June document — had narrowed while I was busy elsewhere. Arguments made against a moving target expire, and nothing in my process was set up to notice that one had.

FactorJune weightAugust weight
Time to productionHigh — dominantLower — no customers waiting
Tamper resistanceEngineering preferencePart of the commercial claim
SDK maturity gapHigh — decisive againstNarrowed materially
Image size and startupFootnotePer-tenant operational cost, permanently
Rewrite riskHighLower — thin dependencies, separated domain logic

How the port ran

In nine waves, over roughly two weeks, with both trees alive simultaneously and the Go data models held byte-compatible with the Python ones so that audit records and state files written by one could be read by the other. That compatibility contract was the thing that made it survivable — at no point was there a flag day, and at every point there was a working system.

On the seventh of September the Python tree was formally dropped, the parity contract released, and new fields started being added to the Go models alone. Writing that decision down as a dated record mattered more than it sounds: until it existed, every new field was an implicit question about whether we were still maintaining two implementations.

What made a three-to-six-month estimate land in two weeks. Thin dependencies and a thirty-line agent loop. The June estimate was honest for a framework-coupled codebase. It was wrong for this one, and I had built the thing that made it wrong without realising I was buying that option.

What the rewrite actually cost

The hard parts were exactly where you would predict and nowhere else: query builders with accumulated schema knowledge, authentication plumbing, and the dozens of small behavioural details that were not written down anywhere because they lived in the Python code as the only specification.

That last category is the real tax on any rewrite. Not the code — the undocumented decisions embedded in it. Every one of them has to be rediscovered, usually by finding out at runtime that something no longer works the way it used to.

The meta-point

A well-argued decision is not a permanent one, and having argued a position publicly and carefully creates a real pull toward defending it. That pull is the thing to watch for. The June document was not wrong; it was correct under June's weights, and weights change because the business changes around them.

What I would do differently is small and concrete: when writing a decision document, name the specific conditions that would reverse it, and date them. My June assessment did include revisit triggers — twenty customers, or a hard scale-to-zero requirement — and the actual reversal came from none of them. It came from tamper resistance being promoted from preference to sales requirement, which was not on the list because I had not yet understood which conversation would decide the sale.

The triggers you can write down are the ones you already know about. The one that gets you is the variable that changes category.

Who is writing this

I am Ivan Melekhin. Twenty-five years in cybersecurity, most of the last decade running security operations — building and operating distributed SOC and MSSP teams across Asia-Pacific, with a long detour through OT and maritime environments. This log is the build record for an autonomous SOC investigation agent I started in January 2026, written as the decisions happened rather than tidied up afterwards. I am on LinkedIn if you want to argue with any of it.