Around 2014, I joined the web team at a web security company as their team lead, and the flagship product had one specific problem you noticed on your very first day: login took roughly two minutes. Not “felt slow.” Actually two minutes on a stopwatch. The rest of the app was equally slow, but login was the first thing every customer touched, so login was the failure everyone saw.

This is the story of the six months we spent rewriting the whole thing without letting any customer know, and the shape of the 2026 lesson I keep coming back to when founders ask about “AI rewrites.”

What Was Actually Broken

The product’s data model had been designed as a tree of entities: user, organization, permissions, roles, sites, policies, and so on. Every two adjacent nodes in that tree were connected with a JOIN in the ORM layer. The design was clean on a whiteboard. In production, at customer scale, it meant that every login had to traverse the whole tree to figure out who the user was and what they could see. Every JOIN was, technically, an inner JOIN on a growing table. The math got away from the team.

Two other things compounded the code problem. The web team was split across two offices: two frontend developers in Israel and five backend developers in Berlin. Beyond the timezone friction, there was a genuine cultural gap in how the two sides worked: how much design got settled before code, how bugs got triaged, how late in the sprint you were allowed to change scope. And on top of both, we were shipping to European enterprise customers, so GDPR compliance had to be real, not a settings screen.

Why We Could Not Just Fix It

The first instinct with a two-minute login is “add a cache, denormalize, indexes, done.” We looked at that. The problem was that the two-minute login was not a symptom of one bad query. It was the shape of the data model showing up at the surface. Every feature the team wanted to add next required more JOINs on the same tree. Any local fix bought a few weeks of headroom before the next feature undid it.

Once the leadership and the team both accepted that the data model, the framework, and the frontend were the actual failure surfaces, an incremental fix stopped making sense. We committed to rewriting the whole thing behind the existing UI, and to shipping the rewrite without asking customers to migrate, retrain, or take downtime.

What We Chose to Build

The new stack was deliberately boring:

  • Django on the backend, replacing Pyramid. Not because Django was fashionable, but because the team was going to write ten times more code in the next year than in the previous year, and Django’s opinionated stack meant fewer places to make an architectural call at 2am.
  • Marionette on the frontend, replacing vanilla JavaScript. It gave us a real component model so the screens could be rebuilt one at a time.
  • A redesigned data model where user, organization, and permission lookups were denormalized enough that login became a single indexed read. The tree was still there conceptually, but it stopped being a query.
  • An EU-private database with cross-region writes, so GDPR was structural, not procedural. European customer data lived in an EU database, always.
  • Active Directory sync as a first-class subsystem, not a background job. Enterprise customers lived in AD. Every user in the product came from AD. It had to work.

 

The Screen-by-Screen Cutover

The rewrite itself was a strangler pattern executed at the screen level. The customer-facing URLs stayed constant. Behind each URL, we built the new screen against the new backend, and used the load balancer to decide, per endpoint, whether to serve the old app or the new one.

Screens moved over one at a time. The dashboard first, then the reporting screens, then the settings screens, then the policy screens, then the user management screens, then finally the login flow itself. For weeks, individual customers were using an app whose top half was new and whose bottom half was old, and had no way to tell.

The reason it worked was that we treated the visible surface, meaning the shape of every screen, the exact URL of every endpoint, the exact fields on every form, as immutable. The rewrite could change anything it wanted underneath. It could not change what the customer saw. When that rule held, screen-by-screen replacement was mostly a mechanical exercise. When we broke that rule accidentally, which happened, we caught it in a screenshot comparison before it went live.

The AD Sync Grind

Inside those six months, the single hardest thing was Active Directory sync. Enterprise customers ran AD forests with hundreds of thousands of users and nested groups. Every login, every permission check, and every policy assignment depended on that sync being correct and current. Every customer had to sync every thirty minutes. Sync itself took roughly fifty minutes on real customer data. A job that had to run twice as often as it could finish. That was the shape of the problem.

Fixing it was not one clever change. It was a sequence, and every step exposed the next bottleneck.

We started by computing deltas instead of recomputing the world on every cycle. That was the single biggest win and it was still not enough. We made the sync call atomic, so a mid-run failure never left a customer half-synced. We converted delta calculations from row-by-row writes into bulk creates. We reworked queries and rebuilt indexes to match the new write shape. We split the Celery queues so one slow tenant stopped blocking the rest. We tuned Celery retry and failure parameters so transient AD errors stopped amplifying into cascading load.

By the end, a single customer’s sync ran in about six seconds, comfortably inside the thirty-minute cadence with room to grow. Fifty minutes down to six seconds.

From the outside, this part of the rewrite did not look like progress. From the inside, it was the reason the rewrite could be invisible at all. If AD sync was flaky, every login was flaky, and every flaky login is a customer noticing.

The Invisible Switch

Toward the end of the six months, most screens were already being served by the new backend, and only a handful of endpoints were left on the old one. We pointed the load balancers at the new product one endpoint at a time, watched error rates for a day or two per switch, and moved on.

There was no cutover announcement, no downtime window, no customer email. The last endpoint moved on a Tuesday afternoon. Nobody outside the team noticed. Login went from two minutes to something you would not measure without a stopwatch. Customers noticed that. They did not know it was a new product.

What It Cost the Team

The Israel/Germany split did not survive the rewrite. Part of what made the six months possible was consolidating the web team in Israel and growing it to seven full-stack engineers. The Berlin backend team wound down over the same period. That was not free. It was the right call for what the product needed next, and it was a hard call to make well.

The rewrite itself did what a rewrite is supposed to do and rarely does: features that used to take a month to ship started shipping in a week. The team stopped being scared of the codebase. Onboarding new engineers went from a month of “why is it like this” to a week of “this is how it works.”

The 2026 Version of This Pattern

Twelve years on, the shape of this story is what I keep telling founders who are wrestling with “should we let AI rewrite our codebase.”

The answer they usually want is either “yes, ship it” or “no, too risky.” Neither is right. The useful answer is: your surface is your contract with the customer. Your substrate is yours to rewrite. If your codebase is structured so that each component can be replaced with a newer implementation (a newer framework, a newer model, a newer AI-generated version) without touching the interface it exposes, you get to keep swapping models forever and your customers never notice. If your codebase is structured so that every component knows too much about every other one, no rewrite is safe, whether a human or an AI writes it.

The pattern is not new. It is just that in 2026, the reason to care about it is less “we might rewrite in a different framework someday” and more “we will re-generate this component with a different model this quarter.” The design principle scales cleanly. The teams whose codebases already had clean seams are the teams shipping the most useful AI-driven changes right now. The teams whose codebases did not are stuck in the same spot they were in before AI, just with more confident-sounding pull requests.

The One-Line Version

Design every component so it can be swapped for a newer version, human or model, without touching what the user sees. Then the rewrite is boring. Boring rewrites ship.

Let’s Talk

If you are staring at a codebase where a rewrite is starting to sound necessary, or where you are trying to decide how to structure things so an AI-driven rewrite is safe, that is exactly the kind of decision I work through with technical founders. No pitch. Happy to look at the current shape and tell you what I would do. Reach out.