Building Domain Driven Microservices

Chandra Ramalingam

Follow

1 jul, 2020 – 18 min read

Image credits:

De term ‘micro’ in Microservices geeft weliswaar de omvang van een service aan, maar is niet het enige criterium dat van een applicatie een Microservice maakt. Wanneer teams overstappen op een op microservices gebaseerde architectuur, willen ze hun wendbaarheid vergroten – functies autonoom en frequent implementeren. Het is moeilijk om één enkele beknopte definitie van deze architectuurstijl te geven. Ik vond deze korte definitie van Adrian Cockcroft – “service-oriented architecture composed of loosely coupled elements that have bounded contexts.”

Hoewel dit een high-level design heuristic definieert, heeft Microservices architectuur een aantal unieke kenmerken die het onderscheiden van de service-georiënteerde architectuur van weleer. Een paar van die kenmerken, hieronder. Deze en een paar meer zijn goed gedocumenteerd – Martin Fowler’s artikel en Sam Newman’s Building Microservices, om er een paar te noemen.

  1. Services hebben goed gedefinieerde grenzen gecentreerd rond de zakelijke context, en niet rond willekeurige technische abstracties
  2. Verberg implementatiedetails en stel functionaliteit bloot via intentie-onthullende interfaces
  3. Services delen hun interne structuren niet buiten hun grenzen. Bijvoorbeeld, geen delen van databases.
  4. Services zijn veerkrachtig voor storingen.
  5. Teams bezitten hun functies onafhankelijk en hebben de mogelijkheid om veranderingen autonoom vrij te geven
  6. Teams omarmen een cultuur van automatisering. Bijvoorbeeld geautomatiseerd testen, continue integratie en continue levering

In het kort kunnen we deze architectuurstijl als volgt samenvatten:

Losjes gekoppelde servicegeoriënteerde architectuur, waarbij elke service is ingesloten binnen een goed gedefinieerde afgebakende context, waardoor snelle, frequente en betrouwbare levering van applicaties mogelijk wordt.

Domein-gedreven ontwerp en begrensde contexten

De kracht van microservices komt voort uit het duidelijk definiëren van hun verantwoordelijkheid en het afbakenen van de grens tussen hen. Het doel hier is om een hoge cohesie binnen de grens en een lage koppeling erbuiten op te bouwen. Dat wil zeggen, dingen die de neiging hebben om samen te veranderen moeten bij elkaar horen. Zoals bij veel echte problemen is dit gemakkelijker gezegd dan gedaan – bedrijven evolueren, en veronderstellingen veranderen. Daarom is de mogelijkheid om te refactoren een ander belangrijk punt om te overwegen bij het ontwerpen van systemen.

Domain-driven design (DDD) is een belangrijk, en naar onze mening, een noodzakelijk hulpmiddel bij het ontwerpen van microservices, of het nu gaat om het doorbreken van een monoliet of het implementeren van een greenfield project. Domain-driven design, beroemd geworden door Eric Evans in zijn boek , is een set van ideeën, principes en patronen die helpen bij het ontwerpen van software systemen gebaseerd op het onderliggende model van het business domein. De ontwikkelaars en domeinexperts werken samen om bedrijfsmodellen te creëren in een Ubiquitous gemeenschappelijke taal. Vervolgens binden zij deze modellen aan systemen waar dat zinvol is en stellen zij samenwerkingsprotocollen op tussen deze systemen en de teams die aan deze diensten werken. Belangrijker nog, zij ontwerpen de conceptuele contouren of grenzen tussen de systemen.

Microservice-ontwerp put inspiratie uit deze concepten, omdat al deze principes helpen bij het bouwen van modulaire systemen die onafhankelijk van elkaar kunnen veranderen en evolueren.

Voordat we verder gaan, laten we snel enkele van de basisterminologieën van DDD doornemen. Een volledig overzicht van Domain-Driven Design valt buiten het bestek van deze blog. We bevelen het boek van Eric Evans ten zeerste aan voor iedereen die microservices wil bouwen

