Patterns en practices in microservices

We zijn de afgelopen twintig jaar zó gewend geraakt aan softwareontwikkeling volgens het SOA-model, dat we de bijbehorende patterns en practices als vanzelfsprekend aannemen. Maar zijn die met de opkomst van microservices nog wel relevant, juist belangrijker, of soms zelfs een antipattern? In dit artikel bekijken we een paar interessante verschuivingen.

Ik werk al het grootste deel van mijn loopbaan als Java developer in de finance sector. Hier bouwen we altijd Java-applicaties volgens Service Oriented Architecture (SOA), het “laagjesmodel” of “lasagne architectuur”. Dit bevat grote EAR’s, een flinke database en met een beetje pech ook nog een ESB.

De opkomst van microservices

Time-to-market en business agility (het snel kunnen inspelen op veranderingen in de markt) zorgt ervoor dat we software op andere manieren gaan bouwen. Microservices is een architectuurstijl, die hierdoor snel aan populariteit wint. Het toepassen van microservices geeft je systeem de volgende eigenschappen:

  • Resilience: het kunnen omgaan met fouten. Binnen microservices is dit een natuurlijk gegeven, vanwege de complexere infrastructuur zal er altijd iets “stuk” zijn en het systeem dient hier mee om te kunnen gaan;
  • Schaalbaarheid: het vermogen om services onafhankelijk van elkaar te kunnen op- en afschalen.
  • Productiviteit: elk team kan zijn microservice onafhankelijk ontwikkelen van andere teams

 

Op het moment bevinden microservices zich in de piek van de hype cycle. Nu zijn microservices natuurlijk geen oplossing voor elk probleem, want niet iedereen heeft de schaalbaarheid nodig van bijvoorbeeld Netflix of Amazon. Daarnaast haal je ook een hoop complexiteit naar binnen en zonder de juiste voorwaarden (bijvoorbeeld een DevOps organisatie) moet je er niet eens aan beginnen.

Mocht je toch besluiten om in te zetten op een microservice-architectuur, vraag je dan af of die patterns en practices, die je jarenlang succesvol hebt ingezet, dan nog wel zo goed werken.

In dit artikel houden we een paar voorbeelden tegen het licht. Zitten ze in de lift (een superstip, om maar even in Top 40 terminologie te spreken) of zijn ze juist op zijn retour? We bespreken ze hier achtereen.

Stijging met superstip: Single Responsibility Principle

Het Single Responsibility Principle is één van de SOLID principles. Dit is bedacht door Michael Feathers aan de hand van 5 principes van Robert C. Martin (uncle Bob).

Bij microservices is dit principe belangrijker dan ooit. Een microservice “should do one thing, and do it well”, een adoptie uit de Unix filosofie. Bijvoorbeeld het realiseren van een uniek stukje business capability.

Bekijk afbeelding 1 als voorbeeld. Je systeem heeft een Customer Service en een Order Service. Het verdelen van verantwoordelijkheid over kleine, onafhankelijke services geeft je de volgende voordelen:

  • Kleine codebase, eenvoudiger te begrijpen;
  • Geen long-term technology stack commitment;
  • Snelle opstarttijd, ook goed voor development;
  • Development wordt makkelijker schaalbaar. Een team is verantwoordelijk voor één service en kan deze zelf ontwikkelen, schalen en deployen.

Heeft je service teveel verantwoordelijkheid, bijvoorbeeld een Customer-And-Order Service? Dan raak je bovenstaande voordelen kwijt. Dus maak up front een correct design van je microservices. Identificeer je business capablities goed en ontwerp je microservices zo dat elke microservice exact één business capability ontsluit.

Stijging met superstip, golden oldie: het Service Locator pattern

“Herinnert u zich deze nog?” klonk het vroeger op de radio. In oude JEE-versies moest je EJB’s ophalen middels lookup van home interfaces, een implementatie van het Service Locator pattern [3].

Omdat alle EJB’s in de regel mee gepacked worden in een EAR, zijn we overgegaan op local interfaces. Dit scheelde ook flink in de slechte performance.

In een microservice landschap is elke call tussen services remote. Daarmee is het Service Locator pattern terug van weggeweest! Geautomatiseerde service discovery is een essentieel onderdeel van een microservicelandschap.

Er zijn twee varianten:

  • Client-side discovery (zie afbeelding 2). De client zoekt zelf een instantie. Ribbon[1] in combinatie met een service registry als Eureka[2] is hier een voorbeeld van.
  • Service-side discovery (zie afbeelding 3). De client zet zijn request simpelweg door naar een router, die het request met behulp van een service registry dan naar een instantie van de gewenste microservice stuurt. Clustering oplossingen als Kubernetes en Marathon implementeren deze variant.

Daler in de charts: DRY (Don’t Repeat Yourself), het nieuwe antipattern?

Code duplicatie is slecht. Toch? Als Java EE developers zijn we gewend om code te schrijven met het oog op maximaal hergebruik. Dit resulteert in utility JARs, base classes, kortom alles om gedupliceerde code te vermijden.

Een gedeelde component creëert echter een afhankelijkheid tussen de afnemers van dat component. De coupling tussen verschillende stukken code in je applicatie stijgt hierdoor.

