arrow up
Nieuws overzicht
Guy De Gruyter

Door Guy De Gruyter

2 december 2022

Flexibel beheer software loonmotor
Deel 2: Modulaire software in de praktijk

Bij Teal Partners zijn we gespecialiseerd in complexe softwareprojecten. Graag vertellen we jullie in deze blogreeks in twee afleveringen meer over de inhoudelijke aanpak van één van onze projecten. Lees deel één hier.

Het developersteam van Teal Partners ging op zoek naar de ideale architectuur voor het ontwikkelen van een internationale loonmotor. In dit blogartikel legt Guy De Gruyter, software en developer architect bij Teal Partners, de keuze voor een modulaire aanpak toe.

In samenwerking met SD Worx ontwikkelt Teal Partners een applicatie die hr-departementen van kmo’s ondersteunt. Met dit softwarepakket beheren kmo’s alle hr-gerelateerde informatie voor hun medewerkers. De nieuwe loonmotor berekent onderliggend de lonen, de sociale zekerheidsbijdragen en de inhouding van belastingen.

Een loonmotor kan vandaag veel meer dan rekenen en afrekenen (het uitbetalen van de lonen). Werkgevers willen hun werknemers graag een interessant pakket van extralegale voordelen bieden om het verschil te maken in de ‘war for talent’. Ook de mogelijkheid om deeltijds of van thuisuit te werken voor een betere werk-privébalans maken deel uit van zulk pakket.

Met de nieuwe loonmotor willen we bedrijven toelaten om loonkostvoorspellingen te maken tot een jaar vooruit. Hij moet simulaties mogelijk maken van diverse verloningspakketten of tewerkstellingsscenario’s, en hun impact op de kost en nettolonen.

Drie redenen om te kiezen voor een modulaire aanpak

Om die opties in het softwarepakket van de loonmotor te verwerken, kozen we voor een modulaire aanpak. Er zijn drie redenen om te kiezen voor een modulaire aanpak: (1) verschillende teams kunnen gelijktijdig features opleveren, (2) de oplossing kan gradueel geïntroduceerd worden in een nieuwe markt, en (3) de modules kunnen apart geschaald worden. We gaan er dieper op in.

Een modulaire aanpak laat toe om gelijktijdig nieuwe features te ontwikkelen in verschillende teams:

De modules hebben elk een eigen scrum board en GIT repo. Daardoor kunnen de teams gelijktijdig aan nieuwe features werken en software releases doen in hun eigen tempo. Met meerdere kleinere teams blijven overleg en merge conflicten beperkt. Softwareontwikkelaars zich kunnen concentreren op hun kerntaak: het analyseren en oplossen van softwarevraagstukken, elk binnen hun eigen module.

Guy De Gruyter

Een modulaire aanpak laat toe om gefaseerd te werken en deeloplossingen in te zetten:

Het omzetten van wetgeving in parametrisatie van de software en het integreren van de software met overheidsdiensten is een uitgebreid proces. Door het moduleren van de software kunnen we bijvoorbeeld het beheer van werknemers, contracten, verloning en werktijden al in productie zetten, terwijl we voor de berekening van de payroll en aangiftes aan de overheid nog gebruik maken van bestaande softwareoplossingen.

Een modulaire aanpak heeft het voordeel van schaalbaar te zijn:

Niet elke module heeft dezelfde infrastructuurnoden. Het berekenen van lonen vraagt veel rekenkracht. Het beheer van contracten focust op de interactie met de gebruiker. Door de modulaire aanpak kunnen we telkens kiezen voor de meest optimale onderliggende infrastructuur en verschillende strategieën inzetten voor het opschalen bij piekbelasting.

Een modulaire aanpak laat toe om gelijktijdig nieuwe features te ontwikkelen in verschillende teams. Zo kunnen softwareontwikkelaars zich focussen op hun kerntaak.

Hoe pakten we het aan in de praktijk?