Domein: Vertegenwoordigt wat een organisatie doet. In het onderstaande voorbeeld zou dat Retail of eCommerce zijn.

Subdomain: Een organisatie of business unit binnen een organisatie. Een domein is samengesteld uit meerdere subdomeinen.

Ubiquitous language: Dit is de taal die wordt gebruikt om de modellen uit te drukken. In het onderstaande voorbeeld is Item een Model dat behoort tot de Ubiquitous taal van elk van deze subdomeinen. Ontwikkelaars, productmanagers, domeinexperts en zakelijke belanghebbenden zijn het eens over dezelfde taal en gebruiken deze in hun artefacten – code, productdocumentatie, enzovoort.

Fig 1. Subdomeinen en begrensde contexten in het e-commercedomein

Gebonden contexten: Domain-driven design definieert Bounded Contexts als “De setting waarin een woord of een uitspraak verschijnt die de betekenis ervan bepaalt.” In het kort betekent dit de grens waarbinnen een model zinvol is. In het bovenstaande voorbeeld krijgt “Item” in elk van die contexten een andere betekenis. In de cataloguscontext betekent een item een verkoopbaar product, terwijl het in de winkelwagencontext het item betekent dat de klant aan haar winkelwagen heeft toegevoegd. In de Fulfillment context betekent het een Warehouse Item dat naar de klant zal worden verzonden. Elk van deze modellen is verschillend, en elk heeft een andere betekenis en bevat mogelijk verschillende attributen. Door deze modellen te scheiden en te isoleren binnen hun respectievelijke grenzen, kunnen we de modellen vrij en zonder dubbelzinnigheid uitdrukken.

Noot: Het is essentieel om het onderscheid te begrijpen tussen Subdomeinen en Begrensde contexten. Een subdomein behoort tot de probleemruimte, dat wil zeggen, hoe uw bedrijf het probleem ziet, terwijl Bounded contexten behoren tot de oplossingsruimte, dat wil zeggen, hoe we de oplossing van het probleem zullen implementeren. Theoretisch kan elk subdomein meerdere begrensde contexten hebben, hoewel we streven naar één begrensde context per subdomein.

Hoe zijn Microservices gerelateerd aan begrensde contexten

Nu, waar passen Microservices? Is het eerlijk om te zeggen dat elke begrensde context overeenkomt met een microservice? Ja en nee. We zullen zien waarom. Er kunnen gevallen zijn waarin de grens of contour van uw begrensde context vrij groot is.

Fig 2. Begrensde context en microservices

Bekijk het bovenstaande voorbeeld. De begrensde context Prijsstelling heeft drie verschillende modellen – Prijs, Geprijsde items en Kortingen, die elk verantwoordelijk zijn voor respectievelijk de prijs van een catalogusitem, het berekenen van de totale prijs van een lijst met items, en het toepassen van kortingen. We zouden een enkel systeem kunnen maken dat alle bovengenoemde modellen omvat, maar dat zou een onredelijk grote toepassing kunnen worden. Elk van de gegevensmodellen heeft, zoals eerder vermeld, zijn eigen invarianten en bedrijfsregels. Na verloop van tijd, als we niet oppassen, zou het systeem een grote bal modder kunnen worden met onduidelijke grenzen, overlappende verantwoordelijkheden, en waarschijnlijk terug naar waar we begonnen – een monoliet.

Een andere manier om dit systeem te modelleren is om gerelateerde modellen te scheiden, of te groeperen in afzonderlijke microservices. In DDD worden deze modellen – Prijs, Geprijsde artikelen en Kortingen – Aggregates genoemd. Een aggregate is een op zichzelf staand model dat gerelateerde modellen samenstelt. Je kunt de toestand van een aggregate alleen veranderen via een gepubliceerde interface, en het aggregate zorgt voor consistentie en dat de invarianten goed blijven.

Formally, An Aggregate is a cluster of associated objects treated as a unit for data changes. Externe verwijzingen zijn beperkt tot één lid van de AGGREGATE, aangeduid als de root. Binnen de grenzen van de AGGREGATE geldt een reeks consistentieregels.

