Een driekoppig e-commerceplatform herleiden tot één multi-store systeem
Details
sector
e-commerce / retail
rol
Backend developer (freelance)
periode
2022-2024
team
6-8 developers
stack
PHP · Symfony · MySQL · RabbitMQ · Redis · Elasticsearch
PROBLEEM
Het e-commerceplatform van de retailer was oorspronkelijk gebouwd voor de thuismarkt, maar groeide door de jaren heen, feature na feature, uit tot iets dat nooit bedoeld was: een driekoppig monster.
Het oorspronkelijke platform bevatte de product- en prijsdata, maar bediende enkel de thuismarkt. Een tweede platform bediende meerdere buitenlandse markten en bevatte de nodige belastinglogica, valutanotatie en andere lokalisaties, maar was voor de brondata afhankelijk van het oorspronkelijke platform. Nog een derde platform bediende slechts één buitenlandse markt en stond volledig los van de andere twee.
Een bijkomende moeilijkheid was dat het platform ooit vertrokken was van een CMS, maar door de jaren heen zo grondig aangepast werd dat er nagenoeg niets van overbleef. Bijna elke component en feature was ofwel gestript, ofwel geëvolueerd naar een eigen versie, tot en met een volledig zelfgebouwde database-abstractielaag. Er was al een eerste aanzet gegeven om de tech debt terug te dringen door Symfony gedeeltelijk te introduceren, maar dat was blijven steken.
Op dat moment kwam ik in het team terecht, met een duidelijke opdracht: de drie platformen samenbrengen in het oorspronkelijke platform en daarbij zoveel mogelijk tech debt wegwerken.
RANDVOORWAARDEN
- Een rewrite was uitgesloten: het platform was te groot en te sterk gecustomized om te vervangen, en het interne team had ervoor gekozen om in het oorspronkelijke platform te investeren. Elke verbetering moest landen binnen de codebase zoals die was.
- Alle drie de platformen moesten blijven verkopen: de normale handel moest blijven doorlopen tijdens de consolidatie. Elk platform bleef zijn eigen markten bedienen, dus niets mocht uitvallen en geen enkele stap mocht afhangen van een big-bang cutover. De prestaties van alle winkels werden nauw opgevolgd door het data science team, dus elke negatieve impact zou meteen opgevallen zijn.
- Eindejaarsperiode: Black Friday, Sinterklaas, Kerstmis en Nieuwjaar vallen allemaal binnen ongeveer één maand, veruit de drukste en belangrijkste periode van het jaar. Daarom geldt er een bedrijfsbrede code freeze vanaf één week voor Black Friday tot de eerste week van januari. Over een project van twee jaar betekende dat twee freeze-periodes, samen ruwweg drie maanden waarin er niets kon worden uitgerold.
AANPAK
Workflow
Vanaf de start van het project spraken we af om releases zo klein mogelijk te houden en ze vaak uit te rollen. Zo kreeg elke wijziging al tijd om te draaien in productie en konden we problemen opsporen die door de ontwikkelcyclus geglipt waren, nog voor we de eerste winkel migreerden. Het betekende ook dat we nooit op een grote, half afgewerkte wijziging bleven zitten wanneer de freeze inging.
Fundament
Terwijl ik de eerste weken mijn draai vond, maakte ik de integratie van Symfony af en bracht die meteen naar de laatste LTS-versie. Zo leerde ik het project goed kennen en tijdens dat werk werden een paar belangrijke beslissingen genomen over de architectuur:
- Dependency injection: een overgebleven apart container-/dependency-injectiemechanisme bleef voorlopig behouden, tot we genoeg delen gerefactord hadden om het te vervangen door de container van Symfony.
- De databaselaag: de volledig zelfgebouwde databaselaag bleef eveneens behouden, omdat die zijn werk goed deed. Migreren naar Doctrine zou te veel tijd gekost hebben voor te weinig opbrengst, en zou bovendien betekend hebben dat we van het Active Record-patroon van de eigen laag naar het Data Mapper-patroon van Doctrine moesten overstappen.
Routing
De legacy codebase zelf was bij elke stap een uitdaging, door de hoge graad van customisatie die er door de jaren heen in geslopen was. Eén van die uitdagingen was de routing. Dat was een van de weinige componenten van het oorspronkelijke framework die nog grotendeels intact was.
Omdat de volledige codebase en content bovenop dit systeem gebouwd waren, wilden we het niet volledig omgooien en kozen
we ervoor om de ChainRouter van symfony-cmf/routing-bundle in te zetten:
cmf_routing:
chain:
routers_by_id:
cmf_routing.dynamic_router: 300
router.default: 200
App\Routing\LegacyDatabaseRouter: 100
dynamic:
enabled: true
route_provider_service_id: 'App\Routing\TranslatableRouteProvider'
persistence:
orm:
enabled: true
route_class: App\Entity\TranslatableRoute
De ChainRouter werd zo opgezet dat de nieuwe vertaalbare routes altijd eerst aan bod kwamen, zodat we elke bestaande URL konden overschrijven. Daarna kreeg de Symfony-router een kans om een route te matchen, en pas dan handelde de oorspronkelijke routing-engine (verpakt in een facade om netjes met Symfony samen te werken) de legacy-URL's en -pagina's af, of gaf een 404 terug als er niets matchte.
Vertalingen
Door de omvang van het project bleek ook het opsporen van alle plaatsen waar vertalingen nodig waren een uitdaging, zij het een kleinere. De verschillende templates en al hun varianten waren omslachtig om te vinden, maar nog moeilijker te achterhalen waren alle plaatsen waar afbeeldingen getoond werden, aangezien die meestal per vertaling een andere versie moesten kunnen tonen.
Een combinatie van scripts en dumps, fallback-logging en handmatig zoekwerk, ondersteund door meldingen van gebruikers die op acceptatie testten, was nodig om alle vertalingen boven water te krijgen.
Productzoekfunctie
Een uitdaging die niets met de codebase te maken had, was het management een beslissing laten nemen over de implementatie van de vernieuwde productzoekfunctie. De keuze was of we onze eigen Elasticsearch-infrastructuur zouden opzetten of een gehoste Elasticsearch-provider zouden gebruiken. Die keuze sleepte zes maanden aan voordat er uiteindelijk voor eigen infrastructuur gekozen werd.
Dat vertraagde de productzoekfunctie en de migratie van de eerste winkel aanzienlijk, want de infrastructuur hiervoor moest eerst nog opgezet worden.
RESULTAAT
Over een periode van ongeveer twee jaar hebben we een groot deel van de bestaande codebase herwerkt, met een nieuwe routing-infrastructuur die de multi-store- en meertaligheidsfunctionaliteit toeliet die nodig was om de winkels van de zusterplatformen naar het oorspronkelijke platform te migreren.
Ondanks de vertraging door de aanslepende beslissing werd ook een nieuwe productzoekfunctie opgeleverd voordat de eerste winkel migreerde.
Kort nadat ik naar een ander project overstapte, werd de eerste winkel met succes gemigreerd naar het opgewaardeerde oorspronkelijke platform.
Vergelijkbare uitdaging?