De oplossing is opgebouwd volgens de principes van een ’microservices architectuur’ met vier modules.

  • De ‘contracting module’ is verantwoordelijk voor het capteren van payroll-input.
  • De ‘payroll module’ is verantwoordelijk voor de berekening en afrekening van de lonen.
  • De ‘payment & declaration module’ is verantwoordelijk voor betalingen en aangiftes.
  • De ‘config module’ is verantwoordelijk voor het beheer en de publicatie van de parametrisatie.

Elke module heeft een eigen domeinmodel en een aparte databank. De data wordt tussen de modules op een asynchrone manier uitgewisseld. In de databank wordt een onderscheid gemaakt tussen

  • de ‘owned data’ die door de module beheerd wordt;
  • de ‘reference data’ die de module ontvangt van andere modules;
  • de ‘definition data’ die een weerslag is van de parametrisatie.

Een voorbeeld vanuit het perspectief van de payroll module:

  • Alle payroll-input die gecapteerd wordt in de contracting module wordt uitgewisseld met de payroll module en wordt daar opgeslagen in de reference data.
  • Alle data over loonperiodes en loonberekeningen wordt beheerd door de payroll module en dus opgeslagen in de owned data.
  • De parametrisatie van de loonmotor, zoals o.a. de invulling van de berekeningsniveaus en berekeningsmodellen, wordt gepubliceerd door de config module en wordt opgeslagen in de definition data.

Van bij de start kozen we ervoor om de ‘definition dataset’, de relevante set aan parametrisatiedata, in elke databank te bewaren. Dit laat ons toe de referentiële integriteit te garanderen.

Onderstaande tekening verduidelijkt de opzet van de softwareoplossing in modules.

Kiezen van de juiste grenzen

Het bepalen van de juiste grenzen tussen de modules is cruciaal in een modulaire architectuur.

Volgende drivers hebben ons geholpen om de juiste opdeling te kiezen:

  • Modules moeten onafhankelijk van elkaar kunnen ingezet worden. Dit ondersteunt de mogelijkheid om de software gradueel te introduceren in een nieuwe markt.
  • Er is een beperkte afhankelijkheid tussen de modules en de afhankelijkheid gaat in één richting. Bij een wirwar van afhankelijkheden gaan de voordelen van een modulaire architectuur immers verloren.
  • De data-uitwisseling tussen de modules kan asynchroon uitgevoerd worden. Dit criterium is belangrijk om modules onafhankelijk van elkaar te kunnen releasen.

Het opsplitsen van de software in modules is dus anders dan het opsplitsen van de software in services. Terwijl we bij services kijken naar de technische verantwoordelijkheid kijken we bij modules naar het splitsen van het domein in functionele entiteiten met een beperkte onderlinge afhankelijkheid.

Naast het opsplitsen van de modules hebben we een aantal technische patronen gedefinieerd om ze doorgedreven te ontkoppelen zonder de consistentie van de data te verliezen.

Referentiedata

Elke module bewaart de relevante data van de andere modules in de eigen database. Dit noemen we de ‘referentiedata’. Deze werkwijze biedt drie voordelen bij het ontkoppelen van de software.

  • Elke module bewaart alleen de relevante data. Hij bewaart deze data in de vorm zoals zij het best kan geconsumeerd worden voor het functioneel domein waarin de module opereert.
  • De afhankelijkheid gaat in één richting. Module A stuurt op het moment van de wijziging de data naar module B. Doordat module B de data lokaal bewaart, is een callback van module B naar module A om de data op te vragen overbodig.
  • Het effect van een datawijziging kan asynchroon verwerkt worden, los van het proces dat de data in de referentiedata opslaat.

Inbox/outbox zorgt voor dataconsistentie tussen de modules

Nadat de software in modules is opgesplitst, is de weg vrij om te ontwikkelen. De teams kunnen in hun eigen tempo en met een eigen aanpak hun module ontwikkelen. Cruciaal voor de duurzaamheid van een modulaire aanpak is het uitwisselen van data tussen de modules. Wij kozen voor een inbox/outbox-patroon.