Fig 3. Microservices in de prijscontext

Ook hier is het niet nodig om elk aggregaat als een aparte microservice te modelleren. Dat bleek zo te zijn voor de services (aggregaten) in Fig 3, maar dat is niet noodzakelijkerwijs een regel. In sommige gevallen kan het zinvol zijn om meerdere aggregates in een enkele service te hosten, vooral als we het bedrijfsdomein niet volledig begrijpen. Belangrijk om op te merken is dat consistentie alleen gegarandeerd kan worden binnen een enkel aggregaat, en dat de aggregaten alleen gewijzigd kunnen worden via de gepubliceerde interface. Elke overtreding hiervan draagt het risico in een grote bal modder te veranderen.

Context maps – A way to carve out accurate microservice boundaries

Een andere essentiële toolkit in je arsenaal is het concept van Context maps – opnieuw, van Domain Driven Design. Een monoliet is gewoonlijk samengesteld uit ongelijksoortige modellen, meestal strak gekoppeld – modellen kennen misschien de intieme details van elkaar, het veranderen van één kan neveneffecten op een ander veroorzaken, enzovoort. Als je de monoliet afbreekt, is het van vitaal belang om deze modellen – aggregaten in dit geval – en hun relaties te identificeren. Context maps helpen ons precies dat te doen. Ze worden gebruikt om relaties tussen verschillende begrensde contexten en aggregaten te identificeren en te definiëren. Terwijl begrensde contexten de grens van een model bepalen – Prijs, Kortingen, enz. in het bovenstaande voorbeeld, bepalen Contextkaarten de relaties tussen deze modellen en tussen verschillende contexten. Nadat we deze afhankelijkheden hebben geïdentificeerd, kunnen we het juiste samenwerkingsmodel bepalen tussen de teams die deze diensten zullen implementeren.

Een volledige verkenning van Context maps valt buiten het bestek van deze blog, maar we zullen het illustreren met een voorbeeld. Het onderstaande diagram geeft de verschillende toepassingen weer die de betalingen voor een eCommerce-bestelling afhandelen.

  1. De winkelwagen context zorgt voor online autorisaties van een bestelling; Order context verwerkt post fulfillment betalingsprocessen zoals Settlements; Contact center handelt eventuele uitzonderingen af zoals het opnieuw proberen van betalingen en het wijzigen van de betalingsmethode die voor de bestelling is gebruikt
  2. Voor de eenvoud, laten we aannemen dat al deze contexten zijn geïmplementeerd als afzonderlijke services
  3. Al deze contexten kapselen hetzelfde model in.
  4. Merk op dat deze modellen logisch gezien hetzelfde zijn. Dat wil zeggen dat ze allemaal dezelfde Ubiquitous-domeintaal volgen – betalingsmethoden, autorisaties, en vereffeningen. Alleen maken ze deel uit van verschillende contexten.

Een ander teken dat hetzelfde model verspreid is over verschillende contexten is dat ze allemaal rechtstreeks integreren met een enkele betalingsgateway en dezelfde operaties uitvoeren als elkaar

Fig 4. Een onjuist gedefinieerde context map

Herdefiniëren van de dienstgrenzen – Breng de aggregaten in kaart met de juiste contexten

Er zijn een paar problemen die zeer duidelijk zijn in het bovenstaande ontwerp (Fig 4). De aggregaten van betalingen maken deel uit van meerdere contexten. Het is onmogelijk om invarianten en consistentie af te dwingen over verschillende diensten, om nog maar te zwijgen van de concurrency problemen tussen deze diensten. Bijvoorbeeld, wat gebeurt er als het contact center de betalingsmethode wijzigt die geassocieerd is met de order terwijl de Orders service probeert om de afwikkeling van een eerder ingediende betalingsmethode te posten. Merk ook op dat elke verandering in de betalingsgateway veranderingen zou forceren in meerdere diensten en mogelijk talrijke teams, omdat verschillende groepen eigenaar zouden kunnen zijn van deze contexten.

