We Rewrote a 5,600-Line ColdFusion Backend in Python — With Zero Downtime and 145 Tests Proving It
A live application on an aging ColdFusion backend that couldn't run on modern Linux. The lift-and-shift would have cost 1–2 GB of RAM and locked the client into a runtime they were trying to leave. So we rewrote the whole thing — carefully. Here's exactly how, including the four bugs we found just by reading the old code.
A client had a capable, live product: a recipe and meal-planning platform with PDF/URL/photo recipe import, AI-assisted categorization, meal planning, shopping lists, and analytics. It worked. But the backend was ColdFusion — about 5,600 lines across 11 files — and their target Linux hosting doesn't run ColdFusion natively. (The owner didn't even know the backend was ColdFusion; they knew about the Python that powered the AI features, not the CFML underneath.)
There was a fast path: install a Java-based CFML engine on the new server and move the files. We turned it down on purpose. That path carries a persistent JVM service — 1–2 GB of RAM and a second runtime to maintain — forward forever, and it keeps the client married to exactly the technology they wanted to leave. The application's AI tooling was already Python. Consolidating onto one language meant one stack, one runtime, one set of skills, and lightweight native hosting.
So the brief — get it online, on modern infrastructure, without losing anything — became a full backend rewrite. The hard part of any rewrite is the silent behavior change: the new system looks done but quietly does something different. We engineered the project specifically to make that impossible.
Parity is a feature you have to build
Five rules ran the whole engagement:
- Mirror the old contract exactly. Same endpoints, same JSON response shapes. The React frontend needed only a base-URL change — and old and new responses could be diffed directly.
- Never touch production. The rewrite lived in a parallel codebase. The original stayed live as both reference and fallback the entire time.
- Develop against a cloned database. Write operations were exercised on a clone, so live data was never at risk during the port.
- One component at a time, tests first. Each ColdFusion component was ported under its own plan, tests written before the code, with an independent review before moving on. A diffing harness compared each new endpoint against the original to prove parity.
- Phase it. Phase 1 reached full feature parity locally. Phase 2 handled deployment and cutover. The client could see a complete, working system before any infrastructure changed.
The result was 145 automated tests proving the new backend matches the old one method-for-method — a regression safety net the original never had.
Reading every line found bugs that had been live for years
Matching behavior exactly means actually reading every method — and that surfaced bugs the old system had carried silently:
- A recipe-count query left its LIMIT/OFFSET in place, so the reported total was capped at the page size. Pagination had been quietly wrong.
- An AI “suggest leftovers” helper flattened its data with keys that didn't exist — it threw every time it was called.
- One endpoint accepted a parameter named
idwhile the UI sentleftoverId. They'd been talking past each other.
We fixed each one in the new system rather than faithfully reproducing it. A rewrite is the moment to correct, not just translate — as long as you document each divergence so “fix” never becomes an accidental behavior change.
Security: harden it without changing what it does
A full adversarial security audit drove a hardening pass, and this is where discipline matters most. The ColdFusion original made nearly every read method public — the entire recipe library, meal plans, family members, arbitrary user settings. We cut the public surface from 27 methods to exactly one (login); everything else now requires authentication. We added login rate-limiting, SSRF protection on the URL-import feature, path-traversal jails on every file operation, upload limits, constant-time credential checks, and a stronger password scheme that transparently upgrades each user's stored hash on their next login — no one gets locked out, no one resets a password.
The rule we held to: a security change must be invisible to legitimate users. When an early pass altered which features were reachable, we reverted that part. Security improvements that users never notice were kept; anything that changed product behavior was rolled back to match the original exactly. Tightening security is not a license to quietly change the product.
Code is the easy part — state is where rewrites die
A modernization isn't just code; it's three kinds of state, each migrated its own way:
- Relational data copied to the production database and validated — which caught several column defaults that an earlier database-engine migration had silently dropped.
- User uploads (the library of recipe images and PDFs) seeded once, then kept on the server and deliberately out of version control, so a deploy can never overwrite live user files.
- Secrets moved into server-side environment configuration, never committed — so credentials never touch the Git history.
Get those boundaries wrong and you get the classic modernization disasters: a deploy that wipes user uploads, or API keys leaking into a repo.
The finish line: one push
Delivery is a single git push. The server pulls the change, rebuilds the frontend, and gracefully restarts the application — no SSH, no manual steps, no downtime, and intentionally designed to leave user data and live configuration untouched. The database runs as a high-availability cluster with automatic failover the application handles transparently.
What it added up to
ColdFusion gone. The 1–2 GB JVM runtime gone. The codebase roughly halved. 145 tests where there were none. Four long-standing bugs fixed. Zero data lost, and the original system running safely the whole time.
Five things this teaches about legacy modernization
- Parity is engineered, not hoped for — match the exact contract and diff against it.
- A rewrite is the moment to fix, not just translate — but document every divergence.
- Security improvements should be invisible — if a “security” change alters behavior, it's a product change in disguise.
- State migrates differently than code — relational data, uploads, and secrets each need their own plan.
- “Reachable in production” is its own milestone — access rules, file ownership, runtime config, and deploy automation are where a prototype becomes a product.
If you've got an aging system that still runs the business but is getting harder to host, hire for, or extend — that's exactly the kind of work we do. Book a discovery call.