Om het concept van het inbox/outbox-patroon uit te leggen, doorlopen we de verschillende stappen van een datawijziging.

Guy De Gruyter

Stap 1. De data wordt gewijzigd in module A

Elke request in een module (bijvoorbeeld het opslaan van een invulformulier op de UI) wordt behandeld door een unit of work en een bijhorende databasesessie. Dat wil zeggen dat de verwerking atomair is: de data wordt in zijn geheel bewaard of verworpen.

Elke wijziging van de database resulteert in één bericht per externe module met de gewijzigde data. Het bericht wordt echter niet onmiddellijk verstuurd maar eerst weggeschreven in de outbox. De outbox is een tabel die de uitgaande data van een module bevat. Door de outbox te vullen in dezelfde sessie als degene waarin de ‘owned data’ wordt gewijzigd is het bericht in de outbox deel van de atomaire actie. Met andere woorden; er is geen wijziging van de owned data zonder record(s) in de outbox en vice versa.

Stap 2. De berichten worden verstuurd van module A naar module B

Na het afwerken van de request wordt een achtergrondproces in gang gezet dat alle onbehandelde berichten in de outbox oppikt en doorstuurt naar de correcte modules. Dit achtergrondproces heeft zijn eigen unit of work en wordt volledig asynchroon uitgevoerd. De ontvangende module heeft een inbox API voor het ontvangen van de berichten.

De ontvangende module bewaart het bericht in een Inbox-tabel. Het is niet meer dan een log van alle inkomende berichten van de andere modules.

Stap 3. Het inbox-bericht wordt verwerkt in module C

Na het ontvangen van een inbox-bericht zal de ontvangende module een nieuw proces starten, opnieuw met een eigen unit of work, voor het verwerken van dit inkomende bericht. De impact van de inkomende data wordt weggeschreven in de referentiedata, en de impact op de owned data wordt al dan niet asynchroon verwerkt.

Dit patroon brengt een aantal eigenschappen met zich mee.

  • De data tussen de modules is ‘eventually consistent’: de consistentie is gegarandeerd maar niet onmiddellijk.
  • De berichten worden verwerkt in de volgorde waarin ze zijn ontstaan.
  • De verwerking is idempotent. De unieke ID’s van de berichten vermijden dat zij in de ontvangende modules twee keer verwerkt worden, ook wanneer een bericht bij een ‘retry’ een tweede maal wordt aangeboden.
  • Wanneer een module tijdelijk offline is, kunnen de andere modules onafhankelijk verder functioneren. De data wordt bijgewerkt zodra de module terug online is.

Conclusie

In dit artikel hebben we de voordelen zijn van een modulaire aanpak besproken.

  1. We kunnen verschillende teams gelijktijdig features laten opleveren;
  2. De oplossing kan gradueel geïntroduceerd worden in een nieuwe markt;
  3. De modules kunnen apart geschaald worden.

De grootste uitdaging is om een doorgedreven ontkoppeling tussen de modules te realiseren en toch de consistentie van data tussen de modules te garanderen. Dit realiseren we door de keuze van de grenzen van de modules, het gebruik van referentiedata en het outbox/inbox patroon.

Herkennen jullie in dit artikel concepten uit ‘Domain Driven Design’ of ‘Microservices’? Dat kan kloppen. We zijn in dit artikel bewust de hype rond deze technologieën uit de weg gegaan door ze niet expliciet zo te benoemen. De concepten uit de literatuur hebben ons op weg gezet. Wij passen ze in de praktijk toe, vertrekkende vanuit de probleemstelling. Met dit artikel wilden we graag de spotlight zetten op de pragmatische aanpak.

We hopen jullie iets te hebben bijgebracht door onze ervaring te delen. Het zou leuk zijn om reactie te krijgen op deze blog. Elke feedback is welkom. Wat vinden jullie van deze oplossing?