Met een paar aanpassingen en het uitlijnen van de aggregaten naar de juiste contexten, krijgen we een veel betere voorstelling van deze subdomeinen – Fig 5. Er is veel veranderd. Laten we de veranderingen eens bekijken:

  1. Betalingen aggregaat heeft een nieuw huis – Betalingsdienst. Deze dienst abstraheert ook de betalingsgateway van de andere diensten die betalingsdiensten nodig hebben. Omdat een enkele begrensde context nu eigenaar is van een aggregaat, zijn de invarianten eenvoudig te beheren; alle transacties vinden plaats binnen dezelfde service begrenzing, wat helpt om eventuele concurrency problemen te voorkomen.
  2. Betalingen aggregaat gebruikt een Anti-corruptie Laag (ACL) om het kerndomeinmodel te isoleren van het gegevensmodel van de betalingsgateway, die gewoonlijk een derde partij provider is en misschien gebonden is aan verandering. We zullen dieper ingaan op het applicatie-ontwerp van een dergelijke service door gebruik te maken van het Ports and Adapters patroon in een toekomstige post. De ACL laag bevat gewoonlijk de adapters die het datamodel van de betalingsgateway transformeren naar het geaggregeerde datamodel van Betalingen.
  3. Kartservice roept de Betalingen-service aan via directe API-aanroepen, omdat de wagenservice mogelijk de betalingsautorisatie moet voltooien terwijl de klanten op de website zijn
  4. Maak een notitie van de interactie tussen Bestellingen en Betalingen-service. Orders service zendt een domein event uit (meer hierover later in deze blog). Payment service luistert naar dit event en voltooit de afhandeling van de order
  5. Contact center service kan vele aggregates hebben, maar we zijn alleen geïnteresseerd in de Orders aggregate voor deze use case. Deze dienst zendt een gebeurtenis uit wanneer de betalingsmethode verandert, en de dienst Betalingen reageert hierop door de eerder gebruikte creditcard terug te draaien en de nieuwe creditcard te verwerken.

Fig 5. Herdefinieerde context map

Over het algemeen heeft een monolithische of een legacy-applicatie veel aggregaten, vaak met overlappende grenzen. Het maken van een context map van deze aggregaten en hun afhankelijkheden helpt ons de contouren te begrijpen van eventuele nieuwe microservices die we uit deze monolieten gaan wringen. Onthoud dat het succes of falen van een microservices architectuur afhangt van een lage koppeling tussen de aggregaten en een hoge cohesie binnen deze aggregaten.

Het is ook belangrijk op te merken dat begrensde contexten zelf geschikte samenhangende eenheden zijn. Zelfs als een context meerdere aggregaten heeft, kan de hele context, samen met zijn aggregaten, worden samengesteld in een enkele microservice. Wij vinden deze heuristiek bijzonder nuttig voor domeinen die een beetje obscuur zijn – denk aan een nieuwe bedrijfstak waar de organisatie zich aan waagt. Je hebt misschien niet voldoende inzicht in de juiste scheidingsgrenzen, en een voortijdige decompositie van aggregaten kan leiden tot dure refactoring. Stel je voor dat je twee databases moet samenvoegen tot één, samen met datamigratie, omdat we toevallig hebben ontdekt dat twee aggregates bij elkaar horen. Maar zorg ervoor dat deze aggregates voldoende geïsoleerd zijn door middel van interfaces, zodat ze de ingewikkelde details van elkaar niet kennen.

Event Storming – Een andere techniek om service boundaries te identificeren

Event Storming is een andere essentiële techniek om aggregates (en dus microservices) in een systeem te identificeren. Het is een nuttig hulpmiddel zowel voor het afbreken van monolieten als bij het ontwerpen van een complex ecosysteem van microservices. We hebben deze techniek gebruikt om een van onze complexe applicaties af te breken, en we zijn van plan om onze ervaringen met Event Storming in een aparte blog te behandelen. Voor het bestek van deze blog, willen we een snel overzicht geven op hoog niveau. Bekijk Alberto Brandelloni’s video over het onderwerp als je geïnteresseerd bent om het verder te onderzoeken.

