The owner of a furniture store comes with a problem many will recognise: the store works, it sells, it has good Google rankings — and it stands on code nobody dares to touch. Twelve years of custom development on PHP 5.4, one programmer who still remembers why certain things work the way they do, and an ever-growing list of features that are "better left alone". This is not a story about outdated technology. It is a story about business risk frozen in code.
Below we take apart the Bogart Meble migration — a store that moved from legacy PHP to Symfony 6.4 without a single day of downtime and without losing rankings. We will show the starting point (technical debt), the reason we rejected an all-at-once rewrite, the choice of stack, a sixteen-week schedule in four phases, and what actually changed. No marketing — with the mechanics you can translate to your own store.
The starting point: twelve years of technical debt
We started with an audit of what was there. The picture was typical of a store that had grown faster than its technical foundation for years. Five problems, each of which is survivable on its own, but which together create a state of heightened risk.
PHP 5.4 past end of support. No security patches means an open door and a growing compatibility problem — payment gateways and integrations require newer versions, and every month deepens the debt.
A custom framework with no documentation. In practice one person understood the whole thing. That is the classic bus factor of one: illness, a holiday or the departure of a single person leaves the store without real support.
No tests. Every change was a roulette, because the cost of a regression was unknown. The effect is always the same — the team stops changing anything so as not to break anything, and development dies.
A monolithic database with queries taking several seconds. This is not an abstract engineering problem. Slow queries hit Core Web Vitals directly, and through them conversion and rankings.
No way to integrate with a modern API. Connecting BaseLinker, a new payment gateway or a marketplace was practically impossible. The store could not develop in directions that had long been the market standard.
None of these points on its own topples a business. Together they create a situation in which development costs more and more while risk grows every month — until a small failure can become existential. That is usually the moment a store like this reaches us: not because something broke, but because the owner worked out how much a failure with nobody to fix it would cost.
Why we rejected a "big bang" rewrite
The most tempting route is usually the worst: write a new store from scratch alongside the old one and then, over a single weekend, switch traffic from old to new. It sounds clean. In practice, with twelve years of undocumented logic, it is asking for a disaster.
With a "big bang" approach all the value only appears on switchover day — months of work with no effect until flag day. If something goes wrong, a rollback is often impossible, because the old system has been living its own life in the meantime. And the most dangerous part is that you learn about the existence of some business rules only when a customer clicks a path nobody recreated — that is, already in production, in front of buyers.
We chose the Strangler Fig pattern. The name comes from the strangler fig, which wraps around a tree and gradually takes its place. In the same way the new system wraps around the old one: it takes over functions module by module until the old code can finally be removed. We recreated the business logic in Symfony piece by piece, while the old store kept working and selling the whole time. Every module got tests before it took over real traffic, so the risk was spread over time and reverting a change was local, at the level of a single module rather than the whole store.
Technically this is tied together by a routing layer in front of both systems. It decides which request is still handled by the old code and which has already been taken over by the new one — the least risky paths went there first, checkout and payments last. For the customer and for Google the address stays the same; underneath, only which system responds changes. As a result switching traffic is gradual and reversible: if a new module behaves differently from how it should, we go back to the old path without touching the rest of the store.
Criterion "Big bang" (all-at-once rewrite) Strangler Fig (module by module) Risk on switchover day Extreme — all or nothing Spread out — each module separately Rollback Often impossible Local, at module level Undocumented logic Comes to light after launch, at the customer Comes to light along the way, module by module Value before switchover Zero until "flag day" Builds up with every module Test on real traffic None — the first traffic is a baptism of fire Parallel run on production traffic When it makes sense Small, well-understood code A large, old, poorly documented monolith
With twelve years of undocumented code we did not ask whether a "big bang" would succeed. We asked what would happen if it did not — and nobody wanted to hear that answer.
The new stack — and why exactly this one
Choosing technology is not a matter of fashion but of consequences the client lives with for years. Every element of the stack had to solve a specific problem from the technical debt list rather than add another one.
Layer Technology What for (the mechanism) Backend Symfony 6.4 A mature framework with long support and a huge ecosystem — the end of a bus factor of one API API Platform REST and GraphQL out of the box — a ready foundation for integrations (BaseLinker, marketplaces) and a headless front end in the future Database MariaDB 10.11 A stable, well-indexed database in place of a monolith with queries taking several seconds Cache and sessions Redis The main hero of the drop in load time — it relieves the database on the most frequent paths Frontend Twig + Stimulus Server-side rendering, light and SEO-friendly, without the weight of a full SPA Search Elasticsearch Fast and accurate search across a large furniture catalogue that a relational database cannot carry Queues RabbitMQ Asynchronous tasks (emails, integrations, recalculations) outside the user request — the page does not wait Deployments Docker + GitLab CI Repeatable, tested deploys — the end of "it works on my machine" and manual file uploads
One thing is worth stating honestly: Symfony is the same foundation on which Sylius stands. Custom on Symfony makes sense where the business logic is thick and unusual — as at Bogart. When a store mostly fits within a platform's standards, we more often recommend ready-made Sylius over building from scratch, because it is faster and cheaper. The "custom versus ready platform" choice is a decision we take at the audit stage, not during the project.
Sixteen weeks in four phases
The whole project was completed in four phases over sixteen weeks — around four months. That was Bogart's schedule; with a larger catalogue or thicker logic such a scenario can take considerably longer. More important than the number of weeks is the order of the steps.
Phase 1 (weeks 1–3): audit and reverse engineering
The most important product of this phase was not code, but documentation. The team recreated the business logic hidden in the old code and mapped the URL structure and the database structure. There was also a document that in every one of our migrations we call "What we preserve unconditionally" — a list of several hundred of the most important URLs in terms of traffic and conversion. It sets the boundaries that must not be crossed in later phases.
Recreating undocumented logic is detective work, not guesswork. You read the code paths where decisions are made — how discounts, delivery costs and variant availability are calculated — and confront them with what actually happens in production, on real orders. Where only one person was left who remembered "why this works exactly like that", their knowledge went onto paper instead of continuing to exist only in their head. This phase pays off the largest part of the debt before the first line of new code is written — because it turns a bus factor of one into shared, written team knowledge.
Phase 2 (weeks 4–12): rewriting the modules
The heart of the project. Catalogue, cart, checkout, customer panel and integrations — recreated module by module, each with automated tests covering the critical purchase paths. The tests were not decoration: they were what allowed us to hand over module after module safely, because they gave an unambiguous answer to the question of whether the new version does exactly what the old one did.
Data migration: where quiet migrations die
Moving the catalogue, historical orders and customers is the moment when it is easiest to lose something irretrievably. We ran the data migration with integrity validation — after every pass the data was compared with the source so that no record disappeared or was quietly distorted. The result: no lost orders throughout the whole process. With orders that is not a "nice metric" but a boundary condition — one lost order means a real customer and a real dispute.
301s for thousands of URLs — the layer where migrations lose traffic
The most common cause of a fall in rankings after a migration is not technology but abandoned addresses. That is why we prepared a complete mapping of 301 redirects for all URLs — one to one, from the old structure to the new. The key detail: the map was validated by an SEO specialist, not only by a programmer, and it was imported into regression tests before go-live, so as to catch every broken link before Google saw it.
This approach is backed by numbers. The industry benchmark for the drop in organic traffic after a migration is usually 15–30%; in our projects we stay below 3%. At Bogart that translated into almost all rankings being preserved — more on that in the results section.
Migrations do not lose traffic on complicated code. They lose it on abandoned addresses — which is why with us the redirect map is validated by an SEO specialist, not only by a programmer.
Phases 3–4 (weeks 13–16): parallel run and switchover without downtime
For around two weeks the old and the new system ran in parallel. The parallel run made it possible to test the new store on real production traffic — including load tests — before anything depended on the new code. The point of this stage is simple: instead of believing that the new system will behave correctly, you check it on real traffic, in conditions no test environment can recreate. Differences in results between the old and the new system — the price in the cart, the delivery cost, the availability of a variant — then come to light while the old store is still running, rather than after a customer reports them. Only once the new system had carried real traffic and stopped differing from the old one where it should not, did we move on to the switchover.
The switchover itself we did outside peak hours, with a rollback plan ready. In Bogart's case the cutover was demanding enough that we spread it over three stages in time — each in a low-traffic window, and none of them required taking the store offline. The first hours after each stage meant intensified monitoring, and we observed SEO rankings and system behaviour long after the start. Customers were able to buy normally throughout.
Results — and how to read them
Four effects describe the outcome of the migration best. We give them as the result of the project, but treat them as an illustration of the mechanism rather than a guarantee — every store starts from a different place.
Product page load time: from several seconds to under one second. A several-fold speed-up, mainly thanks to Redis and rewriting the heaviest queries. This is the most direct effect — and the starting point for the others.
A clear rise in conversion within three months. Here honesty matters more than an impressive number: the rise was the product of both the new checkout and the speed itself. We do not attribute all of it to one variable — but the direction is consistent with what the whole industry's data shows.
Almost all Google rankings preserved. The effect of the 301 mapping and of keeping the same semantic structure. Against an industry benchmark of a 15–30% drop, that is the difference between a calm start and months of rebuilding traffic.
No lost orders and no downtime. The result of the parallel run, the staged switchover and data integrity validation.
Why does speed translate so strongly into sales? Because the cost of a slow page is measurable. In our observations a TTFB above 500 ms correlates with a 5–10% drop in conversion — which is why we aim below 200 ms. Going from several seconds to under one second is not cosmetics, but the removal of a real brake on sales. The team on the client's side had feared this migration for years; the most common reaction after the switchover came down to disbelief that it could be done without downtime and without a fall in Google.
There is one more result that is not visible in the numbers from launch day, and which changes the most in the longer term: the store can develop again. What used to be impossible — connecting BaseLinker, entering further marketplaces, adding a new payment gateway — becomes an ordinary task on a stack with API Platform, rather than a project carrying the risk of damaging the whole. So the migration was not an end in itself. It was the condition for all the subsequent improvements to become possible at all.
When such a migration pays off — and when it does not
Rewriting legacy PHP into Symfony is the longest, most expensive and most difficult migration scenario we run. Not every store needs it — and we will be the first to say so.
It makes sense when:
the business logic is thick and unusual — configurators, non-standard pricing, rules that no ready-made platform will reproduce;
organic traffic is the main channel, so the cost of losing rankings is counted in the hundreds of thousands;
the scale of the store means downtime or a fall in visibility is a real loss — and let us remember that a bad choice of vendor can cost a mid-market store PLN 500,000 and more in the first year.
It does not make sense when:
the store is 90% a standard catalogue and cart — then a ready platform (Shopify Plus or Sylius) is cheaper and faster than building from scratch;
there is no budget or time for a project lasting several months — there are then plenty of lighter migration paths;
nobody on the company's side understands the old rules and nobody wants to recreate them — without that knowledge, or a willingness to extract it, the risk grows regardless of the technology.
In other words: Strangler Fig and custom on Symfony are a tool for a specific problem, not a default answer. Our other projects — such as the Topeshop migration to Shopify Plus or KMK Klinkier to Sylius — show that for many stores the right answer is a ready platform, not rewriting code.
Summary
The Bogart Meble migration shows that twelve years of technical debt does not have to be repaid with one risky leap. Strangler Fig made it possible to recreate the logic module by module, the parallel run and the staged switchover eliminated downtime, and SEO-validated 301 mapping protected the rankings. The most important lesson, however, is strategic: a good migration starts with a decision about order — first what we protect unconditionally, and only then how we rewrite it.
If you are standing on code you are afraid to touch, start by scouting the terrain rather than by choosing a technology. See how we approach store migration, or write to us via our contact page — we will look at your case and say honestly whether rewriting makes sense or whether a ready platform is the better route.