Naar hoofdinhoud
Terug naar Blog

PHP-monoliet moderniseren of opsplitsen? Zo neem je die beslissing

TLDR;

De meeste PHP-monolieten hoeven niet opgesplitst te worden; ze moeten gemoderniseerd worden. Begin met afhankelijkheden in kaart brengen, een vangnet van tests en CI, en PHP- en framework-upgrades. Trek daarna modulegrenzen in de monoliet en splits pas één domein af als daar een meetbare schaal-, team- of releasereden voor is.

Waarom "van monoliet naar microservices" meestal de verkeerde vraag is

Bij moderniseringstrajecten komt vaak een variant van dezelfde vraag voorbij: "Onze PHP-applicatie is een monoliet geworden, moeten we naar microservices?" De vraag klinkt technisch, maar het echte probleem zit er bijna altijd achter. Releases duren te lang. Niemand durft de betaalmodule aan te raken. Een nieuwe developer heeft maanden nodig voordat die iets kan opleveren. De PHP-versie is jaren oud en de upgrade wordt steeds uitgesteld.

Geen van die problemen wordt opgelost door de applicatie in twintig services te knippen. Ze worden er meestal erger van, omdat je bovenop een codebase die je al niet goed kent ook nog netwerkverkeer, meerdere deploys en verdeelde data krijgt.

Oprichter Julian Gardner heeft 25+ jaar ervaring met software en PHP. In die praktijk bleek een migratie naar microservices zelden de juiste eerste stap. Wat wél werkt: eerst begrijpen wat het platform doet, de basis op orde brengen en daarna per domein beslissen of opsplitsen iets oplevert.

De betere vraag is dus niet "monoliet of microservices", maar: wat houdt ons tegen om veilig en regelmatig te releasen, en wat is de kleinste ingreep die dat verandert?

Wanneer een monoliet gewoon moet blijven

Een monoliet is geen fout. Voor de meeste Nederlandse mkb-platformen en veel scale-ups is het de juiste vorm, ook op de lange termijn. Dit zijn de criteria die wij gebruiken:

Teamgrootte. Met één tot pakweg acht developers heb je weinig aan de organisatorische voordelen van microservices. Die architectuur is bedoeld om tientallen teams onafhankelijk van elkaar te laten werken. Met één team koop je de complexiteit zonder het voordeel.

Deploy-frequentie. Als je nu één keer per maand released en dat vooral spannend is vanwege ontbrekende tests en handmatige stappen, dan lost een andere architectuur dat niet op. Je hebt dan een CI-pipeline en een fatsoenlijke deploy-procedure nodig, geen tweede codebase.

Domeingrenzen. Kun je vandaag aanwijzen welke code bij "facturatie" hoort en welke bij "gebruikersbeheer"? Als het antwoord "nee, alles hangt aan alles" is, ga je die grenzen niet ontdekken door services te bouwen. Je ontdekt ze door de monoliet te modulariseren.

Operationele volwassenheid. Heb je centrale logging, monitoring, alerts en een manier om een deploy terug te draaien? Zonder dat is één applicatie al lastig genoeg om in de lucht te houden. Met tien services wordt elke storing een zoektocht.

Scoor je op drie of vier van deze punten "nog niet", dan is het antwoord vrij simpel: de monoliet blijft, en het werk zit in het moderniseren van de monoliet, niet in het opsplitsen ervan.

Wanneer opsplitsen wél zinvol is

Er zijn situaties waarin een deel van een PHP-monoliet opsplitsen echt iets oplost. Je herkent ze aan een concrete, meetbare reden, niet aan het gevoel dat de codebase "te groot" is.

  • Eén onderdeel heeft een totaal ander schaalprofiel. Een rapportage-engine die elke nacht miljoenen records doorloopt, of een import die het hele platform traag maakt. Dat onderdeel isoleren en apart schalen is legitiem.
  • Meerdere teams zitten elkaar aantoonbaar in de weg. Merge-conflicten, wachten op elkaars releases, een team dat de deploy van een ander team breekt. Dat is een organisatorisch probleem met een architecturale oplossing.
  • Een domein heeft een andere releasecadans nodig. Een koppeling met een externe partij die wekelijks wijzigt, terwijl de rest van het platform stabiel is.
  • Een onderdeel moet in een andere technologie. Bijvoorbeeld een realtimecomponent die beter niet in PHP-FPM draait.