In een notendop, Event Storming is een brainstormoefening tussen de teams die werken aan een applicatie – in ons geval, een monolith – om de verschillende domeingebeurtenissen en -processen te identificeren die plaatsvinden binnen een systeem. De teams identificeren ook de aggregaten of modellen waarop deze gebeurtenissen van invloed zijn en de eventuele latere gevolgen daarvan. Terwijl de teams deze oefening doen, identificeren ze verschillende overlappende concepten, dubbelzinnige domeintaal, en conflicterende bedrijfsprocessen. Ze groeperen gerelateerde modellen, herdefiniëren aggregaten en identificeren dubbele processen. Naarmate ze verder komen met deze oefening, worden de afgebakende contexten waar deze aggregaten thuishoren duidelijk. Event Storming workshops zijn nuttig als alle teams in één ruimte zijn – fysiek of virtueel – en beginnen met het in kaart brengen van de events, commando’s en processen op een scrum-stijl whiteboard. Aan het eind van deze oefening zijn hieronder de gebruikelijke uitkomsten:

  1. Gedefinieerde lijst van Aggregaten. Deze worden mogelijk nieuwe microservices
  2. Domain Events die tussen deze microservices moeten stromen
  3. Commands die directe aanroepen zijn van andere applicaties of gebruikers

We hebben hieronder een voorbeeldbord aan het einde van een Event Storming workshop laten zien. Het is een geweldige samenwerkingsoefening voor de teams om het eens te worden over de juiste aggregaten en begrensde contexten. Naast het feit dat het een geweldige teambuildingsoefening is, komen de teams uit deze sessie met een gedeeld begrip van het domein, ubiquitous taal, en precieze service grenzen.

Fig 6. Event Storming board

Communicatie tussen microservices

Om het kort samen te vatten: een monoliet host meerdere aggregaten binnen een enkele procesgrens. Daarom is het mogelijk om de consistentie van aggregaten binnen deze boundary te beheren. Bijvoorbeeld, als een klant een order plaatst, kunnen we de inventaris van de items verlagen, een e-mail sturen naar de klant – allemaal binnen een enkele transactie. Alle operaties zouden slagen, of alle zouden mislukken. Maar, als we de monoliet afbreken en de aggregaten in verschillende contexten verspreiden, zullen we tientallen of zelfs honderden microservices hebben. De processen die tot dan toe binnen de enkele grens van een monoliet bestonden, zijn nu verspreid over meerdere gedistribueerde systemen. Het bereiken van transactionele integriteit en consistentie over al deze gedistribueerde systemen is erg moeilijk, en er hangt een prijskaartje aan – de beschikbaarheid van de systemen.

Microservices zijn ook gedistribueerde systemen. Vandaar dat de CAP stelling ook op hen van toepassing is – “een gedistribueerd systeem kan slechts twee van de drie gewenste eigenschappen leveren: consistentie, beschikbaarheid, en partitietolerantie (de ‘C’, ‘A’ en ‘P’ in CAP).” In echte systemen is partitietolerantie niet onderhandelbaar – netwerk is onbetrouwbaar, virtuele machines kunnen uitvallen, latency tussen regio’s kan erger worden, enzovoort.

Dus dat laat ons de keuze tussen ofwel Beschikbaarheid ofwel Consistentie. Nu weten we dat in elke moderne toepassing het opofferen van beschikbaarheid ook geen goed idee is.

Fig 7. CAP Theorem

Ontwerp applicaties rond uiteindelijke consistentie

