Framework lock-in vermijden: het advies van Voxxed Days
Op Voxxed Days zag ik een talk over het vermijden van framework lock-in in Java, van Rijo Sam (abstract op GitHub). Het idee heet agnostic design: je code loskoppelen van het framework dat je gebruikt, zodat je later relatief eenvoudig kunt wisselen.
Rijo Sam bouwt dit op in drie stappen.
De eerste stap is het vervangen van framework-specifieke annotaties door algemene Java-annotaties, waar dat kan. Deze Spring-annotatie:
import org.springframework.beans.factory.annotation.Autowired;
@Component
public class MyService {
@Autowired
private MyRepository repository;
}
kan vervangen worden door de jakarta-variant:
import jakarta.inject.Inject;
@Component
public class MyService {
@Inject
private MyRepository repository;
}
Helaas hebben lang niet alle framework-specifieke annotaties een directe standaardvervanging.
De tweede stap gaat een stap verder: business logic en framework zoveel mogelijk in aparte modules onderbrengen. Eén module bevat de business logic, een andere linkt die aan het framework:
// module met business logic
public class ExceptionLogger {
public void logException(Exception e){
//
}
}
// module met spring boot implementatie
@Configuration
public class LoggingConfig {
@Bean
public ExceptionLogger exceptionLogger(){
return new ExceptionLogger();
}
}
Zo kun je dezelfde business logic tegelijk deploybaar maken met verschillende frameworks. Je zou bijvoorbeeld naast de Spring-module ook een Quarkus-module kunnen toevoegen, en in je bouwconfiguratie snel schakelen tussen frameworks zonder meerdere repositories te onderhouden.
De derde stap betreft cloud-integraties: veelgebruikte cloud providers leveren vaak libraries specifiek voor jouw framework. Om agnostisch te blijven gebruik je een algemene library en bouw je de koppeling met het framework zelf. Dat hoeft niet veel extra werk te zijn, het hangt af van welke libraries beschikbaar zijn.
Abstractie heeft een prijs
Tijdens de talk zelf had ik al mijn bedenkingen. Zo vroeg abstraheren van een codebase klinkt in theorie aantrekkelijk, maar in de praktijk betaal je daar direct voor.
Te veel abstractie maakt een codebase moeilijker te testen. Elke extra laag tussen je business logic en de plek waar die logic daadwerkelijk draait, is een laag die je moet mocken, opzetten of omzeilen in je tests. Het wordt ook minder duidelijk wat er op elk punt in de codebase gebeurt: als business logic en frameworkkoppeling los van elkaar in aparte modules staan, zie je niet meer in één oogopslag dat de logger bijvoorbeeld in de Spring-context terechtkomt. En het kost simpelweg tijd. Tijd die je besteedt aan het bedenken, implementeren en debuggen van abstractielagen, in plaats van aan de functionaliteit waar het uiteindelijk om gaat.
Die kosten wegen alleen op tegen iets als je ook daadwerkelijk profiteert van de flexibiliteit die agnostic design belooft. En dat is precies waar de vraag uit de zaal om draaide: “Hoe vaak moet je nou echt wisselen van framework?” Mijn eigen antwoord daarop was tot voor kort: bijna nooit. In de vijf jaar dat ik als developer bezig ben, had ik het nog niet één keer hoeven doen.
Voor het eerst in vijf jaar: JBoss naar OpenShift
Na vijf jaar zonder framework-wissel kwam die dus alsnog. De aanleiding lag niet bij de code, maar bij de infrastructuur. De applicatie draaide op een VM met een JBoss application server erop, en die deployment moest naar OpenShift.
Voor OpenShift is geen JBoss-image beschikbaar. Alle images die gebruikt mogen worden, moeten eerst gescreend worden door de organisatie, en JBoss wordt daarbij niet ondersteund omdat het steeds minder gebruikt wordt. Er was dus geen keuze tussen “JBoss-image aanvragen” of “overstappen”: de enige weg vooruit was een ander framework. De keuze viel op Spring Boot, vooral omdat het team daar al kennis van had.
Dit is precies het scenario waar agnostic design voor bedoeld is: een gedwongen framework-wissel. Alleen was de codebase daar niet op voorbereid. Die was, zoals de meeste bestaande codebases, niet agnostisch opgezet.
Dubbel onderhoud, dubbel risico
De daadwerkelijke code-conversie van JBoss naar Spring Boot was intensief: omdat de codebase niet agnostisch was opgezet, raakte het omzetten op meerdere plekken ook de business logic zelf, niet alleen de frameworkkoppeling. Toch bleek dit niet de bottleneck van het traject.
Het meeste tijd ging zitten in het klaarmaken van de OpenShift-omgeving zelf: kennis opbouwen over het platform, keuzes maken over hoe het netwerk opnieuw ingericht moest worden, en een aantal koppelingen met externe systemen die niet meer ondersteund werden en dus vervangen moesten worden. De basis voor de code-conversie stond binnen ongeveer een maand. De omgevingswerkzaamheden lopen, mede door onverwachte vertraging binnen het team, inmiddels al een half jaar.
Het gevolg: de migratie loopt nog steeds, en in de tussentijd bestaat de codebase in twee takken naast elkaar, één op JBoss, één op Spring Boot. Voor elk stuk functionaliteit dat erbij komt of wijzigt, moet nu bepaald worden of het relevant is voor beide takken. Omdat het klaarmaken van de OpenShift-omgeving zoveel tijd kost, lopen de takken intussen steeds verder uit elkaar en wordt het moeizamer om de implementatie in lijn te houden.
Wat ik de volgende keer anders zou doen
Achteraf gezien had een deel van deze pijn voorkomen kunnen worden door gerichter te werk te gaan zodra duidelijk werd dat er een langdurige migratie aankwam, in plaats van de hele codebase preventief te abstraheren.
Als ik dit traject opnieuw zou ingaan, zou ik eerst de codebase refactoren naar agnostic design, specifiek de tweede stap uit de talk: business logic en frameworkkoppeling in aparte modules onderbrengen. Daarna kun je in dezelfde repository twee deployments naast elkaar zetten, één op het oude framework, één op het nieuwe. Elke wijziging aan de business logic werkt dan meteen voor beide deployments. Alleen de dingen die daadwerkelijk frameworkafhankelijk zijn, blijven gescheiden, maar dan binnen één repository in plaats van in twee uit elkaar lopende takken.
Dat is een andere afweging dan preventief overal abstractielagen inbouwen. Het is gericht agnostisch worden op het moment dat je weet dat een migratie eraan zit, in plaats van vooraf gokken of je dat ooit nodig zult hebben.
Conclusie
De belangrijkste les uit deze migratie is dat de code-conversie zelden de bottleneck is, de omgeving eromheen wel. Maar zonder agnostisch opgezette code betaal je die trage omgevingsmigratie dubbel: eerst met een intensieve conversie die ook je business logic raakt, en daarna met een codebase die in twee uit elkaar lopende takken uiteenvalt.
Had ik dit vooraf geweten, dan had ik de business logic en de frameworkkoppeling eerst in aparte modules gescheiden. Het gaat om een gerichte stap zodra een migratie zich daadwerkelijk aandient, in plaats van een preventieve exercitie voor een hypothetische toekomst.
Reageren
Heb je zelf een framework-migratie achter de rug? Deel wat bij jou wel of niet werkte, dat soort praktijkervaring is precies waar dit soort keuzes op zouden moeten steunen.