Evolueren naar microservices – met Axon Framework

Axon is een volwassen Java framework, dat is gebaseerd op Domain-Driven Design (DDD), Command Query Responsibility Segregation (CQRS) en Event Sourcing (ES). Het is inmiddels zo’n 600.000 keer gedownload en wordt wereldwijd gebruikt, onder andere in de financiële sector. Recent is er veel belangstelling voor Axon in verband met microservices. De DDD/CQRS/ES-concepten zijn weliswaar ouder dan microservices, maar passen daar heel goed bij. En Axon daarom ook.

Frans van Buul werkt als evangelist bij AxonIQ sinds de oprichting in 2017. Daarvoor heeft hij gewerkt als Java ontwikkelaar en presales consultant

In dit artikel bespreken we eerst (kort) de kernconcepten, daarna kijken we naar de structuur van een Axon applicatie aan de hand van een voorbeeld en tenslotte bespreken we hoe microservices hierin passen.

 

DDD

DDD is een brede benadering van softwareontwikkeling, met name bedoeld om applicaties met complexe business logica goed te kunnen ontwikkelen en onderhouden. DDD kent zowel strategische aspecten als hele concrete “building blocks”. Building blocks, die je ook in Axon tegenkomt, zijn:

  • Aggregate: één of meer entiteiten, die vanuit een oogpunt van persistentie en consistentie als een eenheid worden beschouwd. Verschillende aggregates kunnen hooguit indirect naar elkaar verwijzen. In databasetaal: er kunnen foreign key constraints zijn tussen de entiteiten binnen een aggregate, maar niet tussen entiteiten in verschillende aggregates.
  • Repository: een interface waarmee aggregates kunnen worden gepersisteerd en teruggelezen, onafhankelijk van de onderliggende databasetechnologie. Die komt pas in de repository implementatie om de hoek kijken.
  • Domain Event: een representatie van iets dat gebeurd is in het domein en waar andere componenten op kunnen reageren. Events zijn immutable (want we kunnen het verleden niet veranderen). In DDD zijn de Domain Events niet puur technisch, maar herkenbaar voor de business owners van de applicatie. Een voorbeeld zou kunnen zijn: “KlantHeeftOrderBevestigd”.

Een belangrijk uitgangspunt van DDD dat ook door Axon wordt gestimuleerd, is om business logica in het domein model en in het bijzonder in de aggregates zelf te plaatsen (en dus niet in een “service layer”, die is gescheiden van het datamodel).

 

CQRS en ES

Het kernidee van CQRS is dat we een onderscheid maken tussen commando’s (wijzigen van state) en queries (opvragen van state zonder deze te wijzigen). Bij CQRS vinden die twee zaken plaats op aparte databases. Dat levert extra complexiteit op, omdat de read-side consistent moet worden gehouden met de command-side. Daar staat tegenover dat de read-side exact kan worden afgestemd op de functionaliteit van de applicaties (zowel qua technologiekeuze als schema). Queries kunnen daarom erg simpel blijven en dus ook erg snel worden uitgevoerd. In veel gevallen is het acceptabel als de read-side asynchroon wordt bijgewerkt ten opzichte van de write-side (eventual consistency). Daardoor blijft ook command processing simpel en snel.

ES is op het eerste gezicht een ongerelateerd concept. ES gaat over persistentie van entiteiten. Met een traditionele CRUD-aanpak (bijvoorbeeld met JPA) wordt actuele state van entiteiten gepersisteerd en die wordt telkens bijgewerkt. Bij ES slaan we de geschiedenis van entiteiten op als een serie van events (append-only) en wordt de actuele state afgeleid van de events. De motivatie voor ES zit in de waarde van historische informatie. Denk aan auditing/compliance, machine learning, analytics, etcetera.

CQRS en ES worden vaak in één adem genoemd. CQRS is prima mogelijk zonder ES, maar omgekeerd niet. Een ES-systeem kan vanuit de opgeslagen events efficiënt individuele entiteiten teruglezen, maar niet efficiënt willekeurig queries uitvoeren op de gehele dataset. Daarom wordt ES in de praktijk vrijwel altijd gecombineerd met CQRS.

 

Praktisch voorbeeld

