3.12 Hændelsesdrevet arkitektur og beskedudveksling
Overblik og motivation
Hændelsesdrevet arkitektur (EDA) er en stil, hvor komponenter kommunikerer ved at producere og reagere på hændelser frem for ved at kalde hinanden direkte. En hændelse er et faktum: noget, der allerede er sket, såsom “OrdreAfgivet” eller “BetalingGennemført.” En producent annoncerer faktummet og går videre, og et vilkårligt antal forbrugere reagerer efter deres eget tidsplan uden at producenten ved, hvem der lytter. Det er en anden holdning end anmodning-og-svar-kaldene i kapitel 2.3, hvor en kalder beder en bestemt tjeneste om at gøre noget og venter på svaret.
For en stor organisation er tiltrækningen afkobling i stor skala. Når I har dusinvis af teams og hundredvis af tjenester, producerer det at koble alt sammen med direkte punkt-til-punkt-kald et skrøbeligt net, hvor ét teams ændring bryder et andets, og ingen kan spore hvorfor. Hændelser lader teams integrere gennem en fælles strøm af fakta i stedet for gennem hinandens indre, og en ny forbruger slutter sig til ved at abonnere, uden at producenten ændrer en linje kode. Den egenskab, mere end rå gennemløb, er grunden til, at hændelsesdrevne tilgange bliver ved med at brede sig gennem virksomheder, der erstatter sammenfiltrede integrationer, og offentlige organer, der forbinder myndigheder, der hver ejer deres egne systemer.
Det offentlige får en anden fordel, der er let at undervurdere: en holdbar, ordnet optegnelse over, hvad der skete, er et revisions- og åbenhedsaktiv. Når en borger spørger, hvorfor en ydelsesafgørelse faldt, som den gjorde, svarer en uforanderlig log over de hændelser, der ledte til den, direkte. Men hændelsesdrevet design er ikke gratis og ikke altid rigtigt: asynkrone flows er sværere at spore, sværere at ræsonnere om og lette at overanvende. Dette kapitel er holdningspræget om, hvornår afkoblingen og skalaen betaler for den ekstra kompleksitet, og hvornår et almindeligt synkront kald ville have tjent jer bedre. Det bygger på realiteterne ved distribuerede systemer i kapitel 3.3, så læs det først, hvis I ikke har.
Grundprincipper
- Hændelser er fakta, ikke instruktioner. En hændelse siger, hvad der skete; en kommando beder om, at noget sker. Hold dem adskilt, og navngiv hændelser i datid.
- Afkobling er pointen. Producenter bør ikke vide eller bekymre sig om, hvem der forbruger deres hændelser. Hvis de gør, har I kobling klædt ud som beskedudveksling.
- Design til mindst-én-gang-levering. Præcis-én-gang-levering er en myte. Gør hver forbruger idempotent, så dubletter er harmløse.
- Rækkefølge er en garanti, du betaler for. Du får rækkefølge inden for en partition, ikke på tværs af et emne. Vælg partitionsnøgler bevidst.
- Skemaet er kontrakten. En hændelses form er en offentlig grænseflade; udvikl den med den omhu, du ville bruge på en offentliggjort API.
- Asynkron betyder ikke uobserverbar. Hvis du ikke kan følge en besked fra ende til anden, kan du ikke drive systemet.
- Kompleksitet skal fortjenes. Event sourcing, CQRS og sagaer er kraftfulde og dyre; ræk ud efter dem, når problemet kræver det, ikke som standard.
Anbefalinger
Skeln mellem hændelser, kommandoer og beskeder, før du bygger noget
Disse tre ord bruges i flæng, og forvirringen forårsager reelle designfejl. En kommando er en anmodning om at gøre noget (“GennemførBetaling”), rettet mod én håndterer, og den kan afvises. En hændelse er en meddelelse om, at noget allerede er sket (“BetalingGennemført”), udsendt til enhver interesseret, og den kan ikke afvises, fordi faktummet allerede er sandt. En besked er den neutrale konvolut, der bærer begge over ledningen. Distinktionen former koblingen: kommandoer kobler afsenderen til en bestemt modtager og et bestemt udfald, mens hændelser afgiver kontrol over, hvad der sker næste. Navngiv dine hændelser i datid, og når du fanger dig selv i at udgive en “hændelse”, der reelt betyder “gå og gør denne specifikke ting,” har du skrevet en kommando i forklædning.
Vælg køer, logge og publish/subscribe med vilje
Ikke al beskedudveksling har samme form, og at vælge den forkerte er en almindelig tidlig fejl. En beskedkø leverer hver besked til én forbruger og fjerner den typisk, når den er behandlet, hvilket passer til arbejdsfordeling: mange workers trækker opgaver, hver udført én gang. En holdbar hændelseslog (en strøm) holder hændelser i rækkefølge og lader mange uafhængige forbrugere læse i deres eget tempo og afspille historik fra et vilkårligt punkt, hvilket passer til hændelsesdistribution og revision. Publish/subscribe har producenter, der udgiver til et emne, og flere abonnenter får hver deres kopi. Den praktiske regel: hvis beskeden er en opgave, én worker skal fuldføre, så ræk ud efter en kø; hvis det er et faktum, mange parter kan interessere sig for nu eller senere, så ræk ud efter en holdbar log, som også giver dig afspilning til genopretning og onboarding af nye forbrugere. Se kapitel 3.4 for, hvordan disse valg interagerer med din datalagringsstrategi.
Foretræk koreografi til autonomi, orkestrering til kontrol
Når en forretningsproces spænder over flere tjenester, koordinerer du den på en af to måder. I koreografi reagerer hver tjeneste på hændelser og udsender sine egne, uden en central hjerne: maksimalt afkoblet og godt for teamautonomi, men den samlede proces eksisterer kun som emergent adfærd, ingen enkelt placering beskriver. I orkestrering driver en central koordinator trinene og kender hele flowet: lettere at overvåge og ændre, på bekostning af en komponent, hvert trin afhænger af. En god standard er koreografi til løst beslægtede reaktioner (“når en ordre afsendes, tildeler loyalitetstjenesten point”) og orkestrering til en defineret transaktion med en klar succesbetingelse og et behov for at rapportere status. Lad ikke en vigtig proces kun leve som stammeviden spredt over ti hændelseshåndterere.
Ræk kun ud efter event sourcing og CQRS, når de tjener deres brød
Event sourcing gemmer tilstand som en kun-tilføjelses-sekvens af hændelser frem for et nuværende øjebliksbillede, du overskriver, og du genopbygger nuværende tilstand ved at afspille dem. Fordelen er et perfekt revisionsspor, evnen til at rekonstruere enhver tidligere tilstand og tidsmæssige forespørgsler; omkostningen er, at du versionerer hændelsesskemaer for evigt, håndterer afspilning og øjebliksbilleder og bærer en mental model, de fleste udviklere aldrig har brugt. CQRS (Command Query Responsibility Segregation) adskiller skrivemodellen fra en eller flere læsemodeller, så læsning og skrivning skalerer og udvikler sig uafhængigt; det parrer sig naturligt med event sourcing, men kræver det ikke. Begge udmærker sig i domæner med ægte revisions-, compliance- eller komplekse forespørgselsbehov, hvilket er grunden til, at reguleret finans og det offentlige finder dem umagen værd. For en simpel create-read-update-delete-tjeneste er de utilsigtet kompleksitet, I vil fortryde, så anvend dem på den skive af jeres domæne, der har brug for dem, ikke hele systemet ved refleks.
Styr distribuerede transaktioner med sagaer, ikke totrinscommit
I kan som regel ikke vikle én atomisk transaktion rundt om flere tjenester og databaser. Distribueret totrinscommit holder låse på tværs af netværket, skærer tilgængelighed og skalerer dårligt, så det passer sjældent til et hændelsesdrevet system. Saga-mønstret erstatter det: modellér transaktionen som en sekvens af lokale transaktioner, der hver udsender en hændelse, som udløser den næste, og giv hvert trin en kompenserende handling, der fortryder det, hvis et senere trin fejler. Hvis “reservér lager” lykkes, men “debitér kort” fejler, frigiver en kompensation lageret. Sagaer kan koreograferes eller orkestreres, og orkestrering vinder som regel for alt, I skal overvåge. Fordi sagaer omfavner eventual consistency, passerer systemet gennem mellemtilstande (“reserveret, men ikke betalt”), før det konvergerer, så design brugeroplevelsen og revisionssporet til at vise “i gang”- og “kompenseret”-tilstande ærligt. Kapitel 3.3 dækker det samme terræn fra synsvinklen distribuerede systemer.
Design til mindst-én-gang-levering, og gør forbrugere idempotente
Beskedsystemer kan ikke reelt levere præcis én gang på tværs af fejl, fordi den kvittering, der siger “jeg behandlede dette,” selv kan gå tabt og tvinge en gensendelse. Det, I kan opnå, er mindst-én-gang-levering med idempotent behandling, som producerer præcis-én-gang-effekter. Idempotens betyder, at behandling af den samme hændelse to gange efterlader det samme resultat som at behandle den én gang; kom dertil med en idempotensnøgle på hver hændelse og en optegnelse over, hvad I allerede har håndteret, så en dublet genkendes og droppes. Højst-én-gang-levering (fire-and-forget, ingen gensendelse) er simplere, men taber stille beskeder, så forbehold den til data, I har råd til at miste. Behandl det “præcis-én-gang”-mærke, nogle leverandører reklamerer med, med mistænksomhed: det betyder som regel præcis-én-gang inden for ét systems grænse under bestemte betingelser, ikke den garanti fra ende til anden, vendingen antyder.
Styr rækkefølge med partitioner, og kend dine forbrugergrupper
Rækkefølge er ikke global og gratis; den er lokal og betalt. En strøm er delt i partitioner, og du får rækkefølge inden for en partition, ikke på tværs af emnet. Hændelser dirigeres til en partition efter en partitionsnøgle, så at vælge den nøgle er, hvordan du kontrollerer, hvad der forbliver ordnet: nøgl efter kunde-ID, og én kundes hændelser forbliver i orden i forhold til hinanden, mens forskellige kunder behandles parallelt. Forbrugergrupper lader et sæt workers dele et emnes partitioner, hver partition håndteret af én worker, hvilket er, hvordan I skalerer gennemløb og samtidig bevarer rækkefølge pr. partition; det er her, skalerbarhed møder korrekthed, i forbindelse med kapitel 3.5. Vælg en partitionsnøgle, der afspejler jeres reelle rækkefølgekrav og fordeler belastningen jævnt, fordi en nøgle, der tragter det meste trafik ind i én partition, skaber et hotspot, ingen mængde workers kan lindre.
Behandl skemaer som kontrakter med et register og udviklingsregler
En hændelses struktur er en offentliggjort grænseflade, forbrugt af teams, I måske aldrig møder, så at ændre den skødesløst bryder dem på afstand. Læg jeres hændelsesskemaer i et skemaregister, et fælles katalog, der gemmer hvert skema og håndhæver kompatibilitetsregler, når en producent forsøger at ændre et. Adoptér en eksplicit politik: bagudkompatible ændringer (tilføjelse af et valgfrit felt) er tilladt; brydende ændringer (fjernelse af et felt, ændring af en type, omdøbning) kræver en ny skemaversion og en migreringsplan. Det lader producenter udvikle sig uden en synkroniseret udrulning på tværs af hver forbruger, hvilket er hele grunden til, at I valgte hændelser. Den samme grænsefladeversioneringsdisciplin fra kapitel 2.3 gælder, fordi et hændelsesskema er en API under et andet navn.
Garantér levering med den transaktionelle outbox, og håndtér fejl eksplicit
En klassisk fejl: jeres tjeneste skriver til sin database og udgiver derefter en hændelse, og den crasher mellem de to, så databasen ændrede sig, men hændelsen aldrig gik ud. Den transaktionelle outbox retter dette ved at skrive hændelsen ind i en outbox-tabel i den samme databasetransaktion som tilstandsændringen, så de gennemføres eller fejler sammen; et separat relæ læser så outboxen og udgiver til mægleren, ofte med change data capture til at følge databaseloggen. Ved forbrugsfejl holder en dead letter-kø beskeder, der gentagne gange fejler, så en giftbesked (en der aldrig vil lykkes, måske fordi den er misdannet) ikke blokerer køen bag sig for evigt. Tilføj modtryk, så en hurtig producent ikke kan overvælde en langsom forbruger: begræns jeres køer, og sæt tempoet ned eller afgiv belastning, når de fyldes, frem for at udtømme hukommelsen. Disse fire mekanismer adskiller en demo fra et system, I kan drive kl. 3 om natten.
Gør asynkrone flows observerbare fra ende til anden
Den sværeste omkostning ved at gå hændelsesdrevet er, at en enkelt forretningshandling nu spredes på tværs af producenter, mæglere og forbrugere uden nogen kaldestak, der binder dem sammen. Propagér et korrelations-ID gennem hver hændelse, så I kan følge ét logisk flow på tværs af hvert spring, den samme disciplin som kapitel 3.3 foreskriver for synkrone kald. Følg forbrugerforsinkelse (hvor langt bagud realtid hver forbruger læser) som en førsteklasses måling, fordi stigende forsinkelse er jeres tidligste advarsel om problemer, og overvåg dead letter-køens dybde, behandlingslatens og gensendelsesrater. Uden dette bliver en hændelse, der stille ikke forbruges, en usynlig fejl, der dukker op dage senere som manglende data.
Afvejninger: fordele og ulemper
| Tilgang | Fordele | Ulemper / omkostning |
|---|---|---|
| Synkron anmodning/svar | Enkel at ræsonnere om, umiddelbart resultat, let sporing | Stram tidsmæssig kobling, kaskadefejl, begrænset skala |
| Hændelsesdrevet (pub/sub over en log) | Afkobling, uafhængig skalering, afspilning, revisionsspor | Eventual consistency, sværere sporing, flere bevægelige dele |
| Beskedkø (arbejdsfordeling) | Belastningsudjævning, buffering, modtryksvenlig | Én forbruger pr. besked, mindre egnet til udsendelse |
| Event sourcing + CQRS | Fuld historik, tidsmæssige forespørgsler, læsning/skrivning skalerer uafhængigt | Skemaversionering for evigt, afspilningskompleksitet, stejl indlæringskurve |
| Saga (vs totrinscommit) | Skalerbar, tilgængelig, ingen distribuerede låse | Eventual consistency, kompensationslogik, sværere at ræsonnere om |
Den centrale spænding er mellem afkobling og forståelighed. Hver hændelse, I tilføjer, løsner koblingen mellem producent og forbruger og køber teamautonomi og uafhængig skalering, og samtidig fjerner den en linje af den historie, et synkront kald ville have fortalt jer klart: flowet bliver emergent og bor i interaktionerne frem for i nogen enkelt fil. Løs det ved at være selektiv: brug hændelser, hvor afkobling reelt betaler sig, såsom integration på tværs af teamgrænser, udsendelse til mange forbrugere, buffering af belastningsspidser og revision. Behold synkrone kald, hvor I har brug for et umiddelbart svar og en simpel mental model, såsom at læse data til at gengive en side. Fejlformen at undgå er at forvandle hvert internt funktionskald til en hændelse og kalde det arkitektur, den samme arkitektoniske dømmekraft, kapitel 3.2 beder om for hvert mønster, I adopterer.
Spørgsmål til drøftelse med dit team
Har vi for denne specifikke interaktion faktisk brug for en hændelse, eller ville et synkront kald være klarere og sikrere? At springe dette spørgsmål over er, hvordan et system akkumulerer tilfældig kompleksitet. Den ærlige test er, om producenten har brug for et resultat tilbage lige nu (et kald) eller annoncerer et faktum, andre kan reagere på i deres eget tempo (en hændelse). Tag den specifikke interaktion med, ikke en generel præference, og spørg, hvilken afkobling I får, og hvilken sporingsklarhed I opgiver. Hvis kalderen blokerer og venter på, at “hændelsen” behandles, har I bygget et langsomt, svært fejlsøgbart synkront kald og betalt ekstra for privilegiet. Standarden for interne interaktioner inden for samme team, hvor I har brug for svaret nu, bør være et direkte kald; forbehold hændelser til, hvor den løse kobling tjener sin omkostning.
Hvad sker der, når en forbruger modtager den samme hændelse to gange, og har vi faktisk testet det? Mindst-én-gang-levering garanterer, at dubletter vil ske, så hver forbruger skal være idempotent, men alligevel er idempotens let at påstå og let at få forkert. Gå en reel forbruger igennem og spor præcis, hvordan en anden levering genkendes og neutraliseres, hvad enten det er med en idempotensnøgle, en log over behandlede hændelser eller en naturligt idempotent operation. Tag resultaterne af en faktisk test med, hvor I gensender en batch og bekræfter ingen dobbelte debiteringer, duplikerede poster eller gentagne notifikationer. Vær særligt opmærksomme på bivirkninger, der forlader jeres database, såsom e-mails, betalinger og tredjepartskald, fordi det er der, ikke-idempotente fejl skader kunder direkte. Hvis jeres team ikke kan pege på en test, der beviser dublet-sikkerhed, så antag, at I ikke er dublet-sikre.
Når et hændelsesflow går i stykker i produktion, hvor lang tid går der, før vi opdager det, og kan vi spore én besked fra ende til anden? Asynkrone fejl er stille, så en forbruger, der stille holder op med at behandle, kan gå ubemærket hen, til manglende data bliver en kundeklage eller et revisionshul. Spørg, hvad jeres tidligste signal er, og om I overvåger forbrugerforsinkelse og dead letter-køens dybde som alarmerende målinger frem for dashboards, ingen ser. Tag en reel hændelse eller en game day-øvelse med og tag tid på, hvor lang tid det tager at følge et enkelt korrelations-ID på tværs af producent, mægler og hver forbruger. Hvis svaret er “vi søger i flere tjenesters logs og gætter,” er jeres observerbarhed ikke klar til den kompleksitet, I har påtaget jer. I regulerede sektorer er det at kunne rekonstruere præcis, hvordan en besked bevægede sig, ofte et compliance-krav, ikke en finesse.
Hvordan vil vi udvikle et hændelsesskema, som mange teams allerede forbruger, uden at bryde nogen af dem? En hændelses form er en offentlig kontrakt, og når dusinvis af forbrugere afhænger af den, kan en uskyldigt udseende ændring bryde systemer, I aldrig har hørt om, på afstand, uden nogen compiler til at advare. Spændingen er reel: producenter vil bevæge sig hurtigt og rydde op i deres hændelser, mens hver forbruger vil have formen frosset for evigt, så aftal på forhånd, hvilke ændringer der er sikre (tilføjelse af et valgfrit felt), og hvilke der kræver en ny version og et migreringsvindue (fjernelse af et felt, ændring af en type, omdøbning). Tag den faktiske liste over forbrugere pr. emne med, om et skemaregister håndhæver kompatibilitetsregler i dag, eller om former ændres ved uformel aftale, og hvor længe to versioner kan køre parallelt under en migrering. I en stor virksomhed eller en offentlig datadeling på tværs af myndigheder kan et stille ødelagt skema korrumpere poster i systemer, I ikke ejer, og dukke op senere som et revisionssvigt, så behandl kompatibilitetshåndhævelse som styring, ikke høflighed.
For vores vigtigste transaktion over flere tjenester, hvad fortryder hver kompenserende handling egentlig, og hvilke mellemtilstande vil brugere og revisorer se? Sagaer bytter den trøstende illusion om én atomisk transaktion for en sekvens af lokale trin, der hver kan fejle, så systemet reelt passerer gennem tilstande som “reserveret, men ikke betalt” og “debiteret, men ikke afsendt”, før det konvergerer, og at lade som om andet er, hvordan I leverer en saga, der lækker penge eller efterlader forældreløse poster. Gå den reelle proces fra ende til anden igennem, navngiv kompensationen for hvert trin (hvad frigiver lageret, hvad refunderer kortet), og beslut, om en orkestreret saga, I kan overvåge, slår en emergent koreografi, ingen enkelt placering beskriver. Tag de fejltilfælde med, I faktisk har testet, ikke den lykkelige sti, og bekræft at brugeroplevelsen og revisionssporet viser “i gang”- og “kompenseret”-tilstande ærligt frem for at skjule dem. I finans-, ydelses- eller skattesystemer vil en tilsynsmyndighed spørge, hvordan posten så ud på hvert mellemliggende tidspunkt, og hvem der var ansvarlig for kompensationen, så sagaens tilstande er selv et compliance-artefakt.
Hvem driver mægleren eller hændelsesloggen, og har vi talt den sande omkostning ved at drive den op mod et administreret alternativ? Beskedrygraden er ikke gratis infrastruktur, der dukker op, når I tegner den på et diagram: nogen patcher den, skalerer dens partitioner, justerer opbevaring, svarer, når den pager kl. 3 om natten, og ejer dens kapacitet og fejlformer. Beslut bevidst mellem selv at hoste en open source-mægler og at købe en administreret tjeneste, vej kontrol og dataresidens mod driftsbyrden og licensomkostningen, og vær ærlige om, hvorvidt jeres team har dybden til at drive en distribueret log godt. Tag de totale ejeromkostninger med: vagtbelastning, de nødvendige specialistfærdigheder, opbevarings- og lagerregningen og hvad et mæglernedbrud gør ved hvert afhængigt flow. For en virksomhed former svaret et platformsteams mandat, og for et offentligt organ kan indkøbsregler, krav om datasuverænitet og et hårdt behov for at undgå leverandørindlåsning tilsidesætte den billigste mulighed, så bring de begrænsninger frem, før I forpligter jer til en teknologi, I vil drive i et årti.
Sektorperspektiv
Startup. Din knappeste ressource er ingeniørernes opmærksomhed, så forbliv synkron, til udsendelse faktisk gør ondt. Når tre ting skal reagere på én handling, så udgiv en enkelt hændelse (som “OrdreAfgivet”) til en administreret kø eller log frem for at opstille din egen mæglerklynge, og behold betaling og alt, du har brug for et umiddelbart svar fra, som et direkte kald. Adoptér ikke event sourcing, CQRS eller sagaer for at se sofistikeret ud; den kompleksitet vil overgå et lille team og sinke netop den iteration, du konkurrerer på.
Lille virksomhed. Du har ingen specialist i beskedudveksling og ingen appetit på at drive Kafka, så behandl asynkron integration som noget, du køber, ikke driver. Støt dig til de hændelser, dine eksisterende værktøjer allerede udsender (webhooks, de køer, der er indbygget i din cloududbyder eller SaaS-platforme), og lad en administreret tjeneste eje levering, opbevaring og idempotensrørføring. Ram valget ind som køb versus byg ærligt: en håndfuld pålidelige webhookhåndterere slår en skræddersyet mægler, du ikke kan bemande eller fejlsøge kl. 3 om natten.
Stor virksomhed. Dit problem er integrationsomkostning på tværs af mange teams, så gevinsten er en styret fælles platform: en holdbar hændelseslog, et skemaregister med en håndhævet kompatibilitetspolitik, klart emneejerskab og standard idempotens- og outbox-mønstre, så hvert team ikke genopfinder dem. Det er den sammenhæng, hvor erstatning af en skrøbelig enterprise service bus eller et net af punkt-til-punkt-links akkumulerer til reelle besparelser, og hvor orkestrerede sagaer med synlig status lader driftspersonale styre flertrinsprocesser. Budgettér observerbarheds- og skemastyringsinvesteringen eksplicit, fordi en stille forbrugerfejl i denne skala bliver til manglende data på tværs af dusinvis af systemer.
Offentlig sektor. En holdbar, ordnet hændelseslog er et revisions- og åbenhedsaktiv: den besvarer “hvorfor faldt denne afgørelse, som den gjorde” med en uforanderlig sekvens af fakta, så læn jer ind i event sourcing, hvor ansvarlighed kræver det. Datadeling mellem myndigheder har brug for mindst-én-gang-levering med deduplikering på et stabilt hændelses-ID, så en gensendt post aldrig skaber en duplikeret sag, og indkøb bør veje datasuverænitet, offentliggørelse af en mæglers begrænsninger og portabilitet mod indlåsning. Offentliggør, hvor det er passende, hvordan flowet virker, og hvordan en borger kan anfægte et automatiseret udfald, og hold hændelsesloggen som den forsvarlige optegnelse, tilsynsorganer vil bede om at inspicere.
Eksempler
Startup. Et lille e-handelsstartup begynder med ét synkront flow: checkout kalder betalingstjenesten og venter. Efterhånden som det vokser, vil det have ordrebekræftelsesmails, lageropdateringer og et loyalitetsprogram til at reagere på køb, og at koble hver som endnu et synkront kald inde i checkout gør checkout langsom og skrøbelig. Teamet udgiver en enkelt “OrdreAfgivet”-hændelse til en holdbar log og lader tre uafhængige forbrugere reagere, så checkout er hurtig igen, og tilføjelsen af en fjerde reaktion senere kræver ingen ændring af den. De holder betalingsgennemførelse synkron, fordi de har brug for ja-eller-nej-svaret, før de bekræfter ordren, hvilket er præcis den rette linje at trække i deres størrelse.
Stor virksomhed. Et globalt forsikringsselskab drukner i punkt-til-punkt-integrationer og en aldrende enterprise service bus (ESB), et centralt knudepunkt, hvert system dirigerer gennem, og som er blevet en flaskehals og et enkelt fejlpunkt. Det migrerer til en holdbar hændelseslog, hvor hvert domæne udgiver sine fakta (police udstedt, skade anmeldt, betaling foretaget), og forbrugende teams abonnerer på det, de har brug for. En skadessaga, orkestreret så driftspersonale kan se hver skades status, koordinerer den flertrins afvikling med kompensationer for trin, der fejler. Et skemaregister lader policeteamet udvikle sine hændelser uden en synkroniseret udrulning på tværs af fyrre forbrugende systemer, netop den skrøbelighed, den gamle ESB pålagde.
Offentlig sektor. En national skatteadministration skal give borgere og revisorer et forsvarligt svar på “hvorfor faldt min vurdering, som den gjorde.” Den modellerer vurderingsdomænet med event sourcing, så hver ændring er en uforanderlig hændelse i en ordnet log, og den nuværende vurdering er en afspilning af de hændelser; når en borger bestrider et tal, rekonstruerer en sagsbehandler den præcise tilstand på enhver tidligere dato og viser sekvensen af fakta, der producerede den. Datadeling mellem myndigheder kører over holdbare emner med mindst-én-gang-levering, og hver myndigheds forbruger deduplikerer på et hændelses-ID, så en gensendt post aldrig skaber en duplikeret sag. Hændelsesloggen fungerer også som det revisionsspor, tilsynsorganer kræver, og forvandler en compliance-forpligtelse til et biprodukt af designet.
Business case: motivation, ROI og TCO
Afkastet af hændelsesdrevet arkitektur domineres af én ting: omkostningen ved integration over tid. Punkt-til-punkt-integration får den omkostning til at vokse med antallet af forbindelser, som vokser hurtigere end antallet af systemer, så integration bliver skatten, der æder jeres leveringskapacitet. Hændelser udjævner det, fordi teams integrerer gennem en fælles strøm af fakta, en ny forbruger slutter sig til ved at abonnere, og producenter udvikler sig bag et versioneret skema, så den marginale omkostning ved den næste integration falder kraftigt. Det er den centrale ROI-historie at fortælle ledelsen: ikke rå ydeevne, men den akkumulerende reduktion i omkostningen ved ændring på tværs af mange teams.
Navngiv de totale ejeromkostninger ærligt, så I bliver troet. I tager mæglerinfrastruktur at drive på jer, skemastyring at vedligeholde og en stejlere driftsmæssig indlæringskurve, fordi asynkrone systemer er reelt sværere at fejlsøge, så budgettér observerbarhedsinvesteringen på forhånd. Vej disse mod omkostningen ved ikke at adoptere hændelser, hvor de passer: et forstenet integrationslag, hvor hver ændring er et projekt på tværs af flere teams, en legacy-ESB, der er blevet en flaskehals, ingen tør røre, og en manglende evne til at tilføje kapaciteter uden at forstyrre gamle. For virksomheden, der erstatter skrøbelig punkt-til-punkt-ledning, og det offentlige, der bygger holdbare revisionsspor, er gevinsten stærkest, hvor revisions- og afkoblingsbehovene er reelle. Hvor disse behov er fraværende, er det ærlige svar, at et simplere synkront design har den lavere TCO, og I bør sige det.
Anti-mønstre og faldgruber
- Den distribuerede monolit i forklædning. Tjenester, der skal udrulles sammen og afhænger af hinandens interne hændelser. I tilføjede en mægler, men beholdt koblingen, så I har ulemperne ved begge stilarter.
- Hændelser som kommandoer. At udgive “hændelser”, der reelt betyder “gør venligst denne specifikke ting for mig,” og genskabe stram kobling med ekstra latens og dårligere sporbarhed.
- At antage præcis-én-gang-levering. Forbrugere, der går i stykker, dobbeltdebiterer eller duplikerer poster, når mægleren uundgåeligt gensender en besked.
- Event sourcing overalt. At anvende event sourcing og CQRS på simple create-read-update-delete-domæner, der aldrig havde brug for historik, og købe stejl kompleksitet uden gevinst.
- Ingen skemastyring. Producenter, der frit ændrer hændelsesformer og bryder nedstrøms forbrugere på afstand uden kompatibilitetskontroller.
- At ignorere outboxen. At skrive til databasen og udgive en hændelse som to separate trin, så en crash imellem stille taber hændelser eller udsender fantomer.
- Ingen dead letter-håndtering. En enkelt giftbesked, der blokerer en partition, eller fejlede beskeder, der forsvinder uden en kø til at fange og inspicere dem.
- Usynlige flows. Asynkron behandling uden korrelations-ID’er, uden forbrugerforsinkelsesalarmer og uden sporing, så fejl forbliver tavse, til de bliver til manglende data.
Modenhedsmodel
- Niveau 1, Opstart: Integration er ad hoc punkt-til-punkt-kald, eller en mægler findes, men bruges som synkron anmodning/svar. Dubletter ødelægger forbrugere, der er ingen skemadisciplin eller sporing på tværs af spring, og fejlede beskeder forsvinder stille.
- Niveau 2, Udvikling: En mægler eller log er i reel brug for nogle flows, og nogle få teams har grundlæggende praksis: forbrugere bliver idempotente, og dead letter-køer fanger nogle fejl. Men tilgangen er inkonsistent på tværs af teams, hændelser og kommandoer er stadig rodede, skemaer ændres ved uformel aftale, og observerbarhed er tynd.
- Niveau 3, Standardisering: Hændelser, kommandoer og beskeder holdes bevidst adskilt i hele organisationen. Forbrugere er idempotente som standard, skemaer bor i et register med en dokumenteret og håndhævet kompatibilitetspolitik, og den transaktionelle outbox garanterer levering. Sagaer med kompensationer håndterer transaktioner over flere tjenester, korrelations-ID’er propageres gennem hvert spring, og disse mønstre er skrevet ned og anvendt ensartet frem for overladt til hvert team.
- Niveau 4, Styring: Hændelsesplatformen måles og styres med data mod baselines. Forbrugerforsinkelse, dead letter-køens dybde, gensendelsesrater og behandlingslatens følges som alarmerende målinger med aftalte tærskler, ikke dashboards, ingen ser; skemakompatibilitet verificeres automatisk, før en producent kan udgive en ændring; afspilning, fejlhåndtering og idempotens testes efter en fast kadence frem for at blive håbet. Om en given interaktion skal være en hændelse eller et synkront kald afgøres på dokumentation, og hver mæglers kapacitet, opbevaringsomkostning og pålidelighed gennemgås mod mål.
- Niveau 5, Orkestrering: Hændelsesdrevne og synkrone stilarter vælges pr. interaktion som en selvfølge, og event sourcing og CQRS anvendes præcis, hvor revisions- og forespørgselsbehov retfærdiggør dem og ingen andre steder. Asynkrone flows er lige så observerbare som synkrone, platformen forbedres løbende, efterhånden som behov skifter (afvikling af døde emner, udvikling af skemastyring, genbalancering af partitioner), og beskedudveksling er integreret med den bredere arkitektur og revisionsstrategi, så organisationen tilpasser sit hændelsesdesign, efterhånden som forretningen og dens forpligtelser ændrer sig.
Idéer til drøftelse
- Hvilke af jeres nuværende “hændelser” er i hemmelighed kommandoer, og hvilken kobling ville I fjerne ved at modellere dem ærligt?
- Hvis I afspillede en hel dags hændelser gennem jeres forbrugere i morgen, hvad ville gå i stykker, og hvad fortæller det jer om jeres idempotens og afspilningssikkerhed?
- Hvilke dele af jeres domæne har reelt brug for event sourcings revisionsspor, og hvilke er simpel tilstand, I blot ville komplicere ved at source?
- Hvordan ville I migrere væk fra en legacy enterprise service bus eller et net af punkt-til-punkt-integrationer uden en risikabel big-bang-omstilling?
- For jeres vigtigste proces over flere tjenester, er det en reel orkestreret saga med kompensationer, eller en emergent koreografi, ingen enkelt placering beskriver?
Vigtigste pointer
- Hændelsesdrevet arkitektur køber afkobling, uafhængig skalering, afspilning og revisionsspor, og den koster forståelighed og driftskompleksitet, så vælg den pr. interaktion, hvor gevinsten er reel.
- Hold hændelser (fakta, der skete), kommandoer (anmodninger om at handle) og beskeder (konvolutten) adskilt, fordi forvirringen forårsager reelle koblingsfejl.
- Design til mindst-én-gang-levering og gør hver forbruger idempotent; præcis-én-gang-levering er en myte, og præcis-én-gang-effekter er en ingeniørpræstation.
- Rækkefølge er pr. partition, skemaer er kontrakter, der hører hjemme i et register, og outboxen, dead letter-køer og modtryk er den rørføring, der gør beskedudveksling produktionsklar.
- Brug sagaer med kompensationer i stedet for totrinscommit, og ræk kun ud efter event sourcing og CQRS, hvor revisions- og forespørgselsbehov retfærdiggør deres stejle omkostning.
- Asynkrone flows er stille, når de fejler, så korrelations-ID’er, overvågning af forbrugerforsinkelse og sporing fra ende til anden er forskellen mellem et system, der kan drives, og et usynligt.
Referencer og videre læsning
- Martin Kleppmann, Designing Data-Intensive Applications
- Gregor Hohpe and Bobby Woolf, Enterprise Integration Patterns
- Chris Richardson, Microservices Patterns (sagas, transactional outbox, CQRS)
- Sam Newman, Building Microservices
- Ben Stopford, Designing Event-Driven Systems
- Adam Bellemare, Building Event-Driven Microservices
- Vaughn Vernon, Implementing Domain-Driven Design (event sourcing and CQRS)
- Martin Fowler, “Event Sourcing” and “CQRS” (martinfowler.com articles)
- Hector Garcia-Molina and Kenneth Salem, “Sagas” (1987)