Als je probeert transacties over meerdere gedistribueerde systemen te bouwen, kom je weer in het land van de monolieten terecht. Alleen zal het deze keer de ergste soort zijn, een gedistribueerde monoliet. Als een van de systemen onbeschikbaar wordt, wordt het hele proces onbeschikbaar, wat vaak leidt tot frustrerende klantervaringen, mislukte beloften, enzovoort. Bovendien brengen wijzigingen aan een dienst meestal wijzigingen aan een andere dienst met zich mee, wat leidt tot complexe en kostbare implementaties. Daarom is het beter om bij het ontwerpen van applicaties de use cases zo af te stemmen dat een beetje inconsistentie wordt getolereerd ten gunste van de beschikbaarheid. In het bovenstaande voorbeeld kunnen we alle processen asynchroon maken en dus uiteindelijk consistent. We kunnen e-mails asynchroon versturen, onafhankelijk van de andere processen; Als een beloofd artikel later niet beschikbaar is in het magazijn, kan het artikel nabesteld worden, of we kunnen stoppen met het aannemen van orders voor het artikel boven een bepaalde drempel.
Occasioneel kun je een scenario tegenkomen dat sterke ACID-stijl transacties zou kunnen vereisen over twee aggregaten in verschillende procesgrenzen. Dat is een uitstekend teken om deze aggregaten opnieuw te bekijken en ze misschien tot één aggregaat te combineren. Event Storming en Context Maps zullen helpen om deze afhankelijkheden in een vroeg stadium te identificeren, voordat we deze aggregaten gaan opsplitsen in verschillende procesgrenzen. Het samenvoegen van twee microservices tot één is kostbaar, en dat is iets wat we moeten proberen te vermijden.

Voorkeur event-driven architectuur

Microservices kunnen essentiële veranderingen uitzenden die in hun aggregaten gebeuren. Deze worden Domain-events genoemd, en alle services die in deze veranderingen geïnteresseerd zijn, kunnen naar deze events luisteren en binnen hun domein actie ondernemen. Deze methode vermijdt gedragskoppeling – het ene domein schrijft niet voor wat de andere domeinen moeten doen, en temporele koppeling – de succesvolle voltooiing van een proces hangt niet af van het feit of alle systemen op hetzelfde moment beschikbaar zijn. Dit betekent uiteraard dat de systemen uiteindelijk consistent zullen zijn.

Fig 8. Event driven-architectuur

In het bovenstaande voorbeeld publiceert de service Orders een gebeurtenis – Bestelling geannuleerd. De andere services die zich op de gebeurtenis hebben geabonneerd, verwerken hun respectieve domeinfuncties: Betaalservice betaalt het geld terug, Inventory-service past de voorraad van de artikelen aan, enzovoort. Een paar dingen om op te merken om de betrouwbaarheid en veerkracht van deze integratie te garanderen:

  1. Producenten moeten ervoor zorgen dat zij ten minste eenmaal een gebeurtenis produceren. Als dat niet lukt, moeten ze ervoor zorgen dat er een terugvalmechanisme is om de gebeurtenissen opnieuw te triggeren
  2. Consumers moeten ervoor zorgen dat ze de gebeurtenissen op een idempotente manier consumeren. Als dezelfde gebeurtenis opnieuw optreedt, mag dat geen neveneffecten hebben aan de kant van de consument. Gebeurtenissen kunnen ook buiten de volgorde aankomen. Consumenten kunnen gebruik maken van tijdstempel- of versienummers velden om de uniciteit van de events te garanderen.

Het is niet altijd mogelijk om event-gebaseerde integratie te gebruiken vanwege de aard van sommige use cases. Kijk eens naar de integratie tussen de Cart service en de Payment service. Het is een synchrone integratie, en daarom zijn er een paar dingen waar we op moeten letten. Het is een voorbeeld van koppeling op gedragsniveau – de Cart service roept misschien een REST API van de Payment service aan en instrueert deze om de betaling voor een bestelling te autoriseren, en koppeling op tijdsniveau – de Payment service moet beschikbaar zijn voor de Cart service om een bestelling te accepteren. Dit soort koppeling vermindert de autonomie van deze contexten en kan een ongewenste afhankelijkheid tot gevolg hebben. Er zijn een paar manieren om deze koppeling te vermijden, maar met al deze opties, verliezen we de mogelijkheid om directe feedback aan de klanten te geven.

  1. Verander de REST API naar een event-gebaseerde integratie. Maar deze optie is mogelijk niet beschikbaar als de betalingsservice alleen een REST API
  2. Kartservice accepteert een bestelling onmiddellijk, en er is een batchjob die de bestellingen ophaalt en de betalingsservice-API oproept
  3. Kartservice produceert een lokaal evenement dat vervolgens de betalingsservice-API oproept