We bespreken hieronder enkele kernpunten van een Axon voorbeeld dat in z’n geheel kan worden gedownload op GitHub (https://github.com/AxonIQ/giftcard-demo-series). We beperken ons tot de command-side.

Het voorbeeld maakt gebruikt van de Axon Spring Boot starter. Axon kan ook zonder Spring (Boot) gebruikt worden, maar Spring Boot maakt het erg gemakkelijk. Alle benodigde infrastructuur wordt dan geconfigureerd met redelijke defaults, zonder dat we iets hoeven te doen.

Het domein van de applicatie is het uitgeven en weer inwisselen van cadeaukaarten (giftcards), die een bepaalde waarde vertegenwoordigen. Dit is een geschikt simpel domein om CQRS/ES te illustreren. Er zijn slechts twee events: er kan een nieuwe cadeaukaart worden uitgegeven (issuing) en een cadeaukaart kan worden gebruikt voor een betaling (redeeming).

Het startpunt bij CQRS/ES zijn commando’s en events, die in de applicatie worden gerepresenteerd als immutable objects. Die kun je in standaard Java coderen, maar dat leidt tot veel boilerplate. Meestal wordt daar een andere oplossing voor gekozen, zoals Lombok @Value, Kotlin data classes of Scala case classes. In Listing 1 zien we een voorbeeld met Kotlin.

 

Het enige Axon-specifieke aan de listing is de @TargetAggregateIdentifier. Dit commando is bedoeld voor een reeds bestaand aggregate. De annotatie vertelt aan Axon dat dit veld de identifier bevat van het aggregate waar het commando voor bedoeld is.

De commando’s en events moeten worden verwerkt door het GiftCard aggregate. Deze is weergegeven in Listing 2.  De class is geannoteerd met @Aggregate (1). Hierdoor zal Axon de class automatisch vinden, een repository instantiëren en scannen voor command en event handlers.

 

Voor beide commando’s uit Listing 1 hebben we een @CommandHandler (4). Voor het aanmaken van nieuwe cards is dat een constructor, voor het gebruiken van een bestaande card is dat een reguliere methode. In de command handler methods wordt besloten of een commando door kan gaan. In dit geval moeten de bedragen positief zijn en mag er niet meer worden uitgegeven dan nog op de kaart staat. In de command handlers wordt echter geen state gewijzigd. Dat gaat via events.  De command handlers eindigen dus met het aanroepen van Axon’s apply method, waarmee een event eerst wordt toegepast op het aggregate en daarna wordt gepubliceerd voor externe listeners.

Uiteraard moeten we ook nog events afhandelen. Daarvoor hebben we twee @EventSourcingHandler methods (5). Het is belangrijk om te beseffen dat bij ES deze methods niet alleen maar worden aangeroepen direct nadat een commando een event publiceert, maar ook iedere keer dat dit aggregate opnieuw wordt teruggelezen. De handlers moeten dus geen side effects hebben (die zouden veel te vaak worden uitgevoerd) en geen beslissingen nemen (dat kan tot inconsistente resultaten leiden). De enige gebeurtenis is het veranderen van de eigen state. Bij het lezen van een bestaand aggregate moeten events worden toegepast op een nieuw, leeg object zonder de commando-constructor aan te roepen. Daarom hebben we ook de lege constructor nodig (3).

De state die door de class wordt bijgehouden (2) bestaat uit de identifier (geannoteerd met @AggregateIdentifier) en er is data nodig om commando’s te valideren. Functioneel willen we in de voorbeeldapplicatie ook de oorspronkelijke waarde van de kaart laten zien en niet alleen de resterende waarde. Echter, omdat die oorspronkelijke waarde niet wordt gebruikt om commando’s te valideren, hoeft die ook niet te worden opgeslagen in het aggregate. De gebruiker krijgt uiteindelijk inzicht in die oorspronkelijke waarde via de read-side.

 

Integratie

Op basis van de code, die we hierboven hebben besproken, kan Axon commando’s gaan verwerken. Zaken als het laden van de aggregate vanuit de repository, het aanroepen van de command handler, opslag van events en transactie management worden allemaal door het framework afgehandeld.

Voor Axon maakt het niet uit welk (web) framework je gebruikt om je applicatie te schrijven. Het integratiepunt is Axon’s CommandGateway. In Listing 3 laten we zien hoe een stukje code (gebaseerd op Vaadin) commando’s kan laten uitvoeren.

 

 

Evolutionaire microservices

Wat is nu het verband tussen DDD/CQRS/ES, Axon en microservices? Dat zit in een aantal dingen. Bij een microservices systeem ontstaat van nature de vraag welke services er moeten zijn. DDD biedt daar natuurlijke antwoorden op. Het strategische DDD-concept van de bounded context kan worden gebruikt om (groepen van) microservices af te bakenen. Nog fijnmaziger is het prima mogelijk om individuele aggregates als microservice ter beschikbaar te stellen, omdat aggregates per definitie een consistentiegrens afbakenen en dus goed onafhankelijk kunnen functioneren.

CQRS/ES voegt daar nog het concept aan toe dat alle informatie wordt uitgewisseld met commands, events en queries. In principe maakt het niet uit of die berichten binnen hetzelfde proces worden afgehandeld of door een ander proces.

Axon Framework helpt om de stap van afhandeling binnen het proces (monoliet) naar afhandeling in een ander proces (microservices systeem) te maken. Alle berichtuitwisseling vindt logisch plaats via een bus: de CommandBus, EventBus en QueryBus (Java interfaces in Axon). Standaard worden er in-process implementaties van deze interfaces geïnstantieerd. Zodra we het systeem willen opschalen, kunnen deze gemakkelijk worden vervangen door gedistribueerde implementaties, zonder de business logica te wijzigen. Zo is het mogelijk te beginnen met een monoliet en te evolueren naar een microservices systeem, zodra dat nodig is.

Meer informatie

Uiteraard hebben we hier alleen de basis van Axon kunnen behandelen. Mocht je interesse gewekt zijn, dan kun je veel meer informatie vinden op https://axoniq.io, zoals info over trainingen, conferenties en case studies van gebruikers.