We’re replacing the frontend of a running store. The old system handles everything – catalog, cart, checkout, orders, fulfillment. The new one is Node.js. We can’t switch over in a weekend. The store has traffic every day, orders every hour. Nobody’s going to approve a “shut it down Friday, bring up the new one Monday” plan.

So both systems run at the same time. On the same domain, behind the same nginx. Writing this down before I forget the order we did it in.

The sequence

It happened in layers, roughly in this order:

URLs first. We loaded the entire URL space into Redis and wrote a middleware that translates the public URL to an internal route. From that point, Node.js owns what the customer sees. The old system is still there, still connected to the same database, but requests go through Node.js first. I wrote about this in the URL mapping post.

Read paths second. Catalog browsing, product pages, category listings, search results. These are read-only. Node.js queries the same MySQL database the old system writes to. No data duplication, no sync problem. The customer sees the same products regardless of which system served the page.

Sessions third. Both systems need to know who’s logged in. We moved sessions into Redis. The old system and the new one both read and write to the same Redis session store. A customer who logs in on a page served by Node.js stays logged in on a page that falls through to the old system. This was the scariest part – if the session format doesn’t match exactly, users get logged out randomly. We tested this for a week before going live.

Writes last. Cart, checkout, order placement. This is where it gets complicated. The old system has its own order IDs, its own state machine, its own hooks into the rest of the operation. The new system can’t just write orders to the database and hope the old system picks them up. The formats are different, the ID spaces are different, the state transitions don’t map cleanly.

We ended up with a mapping layer. When the new system creates an order, it also pushes a copy to the old system through its API. The old system gets an order in its own format, with its own ID. A mapping table links the two IDs. Fulfillment, invoicing, shipping – all that still runs on the old system. The new system tracks its own order and keeps it in sync through the mapping.

It’s ugly. Two representations of the same order, linked by a table that exists only because we’re running two systems. But it works, and it means we can move the write path over without touching fulfillment. That’s the whole point – carve over the parts you’re ready to own, leave the rest running.

The mapping table is the cost of running two systems. Every entity that both systems touch needs a row that says “this ID in system A is that ID in system B.” It’s not elegant. But it’s cheaper than shutting everything down for a weekend.

We’re still running both systems. The old one handles less every month. Every layer we carve over is a week of watching logs and checking numbers. You start breathing again after a few days without incidents.