In deze blogpost ligt een case toe waar ik recent aan gewerkt hebt. Het is een technische blog waarin ik patronen bespreek en laat zien hoe we dit oplossen bij een groot geschaald systeem waar meerdere losstaande microservice-websites los van elkaar informatie verwerken en meerdere services domein overstijgend samenwerken. Het voorbeeld dat ik in deze blogpost gebruik is de case van een registratie proces binnen een microservice architectuur. In het eerste stuk van het artikel ligt ik de randvoorwaarden toe en daarna geef ik een praktisch voorbeeld.
Microservices
Microservices zijn een softwarearchitectuur waarbij een applicatie wordt opgebouwd uit kleine, zelfstandige services die elk een specifiek doel vervullen. Deze services communiceren met elkaar via een gestandaardiseerd interface, zoals een API. Dit stelt ontwikkelaars in staat om snel nieuwe functionaliteiten toe te voegen of bestaande te wijzigen, omdat elke service afzonderlijk kan worden ontwikkeld, getest en gedepolyed. Microservices bieden ook flexibiliteit en schaalbaarheid, omdat elke service afzonderlijk kan worden opgeschaald of bijgewerkt zonder de hele applicatie te hoeven aanpassen. Het gebruik van microservices is vooral geschikt voor grote, complexe systemen met veel veranderende vereisten.
Het antipatroon: Chatty microservices of monolithic microservices
Een antipatroon zijn chatty microservices. Dit zijn microservices die frequent communiceren met elkaar door middel van het sturen van veel kleine berichten, zoals HTTP API-aanvragen. Dit kan leiden tot vertragingen en inefficiënties, omdat elke aanvraag tijd kost om te verzenden en te verwerken, daarnaast loopt het proces vast als een service niet direct antwoord geeft. Bovendien kan het ook leiden tot extra foutgevoeligheid, omdat elke aanvraag een potentieel punt is waar iets mis kan gaan. In plaats van chatty microservices is het beter om microservices te ontwerpen die berichten uitwisselen via een servicebus. Daarnaast houden we een outbox-tabel bij om te voorkomen dat een service een bericht niet kan uitvoeren. Wat dit inhoud ga ik in deze blog uitleggen.
Wat is SAGA?
SAGA is een architectuur patroon dat wordt gebruikt om complexe transacties te behandelen die betrekking hebben op verschillende domeinen of systemen. Het patroon is gebaseerd op het idee dat elke transactie kan worden opgedeeld in een aantal kleinere stappen, waarbij elke stap een “compensatie” -stap heeft die kan worden uitgevoerd op het moment dat de transactie niet succesvol is. Dit maakt het mogelijk om transacties te verwerken die over verschillende domeinen of systemen gaan, zonder dat er consistentieproblemen optreden. SAGA is vooral nuttig voor grote, complexe systemen waarbij verschillende componenten uit het systeem benodigd zijn om de transactie te voltooien.
Wat is een servicebus?
Een servicebus is een softwarecomponent die wordt gebruikt om te communiceren tussen verschillende applicaties of systemen. Het stelt deze systemen in staat om berichten naar elkaar te sturen via een gestandaardiseerd protocol, zoals AMQP (Advanced Message Queuing Protocol). Een servicebus kan worden gebruikt om asynchrone communicatie tussen systemen te ondersteunen, wat kan helpen om de prestaties en schaalbaarheid van het systeem te verbeteren. Een bekend voorbeeld van een servicebus is RabbitMQ, een open source implementatie van AMQP. RabbitMQ wordt vaak gebruikt in microservice-architecturen om communicatie tussen verschillende microservices mogelijk te maken.
Docker en Microsoft .NET 6
Docker is een open source containerplatform dat het mogelijk maakt om applicaties te bundelen en te distribueren als containerimages. Deze containerimages kunnen vervolgens op elk systeem met Docker geïnstalleerd worden, wat ervoor zorgt dat de applicatie op elk systeem op dezelfde manier uitgevoerd kan worden. Dit zorgt ervoor dat de kwaliteit omhoog gaat. In technische termen wordt dit: “environment parity” genoemd. .NET is een softwareframework van Microsoft dat wordt gebruikt voor het ontwikkelen van Windows-applicaties. .NET kan echter ook gebruikt worden om webapplicaties te ontwikkelen en is beschikbaar voor verschillende besturingssystemen. Door het gebruik van Docker kunnen .NET-applicaties op een consistente manier worden uitgevoerd op elk systeem met Docker geïnstalleerd, wat het makkelijker maakt om deze applicaties te distribueren en te implementeren.
In wat voor case zet je SAGA bijvoorbeeld in?
Een goede case om SAGA in te zetten is een registratieproces, op basis van dit voorbeeld lig ik de case concreet toe.
Dit registratieproces begint in de microservice voor een registratie. In deze registratieservice worden niet afgemaakte registraties bijgehouden. In ons geval moet een admin gebruiker toestemming (approve) geven voor de gebruiker om in het platform te komen. Pas dan wordt er in het identity systeem een gebruiker opgevoerd en wordt er een terugkoppeling gedaan aan de registratieservice dat de registratie is afgerond.
Hoe ziet de flow eruit:
In deze alinea leg ik het in hoofdlijnen uit, dan heb je een beeld van hoe het proces eruit ziet. In de volgende alinea heb ik een uitgebreidere toelichting van de stappen. De code die te zien is daarnaast ook beschikbaar op Github
Het begint allemaal met een klant die een registratie opvoert
In de onderstaande afbeelding is dit te zien via swagger, een front-end maakt dit natuurlijk helemaal af
Als je deze Execute dan krijg je een correlationId, dit is een waarde om de registratie aan elkaar te koppelen tussen verschillende losstaande services.
Na dat een registratie is aangemaakt heb je dezelfde CorrelationId nodig om deze openstaande registratie goed te keuren.
Vervolgens wordt er een bericht op de service bus gezet en wordt er door de Identity service een gebruiker aangemaakt. Als je dezelfde correlationId of email adres nog een keer invoert dan wordt het proces gestopt omdat deze gebruiker dan al bestaat.
Het rechterscherm met de Notifications luistert op de servicebus en geeft de registraties realtime weer.
Hoe ziet dit proces eruit in meer detail
Om een beter overzicht te geven van de flow heb ik flowchart gemaakt die alle stappen beschrijven. De code die te zien is daarnaast ook beschikbaar op Github
Http request to /create
endpoint
Bij de stap “User Submit” wordt er vanuit de controller een event op de servicebus gezet. Dit doen we om alle Sagas bij elkaar te houden en in de Saga service een registratie aan te maken. In het onderstaande screen zie je de controller die een queueing service aanroept. Qua naamgeving houden we hier een command aan, dit is een intern bericht dat niet mis kan gaan. Een command blijft altijd binnen dezelfde service. Dat bekent dus dat een event met command in de naam niet gedeeld wordt tussen verschillende services. Een Event is een servicebus-bericht waar wel een mislukte variant van zou kunnen bestaan en die gedeeld kan worden tussen verschillende services.
Registration SAGA listens to RegistrationSubmittedEvent
The Registration Saga is gelinkt aan een onderliggende service, dit is gedaan in de startup door het volgende stuk code uit te voeren: services.RegisterSaga
De Registration Service is afhankelijk van ISagaStateRepository, dit is een interface die ervoor zorgt dat de Saga service altijd op een gelijke manier een create of update kan uitvoeren.
De InstanciateAsync zorgt ervoor dat er een registratie wordt aangemaakt met de Status “Created”
Stap: Http request to /approve endpoint
Als er vanuit Swagger een POST request wordt gedaan naar het approve endpoint dan wordt er op een gelijke manier een command servicebus event uitgevoerd zodat de SAGA service deze kan oppakken.
Stap: Add RabbitMQ Event to service bus: _RegistrationApprovedEvent
In de onderstaande code wordt een registratie op approved gezet, mocht dat mislukken vanwege een Database error dan wordt er een NackException gegooid. Nack betekent Not Acknowledged. Een NackException is om RabbitMQ te laten weten dat die het opnieuw te proberen op een later moment. Als het een andere fout is dan wordt de registratie als mislukt opgegeven.
Stap: Create User
In de Identity service wordt hetzelfde patroon toegepast. De InstanciateAsync zorgt ervoor dat er een gebruiker wordt opgevoerd. In de onderliggende service UserService wordt een gebruiker aangemaakt in de database.
In de Registratie Service wordt er aan het einde de status bijgewerkt zodat er een duidelijk overzicht is van succesvolle registraties
Conclusie
Het SAGA architectuur patroon zorgt ervoor dat elk bericht minimaal één keer aankomt. Het is een complex patroon dat mogelijk relevant kan zijn binnen een complexe architectuur. Ik hoop dat ik doormiddel van deze praktische case een aantal handvaten heb gegeven.