Wat is event-driven architecture?

Event-driven architecture is een manier van software ontwerpen waarbij systemen reageren op gebeurtenissen die binnen het domein plaatsvinden.

In plaats van dat services elkaar continu direct aanroepen, publiceert een service een event zodra er iets relevants is gebeurd. Andere onderdelen van het systeem kunnen daar vervolgens zelfstandig op reageren.

Denk aan een bestelling die is aangemaakt, een betaling die is verwerkt of data die opnieuw moet worden opgehaald. Het event beschrijft wat er is gebeurd. De producer hoeft daarbij niet te weten welke systemen daar uiteindelijk iets mee gaan doen.

Voor mij zit de kracht vooral in die ontkoppeling. Services krijgen hun eigen verantwoordelijkheid en kunnen onafhankelijk van elkaar evolueren.

Belangrijke dingen om te weten

Event-driven betekent niet automatisch dat een architectuur beter of schaalbaarder wordt. Je verplaatst een deel van de complexiteit.

Waar je bij synchrone communicatie direct ziet of iets slaagt, moet je bij events nadenken over onderwerpen als:

  • Eventual Consistency

  • Idempotency

  • Retries en foutafhandeling

  • Event contracts

  • Event ordering

  • Observability

  • Duplicate events

  • Distributed transactions

  • Ownership van data

Juist daarom probeer ik events zo eenvoudig mogelijk te houden.

Een event moet duidelijk maken wat er binnen een domein gebeurd is. Niet welke actie een andere service vervolgens moet uitvoeren.

CarInformationUpdated vind ik bijvoorbeeld sterker dan UpdateSearchIndex.

De eerste beschrijft een feit. De tweede schrijft een ander systeem voor wat het moet doen.

Dat verschil lijkt klein, maar bepaalt uiteindelijk hoeveel vrijheid services hebben om zelfstandig te veranderen.

Hoe ik event-driven architecture gebruik

Ik gebruik event-driven architecture voornamelijk wanneer processen meerdere verantwoordelijkheden bevatten die niet allemaal binnen dezelfde transactie afgehandeld hoeven te worden.

Daarbij kijk ik eerst naar het domein en pas daarna naar technologie.

Niet ieder proces hoeft namelijk een event te zijn.

Wanneer directe consistentie belangrijk is, kan een synchrone oplossing juist eenvoudiger en beter zijn. Wanneer onderdelen onafhankelijk mogen verwerken en eventual consistency acceptabel is, wordt een event-driven aanpak interessanter.

Ik werk daarbij onder andere met Kafka en Redis Streams.

Voor processen die meerdere services raken, heb ik ook het Saga Pattern toegepast om distributed processen beheersbaar te houden. Daarbij accepteer je dat het volledige systeem niet op ieder milliseconde hetzelfde beeld van de werkelijkheid heeft, maar zorg je er wel voor dat het uiteindelijk naar een consistente toestand beweegt.

Waar heb ik het gebruikt?

Mijn meest recente toepassing van event-driven architecture is binnen het Car Platform van ANWB.

Daar kwam ik terecht in een backend waarin veel externe databronnen, verschillende processen en klantgerichte mobiliteitsdiensten samenkwamen.

Ik heb daar een omslag gemaakt van een grotendeels monolithische verwerking naar een duidelijker event-driven model. Onder andere door de applicatie- en organizer-verantwoordelijkheden van elkaar te scheiden en langlopende processen asynchroon te verwerken.

Een belangrijk onderdeel daarvan was een asynchrone task scheduler voor processen rondom externe databronnen. Met Kotlin Coroutines en Redis konden deze processen onafhankelijk uitgevoerd en opnieuw opgepakt worden wanneer iets misging.

Daarnaast heb ik het bestaande queue-mechanisme vervangen door Redis Streams. Daarmee kregen we betrouwbaardere berichtafhandeling, betere throughput en meer inzicht in wat er daadwerkelijk binnen het systeem gebeurde.

Eerder heb ik event-driven architecturen gebouwd met Kafka. Zo heb ik binnen een SaaS-platform meerdere microservices via events met elkaar laten communiceren en het Saga Pattern gebruikt om eventual consistency tussen deze services te bewaken. Tijdens mijn werk bij Sogeti heb ik daarnaast specifiek onderzoek gedaan naar Immediate versus Eventual Consistency binnen Domain Driven Design en hiervoor zowel synchrone als Kafka-gebaseerde prototypes ontwikkeld.

Mijn ervaring met event-driven architecture

Mijn recente ervaring heeft vooral bevestigd dat event-driven architecture voor mij niet draait om zoveel mogelijk events produceren.

Het draait om verantwoordelijkheden goed neerzetten.

Een eventbus lost een slecht domeinmodel niet op. Sterker nog, zodra je processen distribueert wordt onduidelijk eigenaarschap alleen maar zichtbaarder.

Daarom kijk ik eerst naar de grenzen van een service, de eigenaar van de data en welke consistentie daadwerkelijk nodig is.

Bij ANWB betekende dat bijvoorbeeld niet alleen nieuwe events introduceren, maar vooral bestaande verantwoordelijkheden uit elkaar trekken en de verwerking zo opzetten dat onderdelen zelfstandig konden falen en herstellen.

Dat is voor mij een goede event-driven architectuur: eenvoudig te begrijpen, duidelijk gestructureerd en gebouwd om mee te kunnen veranderen.

Veelgestelde vragen die ik krijg over event-driven architecture

Wanneer kies je voor event-driven en wanneer voor REST?

Als het antwoord direct nodig is, kies ik meestal voor synchrone communicatie. Wanneer processen onafhankelijk mogen verlopen en eventual consistency acceptabel is, wordt event-driven interessanter. Ik kies dus vanuit het proces, niet vanuit de technologie.

Kafka of Redis Streams?

Dat hangt volledig af van het probleem. Kafka is sterk als centrale event backbone met hoge schaalbaarheid en uitgebreide event processing. Redis Streams kan juist heel krachtig zijn wanneer Redis al onderdeel van het platform is en je een eenvoudiger messaging-model nodig hebt. Bij ANWB heb ik recent bewust voor Redis Streams gewerkt.

Hoe ga je om met eventual consistency?

Door vooraf duidelijk te bepalen welke tijdelijke inconsistentie acceptabel is. Daarna ontwerp ik onder andere idempotency, retries, compensating actions en waar nodig Saga's rondom dat uitgangspunt. Eventual consistency moet een bewuste architecturale keuze zijn, geen bijwerking.

Maakt event-driven architecture een systeem niet onnodig complex?

Dat kan absoluut. Daarom gebruik ik het alleen waar de ontkoppeling daadwerkelijk iets oplevert. Een event toevoegen omdat het technisch interessant is, maakt een systeem vaak slechter. Bewust eenvoudig blijven is ook weten wanneer je géén event nodig hebt.

Wat vind je het belangrijkste bij een event-driven systeem?

Dat je begrijpt wie eigenaar is van welke informatie. Technologie komt daarna. Zodra ownership onduidelijk is, krijg je events waarvan niemand precies weet wie ze mag produceren, interpreteren of corrigeren. Daar begint veel distributed complexity.