Elke codebase heeft die ene module waar iedereen omheen loopt. Niemand opent hem zonder reden, wijzigingen erin worden een sprint vooruit gepland, en degene die hem schreef is in 2019 vertrokken. Je weet precies welke ik bedoel.
Michael Feathers definieerde die module tweeëntwintig jaar geleden, en niemand heeft er sindsdien een betere definitie voor bedacht: legacy code is code zonder tests. Niet oude code, niet lelijke code, niet PHP. Code die je niet kunt wijzigen zonder te gokken of je iets sloopt.
Dit is precies de code waar een coding agent het beste tot zijn recht komt. En het is precies de code waar hij de meeste schade aanricht, om dezelfde reden.
Waarom je een agent juist op oude code zet
Google publiceerde de cijfers, en dat doen bedrijven veel te weinig. Bij hun migratie van int32 naar int64 was 80% van de codewijzigingen in de changelists die daadwerkelijk gemerged zijn door AI geschreven, en de totale doorlooptijd van de migratie werd ongeveer gehalveerd. Het vervolgonderzoek over 39 migraties in twaalf maanden komt uit op 74,45% van de wijzigingen en 69,46% van de losse edits.
Dat zijn productiemigraties in een levende monorepo, geteld op wijzigingen die het echt gehaald hebben. En de aard van het werk verklaart waarom het goed ging: mechanisch, repetitief, volledig dichtgetimmerd, verspreid over duizenden bestanden waar niemand in wilde duiken. Daar zit de winst.
Er speelt ook iets menselijks mee, en dat weegt zwaarder dan mensen toegeven. Legacy-werk is impopulair omdat het saai en ondankbaar is, dus het wordt uitgesteld tot het een voorstel voor een herbouw wordt. De agent verveelt zich niet. Die leest om drie uur 's middags vierduizend regels betaalmodule uit 2014 met exact evenveel animo als om negen uur 's ochtends.
Waar het misgaat: de agent refactort de betekenis
Dit is de bevinding die moet bepalen hoe je hem inzet. Onderzoekers haalden echte refactorings uit open-source Java-projecten door frontier-modellen en controleerden de uitkomst. 7,4% van de refactorings die GPT-4 voorstelde bevatte een bug, bij Gemini 6,6%. Van de 22 onveilige resultaten waren er 18 semantisch: de code compileerde nog en het gedrag was stilletjes veranderd.
Eén voorbeeld uit het onderzoek: een inline-variable-refactoring vouwde een veilige conditional samen tot een ternary en introduceerde daarmee een null pointer exception.
De verklaring van de onderzoekers is het hele verhaal in één zin: generieke LLM's herkennen het patroon van hoe gerefactorde code eruitziet, zonder te controleren of de code daarna nog hetzelfde doet. Eruitzien als een refactoring en er ook echt een zijn: dat zijn twee verschillende dingen, en maar één daarvan valt te controleren.
Een wijziging die gedrag verandert is een herschrijving met een geruststellende commit message.
Het agentonderzoek uit november 2025 vult de andere helft aan: agents refactoren meer dan mensen, in 24,9% van de agent-PR's tegenover 14,9% van de menselijke, maar ze zitten vooral op renames en typewijzigingen. Veel beweging, meestal cosmetisch, af en toe midden in iets wat er wel degelijk toe doet.
De snelheid van de agent is dus echt, en zijn vermogen om te beoordelen of de code daarna nog hetzelfde doet ontbreekt volledig. Voor die combinatie bestaat precies één veilige oplossing, en Feathers had die al opgeschreven voordat dit alles bestond.
Characterization tests zijn de hele truc
Een characterization test legt vast wat de code nu doet, bugs en al. Wat hij zou moeten doen is een andere vraag, en die komt later. Geeft de functie -0.00 terug bij een restitutie van nul, dan leg je -0.00 vast. Gooit hij een exception bij een lege array in plaats van er een terug te geven, dan leg je die exception vast.
De eerste keer voelt dat verkeerd. Je schrijft een test die een bug tot verwacht gedrag verheft. Zie het als een foto van de bestaande situatie, gemaakt zodat je straks kunt aantonen dat de bug de refactoring ongewijzigd heeft doorstaan. Je kunt onmogelijk vaststellen of een refactoring het gedrag intact liet als je nooit hebt opgeschreven wat dat gedrag was.
En dit is nu net het werk waar agents opvallend goed in zijn, want er komt geen enkel ontwerpoordeel aan te pas. Geen "zou moeten", geen architectuurkeuze, geen discussie over naamgeving. Code erin, observeerbaar gedrag uitschrijven, cases genereren tot de branches gedekt zijn. Saai, uitputtend, controleerbaar: alles waar het model goed in is en niets waar het slecht in is.
Approval testing maakt het nog goedkoper. In plaats van assertions met de hand te schrijven leg je de output vast en keur je die één keer goed als golden master. ApprovalTests, Verify voor .NET, syrupy voor pytest, snapshots in Jest. De agent genereert de invoer, de tool legt de uitvoer vast, jij beoordeelt de opname. Een legacy-functie van duizend regels gaat zo in een middag van ontestbaar naar vastgelegd.
De cyclus die ik in de praktijk draai
1. Kies één wijzigingspunt. Neem de specifieke functie die je volgende week toch moet aanpassen, niet de hele module. Legacy-werk betaalt zich alleen terug op de plek waar je binnenkort echt moet zijn, en een heel bestand als scope nemen is precies hoe een refactoring in een herbouw verandert.
2. Laat de agent de seams inventariseren. Een seam is bij Feathers een plek waar je gedrag kunt aanpassen zonder de code op die plek zelf aan te passen. Injectiepunten in constructors, interfaces, modulegrenzen. Vraag om de inventarisatie: wat deze functie aanraakt, wat hem aanroept, waar de verborgen I/O zit, waar de globale state leeft. Dat is urenlang doorspitten in code die je niet kent, teruggebracht tot minuten, en het hoeft alleen te kloppen op de punten die jij kunt controleren.
3. Genereer de characterization tests. Met de expliciete opdracht om vast te leggen, niet om te corrigeren. De prompt is hier bepalend: vraag je een agent om "tests te schrijven" voor legacy code, dan schrijft hij tests voor hoe hij denkt dat de code zich hoort te gedragen, en meldt hij vervolgens plichtsgetrouw fouten die in werkelijkheid gewoon het gedrag zijn dat de code altijd al had. Vertel hem dat de code per definitie klopt en dat het zijn taak is om hem te beschrijven.
4. Lees die tests zelf. Deze stap kun je niet uitbesteden, en het is dezelfde regel als nooit code shippen die je niet snapt: elke vastgelegde assertion is een bewering over het gedrag van jouw systeem waar je zo meteen een refactoring op gaat baseren. Loop ze stuk voor stuk na en vraag je af of ze de werkelijkheid vastleggen of de aanname van de agent over die werkelijkheid.
5. Controleer of je tests echt iets tegenhouden. Groene tests bewijzen op zichzelf niets. Sloop de code expres, draai een vergelijking om, verander een afrondingsmodus, en bevestig dat de suite rood wordt. Mutation testing maakt van coverage bewijs, en bij gegenereerde tests die legacy code moeten beschermen is dat geen luxe. Een suite die groen blijft wat je ook sloopt is erger dan geen suite, want hij voelt als een vangnet.
6. Nu pas refactoren, seam voor seam. Kleine commits, de hele suite groen na elke stap. Wordt er iets rood, dan weet je meteen wat, in plaats van dat je om middernacht een diff van duizend regels zit te bisecten.
Stap 1 tot en met 5 zijn het meeste werk. Dat is precies het punt. De refactoring was nooit het lastige deel.
Wat ik niet uit handen geef
Bepalen waar de code voor bedoeld was. De tests leggen vast wat hij doet en zeggen niets over waarom. Die betaalmodule uit 2014 rondt in drie branches op drie manieren af, en ergens in het bedrijf is één daarvan een wettelijke eis en zijn de andere twee ongelukjes. Hoe lang je de code ook leest, dat staat er niet in.
Alles met verborgen I/O. Een functie die een bestand wegschrijft, een webhook verstuurt of een betaalprovider aanroept kun je niet veilig vastleggen door hem uit te voeren, en een agent komt daar pas achter als het al geld heeft gekost.
De beslissing om te stoppen. Agents blijven eindeloos verbeterpunten vinden. Legacy-werk moet begrensd blijven tot de wijziging waarvoor je kwam, anders vreet het je sprint op.
Dit alles heeft een prettig neveneffect: de code wordt ook makkelijker voor de agent zelf. Vastgelegde, opgeknipte code met duidelijke seams is code waarin de volgende sessie kan werken zonder te gokken. Het is de tol van technische schuld, maar dan de andere kant op.
Tests maakten legacy code al in 2004 veilig wijzigbaar. Wat er veranderd is: het schrijven ervan kost geen week meer, en daarmee verdween ook het excuus dat iedereen sindsdien gebruikte.
Die module waar je omheen loopt: de tests ervoor zijn nu een middag werk. Eromheen blijven lopen is een keuze.