Een combinatie van het bovenstaande met retries in geval van storingen en onbeschikbaarheid van de stroomopwaartse afhankelijkheid – betalingsservice – kan resulteren in een veel veerkrachtiger ontwerp. Bijvoorbeeld, de synchrone integratie tussen de Cart en de Payment services kan ondersteund worden door een event of batch-gebaseerde retries in geval van storingen. Deze aanpak heeft een extra impact op de klantenervaring – de klanten kunnen verkeerde betalingsgegevens hebben ingevoerd, en die zullen niet online zijn wanneer we de betalingen offline verwerken. Of er kunnen extra kosten voor de onderneming ontstaan om mislukte betalingen terug te vorderen. Maar naar alle waarschijnlijkheid wegen de voordelen van de Cart service, die bestand is tegen onbeschikbaarheid of storingen van de Payment service, zwaarder dan de tekortkomingen. Wij kunnen bijvoorbeeld de klanten verwittigen als wij niet in staat zijn offline betalingen te innen. Kortom, er zijn afwegingen tussen gebruikerservaring, veerkracht en operationele kosten, en het is verstandig om systemen te ontwerpen met deze compromissen in gedachten.

Vermijd orkestratie tussen services voor consument-specifieke gegevensbehoeften

Een van de anti-patronen in elke service-georiënteerde architectuur is dat de services tegemoetkomen aan de specifieke toegangspatronen van consumenten. Gewoonlijk gebeurt dit wanneer de consumententeams nauw samenwerken met de serviceteams. Als het team werkte aan een monolithische applicatie, zouden ze vaak een enkele API maken die verschillende aggregaat grenzen overschrijdt, waardoor deze aggregaten strak gekoppeld worden. Laten we eens een voorbeeld nemen. Stel dat de pagina Bestelgegevens in web- en mobiele applicaties de details van zowel een bestelling als de details van de verwerkte terugbetalingen voor de bestelling op een enkele pagina moet tonen. In een monolithische applicatie, een Order GET API – ervan uitgaande dat het een REST API is – bevraagt Orders en Refunds samen, consolideert beide aggregaten en stuurt een samengestelde respons naar de bellers. Het is mogelijk om dit zonder veel overhead te doen omdat de aggregaten tot dezelfde procesgrens behoren. Zo kunnen consumenten alle benodigde gegevens in een enkele aanroep krijgen.

Als Orders en Refunds deel uitmaken van verschillende contexten, zijn de gegevens niet langer aanwezig binnen een enkele microservice of aggregaatgrens. Een mogelijkheid om dezelfde functionaliteit voor de consumenten te behouden is door de Order service verantwoordelijk te maken voor het aanroepen van de Refunds service en een samengestelde respons te creëren. Deze benadering veroorzaakt verschillende problemen:

1. Besteldienst integreert nu met een andere dienst, louter ter ondersteuning van de consumenten die de restitutiegegevens samen met de bestelgegevens nodig hebben. Bestelservice is nu minder autonoom, omdat elke verandering in de Refunds aggregate zal leiden tot een verandering in de Order aggregate.

2. Bestelservice heeft een andere integratie en dus een ander storingspunt om rekening mee te houden – als Refunds service down is, kan Bestelservice dan nog steeds gedeeltelijke gegevens verzenden, en kunnen de consumenten dan sierlijk falen?

3. Als de consumenten een verandering nodig hebben om meer gegevens uit de Refunds aggregate op te halen, zijn er nu twee teams betrokken om deze verandering door te voeren.

4. Dit patroon kan, als het in het hele platform wordt gevolgd, leiden tot een ingewikkeld web van afhankelijkheden tussen de verschillende domeinservices, allemaal omdat deze services tegemoetkomen aan de specifieke toegangspatronen van de bellers.

Backend for Frontends (BFF’s)