Ook dan geldt: je splitst één bounded context per keer af, met een duidelijke eigenaar, een eigen datamodel en een expliciet contract met de rest. Wat je niet doet, is het hele platform in één project ombouwen. Elke afsplitsing moet op zichzelf waarde opleveren, ook als de volgende nooit komt.

Incrementeel moderniseren zonder risicovolle rewrite

Het alternatief voor "alles opnieuw" is een reeks kleine, omkeerbare stappen. Zo pakken wij het meestal aan wanneer we een bestaand PHP-platform stap voor stap moderniseren.

De modulaire monoliet als tussenstation

Een modulaire monoliet is één applicatie die als geheel wordt uitgerold en waarin domeinen in aparte modules leven met duidelijke grenzen: eigen namespaces, eigen services en communicatie via expliciete interfaces of events in plaats van directe database-joins over domeinen heen. In Laravel of Symfony kun je dit afdwingen met architectuurtests (bijvoorbeeld PestPHP-architectuurtests of Deptrac), zodat een module niet ongemerkt in de tabellen van een andere module gaat lezen.

Het voordeel: je krijgt de meeste winst van duidelijke grenzen zonder netwerkverkeer, zonder gedistribueerde transacties en met één deploy. En als later blijkt dat één module echt apart moet, is die al bijna een service.

PHP- en framework-upgrades eerst

Voordat je iets aan de architectuur doet, wil je op een ondersteunde PHP- en frameworkversie zitten. Elke stap daarna wordt makkelijker: modernere syntax, betere tooling, statische analyse die daadwerkelijk iets oplevert. Een stapsgewijze upgrade van PHP 7.4 naar een ondersteunde PHP 8-versie lijkt saai werk, maar heeft vaak een gunstige verhouding tussen risico en opbrengst. Tools als Rector nemen een groot deel van de mechanische aanpassingen uit handen.

Tests en CI als vangnet

Zonder tests is elke wijziging een gok. Je hoeft niet naar 90% coverage. Wat je nodig hebt, zijn tests rond de paden die geld verdienen of schade kunnen aanrichten: checkout, facturatie, autorisatie, imports. Karakteriseringstests, tests die vastleggen wat het systeem nú doet, ook als dat gedrag vreemd is, zijn hier goud waard. Daarna een pipeline die bij elke merge draait, met statische analyse (PHPStan) op een niveau dat je stapsgewijs kunt aanscherpen.

Het strangler-patroon voor één domein

Kies één domein dat pijn doet én afgebakend is. Zet er een nieuwe implementatie naast, achter dezelfde interface of route. Leid verkeer geleidelijk om, bijvoorbeeld via een feature flag of per klantgroep. Zodra de nieuwe implementatie al het verkeer aankan en de oude code niet meer wordt aangeroepen, verwijder je die. Dit werkt binnen een monoliet (oude module naar nieuwe module) net zo goed als richting een aparte service.

Database-afhankelijkheden en dubbele writes

Hier gaat het meestal mis. De code is misschien netjes gesplitst, maar de database is nog steeds één grote gedeelde toestand. Voordat je een domein isoleert, wil je weten welke tabellen het leest en schrijft, en wie er verder nog aan zit. Foreign keys over domeingrenzen heen, een gedeelde users-tabel en rapportages die overal in prikken zijn de gebruikelijke verrassingen.

Als een domein tijdelijk in twee systemen leeft, krijg je dubbele writes: elke wijziging moet naar beide. Doe dat expliciet, bij voorkeur via events of een outbox-tabel, en controleer actief of beide kanten gelijk blijven. Doe het niet stilzwijgend "even in de controller", want dat is de bron van de data-inconsistenties die je maanden later terugvindt.

Deployment en rollback

Elke stap hierboven moet terug te draaien zijn. Dat betekent: geautomatiseerde deploys, geen handmatige databasemigraties, en migraties die achterwaarts compatibel zijn (eerst kolom toevoegen, dan code aanpassen, pas daarna de oude kolom weg). Een container-image dat in ontwikkeling, test en productie hetzelfde is, haalt een hele categorie "werkt op mijn machine"-problemen weg.

Observability

Zorg dat je vóór een ingreep weet hoe het platform zich gedraagt: responstijden, foutpercentages, queue-lengtes, trage queries. Niet omdat het mooi staat, maar omdat je anders na de ingreep niet kunt zeggen of het beter of slechter werd. Voor alles wat over meer dan één proces loopt, is een correlation-id per request het minimum.

Een praktische volgorde

