Back

Re-platforming a 50-module Laravel monolith with zero downtime | Veli Kaan Çetinel

· 5 min read

Laravel PostgreSQL Migration Architecture

For over a decade, ARES has been the system automotive damage operations run on. Workshops, experts, insurers and integrations all depend on it — every day, all day. Which is exactly what makes replacing it hard: there is no maintenance window. You can’t tell an entire industry to pause while you migrate.

This is the story of how we re-platformed a 15,600-commit CodeIgniter-era monolith into a 50-module Laravel modular monolith on a completely new normalized PostgreSQL schema, without the legacy system ever going down.

The problem with the old schema

A schema that grows organically for ten years accumulates debt in ways code doesn’t. Columns reused for different meanings. Duplicate sources of truth. Table families that made sense for the business of 2015 but fight the business of today. At some point, every new feature costs more because of the foundation it stands on.

The obvious answer — “rewrite it” — is also the classic trap. Big-bang rewrites of live enterprise systems fail so often it’s a cliché. So we didn’t do a big bang. We did a strangler migration whose dual-write changed direction mid-flight.

Act one: observer-driven dual-write

The core idea: the legacy system keeps running as-is, and every write to a legacy table is mirrored — through model observers — into the new normalized schema. The new schema stays continuously up to date without the legacy code ever knowing it exists.

This gave us three things:

Identity resolution: the assumption that collapsed

The hardest part of running two schemas at once isn’t writing to both — it’s agreeing on what things are. Everyone assumed the new file id and the legacy reference number were “basically the same number”. Then we sampled 55 production files: 54 of them differed, and the two id bands overlapped — a value could be a valid id in both spaces, pointing at different records. Silent data corruption waiting to happen.

The fix was an eight-phase wire contract program: every external API id space — vehicle taxonomy, provinces, policy types, branches, document types, campaigns, invoices — moved from legacy references to v2 ids, with dual-read mappers rolled out simultaneously across three clients (the React SPA, the Flutter tablet and the mobile app), so old and new payloads could coexist during each phase.

Strangling table families — a concrete one

With dual-write keeping the schemas in sync, we retired legacy table families one by one: rewrite every read path, rewrite every write path, keep the downstream feeds alive, then drop the tables.

The labor tables were the hardest, and took five phases: first the mini-repair flow, then the damage flows, then severing the pricing dependency, then an atomic migration of the entire labor-id space, and finally a reconcile command before the DROP. The post-DROP review still surfaced nine live error paths that only production traffic could find — each one hunted down the same week. Retire families, not tables; entities travel in groups.

Act two: reversing the mirror

This is the part most migration stories skip. Mirroring legacy writes into v2 keeps v2 fresh — but v2 is still the copy. The real milestone is the flip: read paths repointed to v2 in six waves, roughly 40 endpoints rewritten v2-native out of ~70 audited, and then the mirror itself reversed direction — v2 became the authoritative system, syncing back into the legacy tables for the consumers still reading them.

In August 2026 we removed the last legacy→v2 mirror leg and deleted the feature flags. The decade-old system is still queryable — but today it’s the replica, not the source.

The supporting cast

A migration like this leans on infrastructure choices that don’t show up in demos:

Across the program: 347 migration files, 236 test files, 533 merged MRs, 18 production releases.

What I’d tell you if you’re facing the same thing

This is a condensed version of the story — if you want to talk migrations, reach out.