Heeft DRY hiermee afgedaan? Nee, DRY is beslist goed advies voor code binnen je eigen microservice. Maar microservices dienen wel volledig onafhankelijk van elkaar te zijn. De enige afhankelijkheid tussen microservices is het loosely coupled gebruik van elkaars (TCP) interfaces.

Probeer daarom het delen van code tussen microservices te vermijden. Echter, goed gejat is nog altijd beter dan slecht bedacht. Dit zal ertoe leiden dat je soms een stukje copy-paste programming moet toepassen (Do Repeat Others!).

Op dat moment heb je dus dezelfde code in twee services. Dat lijkt erg, tot je je bedenkt dat, hoewel de gedupliceerde code in eerste instantie wel hetzelfde lijkt, deze in twee verschillende contexten draait. De code in de ene service dient een ander einddoel dan in de andere service. Daarnaast hebben services verschillende lifecycles, waardoor de levensduren van stukken gedupliceerde code verschillen per service.

Voor data geldt: elke microservice beheert een klein stukje van het model. Om toch verbanden te kunnen leggen tussen services is het daarom soms nodig om ook wat data te dupliceren.

Bekijk afbeelding 4 voor een voorbeeld:

  • De Customer service als Order service werken (nu toevallig allebei) met een relationele database. Daarom heeft De Order Service wat persistency code gekopieerd van de Customer service (bijvoorbeeld omdat dit in eerste instantie prima werkte in de Customer service).
  • De Order service slaat een referentie op naar een customer, het customerId. Dit is dus gedupliceerde data. Echter hebben beide services geen toegang tot elkaars database, dus de koppeling is niet sterk. Alle communicatie verloopt via de API’s.

Daler in de charts: Complexe protocollen

Iedere Java EE developer heeft wel eens gewerkt met SOAP services, al of niet in combinatie met een ESB. Er ligt veel complexiteit in de route tussen client en service. Je kunt hier spreken van een smart pipe.

In een microservice-architectuur gaat een smart pipe ten koste van de loose coupling tussen services. Immers, hoe meer complexiteit er in het tussenliggende protocol zit, hoe meer indirecte afhankelijkheid er ligt tussen client en service.

Je kunt dit oplossen door het communicatieprotocol simpel te houden en meer intelligentie in de service zelf te leggen. Met andere woorden: smart endpoints, dumb pipes.

Smart endpoints

Bij microservices is het extra belangrijk dat API’s forward compatible zijn. Dat wil zeggen: denk bij het ontwerp van je interface aan eventuele toekomstige input en zorg ervoor dat je service deze input in ieder geval accepteert. Hiermee is Postel’s law “be conservative in what you do, be liberal in what you accept from others” [4] (uit 1980, notabene!) weer hartstikke actueel. Dit mag echter niet leiden tot het propageren van fouten in het landschap. Komt een service er echt niet meer uit, dan is fail fast and hard de beste oplossing.

Voor clients geldt: cherry picking van een service response. Haal er uit wat je interesseert en laat de overige data links liggen.

Dumb pipes

Gebruik een protocol met weinig overhead, zoals REST in combinatie met JSON. Of een low-latency protocol, zoals ProtoBuf, als readability niet belangrijk is. In het geval van messaging: gebruik een simple queuing infrastructuur, zoals ActiveMQ.

Daler in de charts: Enterprise domain model (yes!) model en ACID (ai!)

In Java EE applicaties is het domeinmodel koning. Meestal afgeleid van een generiek gegevens model (of GigaGegevensModel) resulteert dit in een flinke database. Integriteit wordt bewaakt met database constraints (foreign keys, uniqueness, etcetera). Het onderhoud van zo’n model wordt vanwege de sterke koppeling tussen entiteiten op termijn zeer kostbaar.

Binnen de wereld van microservices past geen allesomvattend domeinmodel. Elke service is autonoom, ontsluit een klein stukje business capability (zie Single Responsibility Principle) en besluit zelf hoe dataopslag geïmplementeerd wordt, of dit nu met een relationele database is, een NoSQL oplossing of een ander alternatief.

ACID (Atomicity, Consistency, Isolation, Durability) is lastig te implementeren in een gedistribueerd systeem. Belangrijker is eventual consistency: de garantie dat je data “uiteindelijk” weer consistent is over je gehele systeem.

 

Een oplossingsrichting voor eventual consistency is een event driven architecture, waarbij services elkaar op de hoogte houden van updates middels events. Een voorbeeld (zie afbeelding 5):

  1. Een Order Service creëert een Order in een pending state, publiceert een orderCreated event.
  2. De Customer Service ontvangt het event en probeert geld te reserveren voor de Order. Er wordt vervolgens een creditReceived event (of een creditLimitExceeded event) gepubliceerd.
  3. De Order Service ontvangt het event en verandert de status van de Order naar approved (middels een commit) of cancelled.

Conclusie

Met de opkomst van microservices zien we een verschuiving in importantie van patterns en practices. Sommigen worden belangrijker, anderen juist minder belangrijk en voor andere eigenschappen van een applicatie (zoals consistency) hebben we goede alternatieven nodig. Feit is dat eens temeer blijkt dat een “Golden Hammer” niet bestaat en dat onze patterns en practices mee moeten bewegen met nieuwe trends!