In de praktijk komt het meestal op deze volgorde neer. Sla je een stap over, dan betaal je die bij de volgende terug.

  1. Breng afhankelijkheden in kaart. Welke PHP-versie, welke frameworkversie, welke packages zijn end-of-life, welke externe koppelingen zijn er en welke tabellen worden door meerdere delen van de code gelezen of gewijzigd. De uitkomst bepaalt alles wat volgt.
  2. Zet een vangnet neer. Een Docker-omgeving die op productie lijkt, een CI-pipeline, karakteriseringstests rond de kritieke paden en statische analyse op een haalbaar niveau.
  3. Upgrade PHP en het framework. Klein, met tests, naar productie. Niet "als we toch bezig zijn, ook nog even de architectuur".
  4. Trek modulegrenzen in de monoliet. Per domein, met architectuurtests die de grenzen bewaken. Verplaats database-writes achter services, zodat er één plek is waar een domein zijn eigen data muteert.
  5. Strangle het domein dat het meeste pijn doet. Eén tegelijk. Meten, omleiden, oude code verwijderen.
  6. Beslis pas nu over afsplitsen. Met een modulaire monoliet, tests en observability op orde is de vraag "moet dit een aparte service worden?" ineens beantwoordbaar. Vaak is het antwoord nee.

Een voorbeeld uit onze eigen praktijk: bij een Nederlands e-learningplatform hebben we precies deze route gevolgd. Een service-gebaseerde structuur stap voor stap ingevoerd, database-writes achter services gebracht, Docker en geautomatiseerde tests toegevoegd, terwijl het platform de hele tijd live bleef voor honderdduizenden studenten. Het team groeide in die periode van één naar zes developers, en dat kon juist omdat de codebase meerdere mensen tegelijk aankon.

Operationele risico's en kosten die je vooraf wilt kennen

Een migratie naar microservices heeft een prijs die zelden in het eerste plaatje staat.

Extra infrastructuur. Elke service heeft een eigen deploy, eigen monitoring, eigen secrets en een eigen database of schema. Dat is werk dat iemand elke maand moet doen, ook als er niets wijzigt.

Distributed failures. Een functie-aanroep faalt zelden; een netwerkcall faalt regelmatig. Timeouts, retries, circuit breakers en idempotente endpoints zijn ineens jouw probleem. Een monoliet heeft die categorie fouten simpelweg niet.

Data-consistentie. Zodra data over services verdeeld is, verdwijnt de databasetransactie. Je krijgt eventual consistency, compenserende acties en rapportages die tijdelijk niet kloppen. Dat is te managen, maar het moet ontworpen worden, niet ontdekt.

Kennisconcentratie. Vaak zit de kennis van de monoliet bij één of twee mensen. Een verbouwing zonder die kennis vast te leggen maakt het platform afhankelijker van hen, niet minder. Dit is ook waarom we bij senior PHP-ontwikkeling voor bestaande systemen zoveel nadruk leggen op zichtbaar werk: pull requests, reviews en documentatie die het eigen team kan overnemen.

Beslismatrix

SituatieMonoliet behoudenEerste stapEén domein afsplitsen
Team van maximaal acht developers, één productjaalleen concrete knelpunten aanpakkenzelden
Releases zijn spannend door ontbrekende tests of CIjatests en CI toevoegennee
Verouderde PHP- of frameworkversiejaPHP en framework upgradennee
Eén onderdeel heeft een ander schaalprofielmeestalisoleren als module en metenals een module niet volstaat
Meerdere teams blokkeren elkaar aantoonbaarmogelijkmodules met een eigen eigenaarmogelijk, per bounded context
Onderdeel vereist een andere technologiegedeeltelijkcontract en datagrens bepalenja, voor dat onderdeel
Geen monitoring, logging of rollbackjaobservability en rollback regelennee

Lees de matrix van links naar rechts: de rechterkolom komt pas in beeld als de eerste stap een meetbare reden voor afsplitsing oplevert.

De volgende stap

Twijfel je of jouw PHP-monoliet moet blijven, gemoderniseerd moet worden of gedeeltelijk opgesplitst? Begin met stap 1 uit de volgorde hierboven: de afhankelijkheden in kaart brengen. Wil je daarover sparren met iemand die dit vaker heeft gedaan, neem dan contact op. We denken graag mee over wat de kleinste zinvolle ingreep is.

PHP

Architecture

Legacy Systems

Platform Modernisation