Een benadering om dit risico te beperken, is om de orkestratie tussen de verschillende domeinservices door de consumententeams te laten beheren. Immers, de aanroepers kennen de toegangspatronen beter en kunnen de volledige controle hebben over eventuele wijzigingen in deze patronen. Deze aanpak ontkoppelt de domeinservices van de presentatietier, zodat zij zich kunnen richten op de kernprocessen van het bedrijf. Maar als de web en mobiele apps verschillende diensten direct gaan aanroepen in plaats van de ene samengestelde API van de monolith, kan dit leiden tot performance overhead voor deze apps – meerdere calls over lagere bandbreedte netwerken, verwerken en samenvoegen van gegevens uit verschillende API’s, enzovoort.

In plaats daarvan zou men een ander patroon kunnen gebruiken, genaamd Backend for Front-ends. In dit ontwerppatroon, een backend dienst gemaakt en beheerd door de consumenten – in dit geval, het web en mobiele teams – zorgt voor de integratie over meerdere domein diensten louter om de front-end ervaring te renderen aan de klanten. De web- en mobiele teams kunnen nu de gegevenscontracten ontwerpen op basis van de use cases die ze bedienen. Ze kunnen zelfs GraphQL gebruiken in plaats van REST API’s om op een flexibele manier query’s uit te voeren en precies terug te krijgen wat ze nodig hebben. Het is belangrijk op te merken dat deze service eigendom is van en onderhouden wordt door de consumententeams en niet door de teams die eigenaar zijn van de domeinservices. De front-end teams kunnen nu optimaliseren op basis van hun behoeften – een mobiele app kan een kleinere payload opvragen, het aantal calls vanuit de mobiele app verminderen, enzovoort. Kijk eens naar de herziene weergave van de orkestratie hieronder. De BFF-service roept nu zowel de Orders- als de Refunds-domeinservices aan voor zijn use-case.

Fig 9. Backend voor Frontends

Het is ook nuttig om de BFF-service vroeg te bouwen, voordat u een overvloed aan services uit de monolith afbreekt. Anders moeten de domeinservices de inter-domein orkestratie ondersteunen, of moeten de web- en mobiele apps meerdere services direct vanuit de front-end aanroepen. Beide opties leiden tot performance overhead, weggegooid werk en gebrek aan autonomie tussen de teams.

Conclusie

In deze blog hebben we verschillende concepten, strategieën en ontwerp heuristieken aangestipt om te overwegen wanneer we ons wagen in de wereld van microservices, meer specifiek wanneer we proberen een monoliet op te splitsen in meerdere domein-gebaseerde microservices. Veel van deze onderwerpen staan op zichzelf, en ik denk niet dat we genoeg recht hebben gedaan om ze in detail uit te leggen, maar we wilden een aantal van de kritische onderwerpen introduceren en onze ervaring met het toepassen ervan. Further Reading (link) sectie heeft een aantal referenties en een aantal nuttige inhoud voor iedereen die dit pad wil bewandelen.

Update: De volgende twee blogs in de serie zijn uit. Deze twee blogs bespreken de implementatie van de Cart microservice, met code voorbeelden, gebruikmakend van Domain-Driven Design principes, en Ports en Adapters design patterns. De primaire focus van deze blogs is om te laten zien hoe deze twee principes/patronen ons helpen modulaire applicaties te bouwen die agile, testbaar en refactorable zijn – kortom, in staat zijn om te reageren op de snelle omgeving waarin we allemaal opereren.

Implementeren van Cart Microservice met behulp van Domain Driven Design en Ports and Adapters Pattern – Deel 1

Implementeren van Cart Microservice met behulp van Domain Driven Design en Ports and Adapters Pattern – Deel 2

Verder lezen

1. Eric Evans’ Domain Driven Design

2. Vaughn Vernon’s Implementing Domain Driven Design

3. Martin Fowler’s artikel over Microservices

4. Sam Newman’s Building Microservices

5. Event storming

7. Backend voor Frontends

8. Onwaarheden van gedistribueerd computergebruik

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.