Tartalom

0. fejezet

A probléma: a terv és a kód elválik

Miről szól ez a fejezet

Miért nem elég elolvasni a Domain-Driven Design könyvet, és miért csúszik szét szinte minden nagy rendszerben az, amit terveztünk, és amit megírtunk. A D3I erre a problémára készült.

Egy ismerős történet

Egy új rendszer indulásakor mindenki pontosan tudja, mi mire való. Van egy szép architektúra-ábra, van egy wikioldal a fogalmakról, és a csapat ugyanazt érti „rendelés” és „vevő” alatt. Két év múlva az ábra már nem igaz. A wikit senki nem frissíti. A „rendelés” három helyen háromféle mezőkészlettel él, és a kódból csak az tudja visszafejteni az üzleti szabályokat, aki ott volt az elején.

Ezt szokás a fejlesztők lustaságára fogni, pedig az oka szerkezeti. A terv dokumentumokban él, a megvalósítás a forráskódban. A kettő között kézi átírás van, és ami kézzel szinkronizálódik, az előbb-utóbb elcsúszik. Az elcsúszás ráadásul láthatatlan, amíg egyszer bajt nem okoz.

A szokásos válasz az, hogy dokumentáljunk a kódból. Ez segít, de a kódból generált dokumentáció technikai: osztályokról és metódusokról szól, nem vevőkről és rendelésekről. Egy üzleti elemző vagy egy domain szakértő nem tud vele mit kezdeni.

Fordítsuk meg az irányt

A D3I mögött egy egyszerű gondolat áll: ne a kódból készüljön a leírás, hanem a leírásból a kód. A rendszert egy erre kitalált nyelven írjuk le, üzleti fogalmakkal: domain, context, aggregátum, esemény, szerződés. Ebből a leírásból készül a backend váza, a szolgáltatások közti szerződések, a kliensoldali típusok és a gépi feldolgozásra szánt leírás.

Így a modell nem utólagos magyarázat, a rendszer belőle készül. Ha a modell változik, a generált kód is változik. Ha a kézzel írt üzleti logika már nem illik az új modellhez, a projekt nem fordul le. A szétcsúszás így a következő buildnél kiderül, nem két év múlva.

HAGYOMÁNYOS ÚT Terv Word, wiki, táblára rajzolt doboz kézi átírás, idővel elcsúszik Kód az igazság itt van, de nehezen olvasható Kódból generált dokumentáció csak fejlesztőknek érthető A D3I ÚTJA A .d3 modell üzleti nyelven, egyetlen forrás Ellenőrzés szintaxis, szemantika, saját szabályok Kód .NET váz Szerződés proto, TS Leírás JSON, doksi a modell változik → újragenerálás → ahol a kézi kód nem illik hozzá, nem fordul le

← az ábra oldalra görgethető →

1. ábraKét irány Hagyományosan a terv és a kód külön él, és a kettő közötti kézi átírás idővel elcsúszik. A D3I-ban a modell az egyetlen forrás: ebből készül a kód, a szerződés és a leírás is.

Miért éppen a DDD

Eric Evans Domain-Driven Design című könyve 2003-ban adott nevet annak, amit a jó tervezők addig is ösztönösen csináltak. A szoftver minőségét hosszú távon nem a technológia dönti el, hanem az, mennyire érti és mennyire hűen képezi le az üzleti területet. Evans bevezette a közös nyelvet (ubiquitous language), amelyet az üzlet és a fejlesztés egyaránt beszél, és az építőelemeket: bounded context, aggregátum, entitás, value object, esemény.

A DDD-t elméletben könnyű elfogadni, a gyakorlatban nehéz betartani: egy csapaton belül, évekig, cserélődő emberekkel szinte lehetetlen következetesen alkalmazni. A szabályok kommentekben és code review-kon élnek, és egy fáradt pénteki délutánon átcsúszik egy kivétel. A D3I ezeket a szabályokat a nyelvbe teszi. Ami a nyelvben van, azt a fordító ellenőrzi, nem a reviewer.

1. fejezet

Mi a D3I, és hol a helye

Miről szól ez a fejezet

A D3I részei: a nyelv, az értelmező, a linterek, az emitterek és a dekorátorok. Megnézzük, hogyan lesz egy .d3 fájlból kód, és mi történik, ha a modell hibás.

Egy nyelv és egy fordító

A D3I neve a DDD Interpreter rövidítése. Két dolgot jelent egyszerre. Egyrészt egy saját, domain-specifikus nyelvet, amelynek kulcsszavai a DDD fogalmai: domain, context, aggregate, entity, valueobject, event, interface, workflow. Ezek nyelvtani elemek, nem annotációk vagy elnevezési konvenciók. A fordító tudja, mi a különbség egy aggregátum és egy value object között, mert a grammatika mondja meg.

Másrészt egy Python nyelvű értelmezőt, amely a nyelvet feldolgozza. Fejlesztési időben fut, mint egy fordító: könyvtárként nem kerül be a programodba, és futás közben sem irányít semmit.

A MicronIQ része

A D3I egy nagyobb egész, a MicronIQ keretrendszer első lépcsője. A MicronIQ felhős backendek építésére készült, ahol egyszerre több ezer felhasználó és esemény jelenik meg, és a rendszernek évekig kell karbantarthatónak maradnia. Az elemei egymásra épülnek:

  • D3I: a tervezés. Ebből indul minden.
  • UniContract: egy interfészdefinícióból több platformra készít konzisztens klienskódot.
  • PolyPersist: egységes felület különböző adattárolókhoz, így a domain-logika nem kötődik egy adatbázishoz.
  • SrvKit: a szolgáltatások közös alapja: kommunikáció, hibakezelés, hívási környezet, HTTP és gRPC egységesen.
  • Orbit: a telepítés. Idempotens deployer, amely a Kubernetes-környezetet, a Helm chartokat és a szolgáltatásokat kezeli.

Ebben az írásban a D3I-ról lesz szó. A többi elemet ott említem, ahol a generált kód rájuk támaszkodik.

Egy futás lépései

Egy futás négy lépésből áll. Az ANTLR 4 alapú elemző beolvassa a .d3 fájlokat, és szintaxisfát épít belőlük. Ebből szemantikus modell lesz, amelyben a nevek már fel vannak oldva: tudjuk, hogy a Customer hivatkozás melyik contextben melyik aggregátumra mutat. A szemantikus modellt linterek ellenőrzik. Végül az emitterek a modellből kimenetet készítenek.

.d3 fájlok domain, context, aggregate, interface… Elemző ANTLR 4 grammatika Szemantikus modell feloldott nevek és hivatkozások Linterek szintaktikai, szemantikai + saját szabályok Emitterek cél: nyelv és oldal (pl. dotnet:backend) Hiba nincs semmilyen kimenet .NET backend C# váz, kontrollerek TypeScript kliens típusok, validátorok Protocol Buffers .proto a gRPC-hez JSON a teljes modell Saját emitter pl. Java, tervben Egy futás: ha bármelyik linter hibát talál, az emitterek el sem indulnak.

← az ábra oldalra görgethető →

2. ábraEgy d3i-futás A .d3 fájlokat az ANTLR-elemző szemantikus modellé alakítja, ezt linterek ellenőrzik, és csak hibátlan modellből készítenek kimenetet az emitterek.

A linterek az emitterek előtt futnak, és ha hibát találnak, a folyamat leáll. Hibás, ellentmondásos modellből nem készül se kód, se dokumentáció. Ez a fail-fast elv: a hiba ott derüljön ki, ahol a legolcsóbb kijavítani.

Linterek, emitterek, dekorátorok

Linterek. Két beépített linter mindig lefut: a szintaktikai és a szemantikai. A szemantikai ellenőrzés a DDD szabályait kényszeríti ki: például hogy interfészen nem jelenhet meg entitás, vagy hogy egy esemény új verziója nem hagyhat el mezőt. Ezek mellé saját lintereket is írhatsz Pythonban, projekt- vagy cégspecifikus szabályokkal.

Emitterek. Az emitter dönti el, milyen kimenet készül. Ma négy van: dotnet:backend (C# szolgáltatásváz), typescript:client (típusok, REST-kliens és validátorok), proto (gRPC-szerződés) és json (a teljes modell gépi formában). Java emitter tervben van. Saját emittert is írhatsz, a mag módosítása nélkül.

Dekorátorok. A dekorátor egy @-cal kezdődő jelölés a modell valamelyik elemén. Új üzleti fogalmat nem vezet be, csak kiegészítő információt ad a lintereknek és az emittereknek. A @public_api(rest, collection="BFF") megmondja, hogy egy interfész milyen technológián és kinek készüljön. Az @optional elhagyhatóvá tesz egy mezőt, a @gdpr személyes adatot jelöl, a @deprecated elavultat. Saját dekorátort is bevezethetsz, és a saját lintered vagy emittered értelmezheti.

A futtatás egyetlen parancs:

python cli.py -p WebShop -i webshop.d3 \
    -e dotnet:backend typescript:client proto json \
    -o ./generated

A D3I mellé VS Code-bővítmény is tartozik: egy Python nyelvi szerver már írás közben jelzi a hibákat, ugyanazokkal a szabályokkal, mint a fordító.

2. fejezet

A térkép: domain és context

Miről szól ez a fejezet

A modell legfelső két szintje: a domain mint üzleti terület, a context mint felelősségi és telepítési egység. Itt dől el, hány mikroszervizből áll majd a rendszer.

A példa: egy webáruház

A szakma régóta webáruházzal szemlélteti a DDD-t, mert mindenki érti, hogyan működik. Én is ezt használom, de nem kitalált részletekkel: a példák a nyilvános samplewebshop repó valódi modelljéből jönnek. Ez a modell 375 sor, négy fájlban.

Domain: üzleti terület, nem modul

A domain a legmagasabb szintű szervezőelem. Egy alkalmazásban kevés van belőle, és mindegyik egy fő üzleti területet határol el: értékesítés, ügyfélkezelés, fizetés, szállítás, jogosultságkezelés. A domain nem technikai modul. Üzleti csoportosítás: azt mutatja meg, hol vannak az üzleti tudás természetes határai.

Egy kis webshopnál ezek a határok elmosódnak, hiszen egy ember is átlátja az egészet. Egy banknál viszont a hitelezés, a kártyakezelés, a számlavezetés és a befektetés külön világ, külön szakértőkkel és döntéshozókkal. A domainhatár ott ennek az üzleti valóságnak a leképezése. A D3I kis rendszernél is a domainek használatát javasolja. Ez valamivel kötöttebb munkát jelent, de a hibakeresés, a független bővítés és a biztonságos refaktorálás bőven megtéríti.

Context: egy felelősség, egy mikroszerviz

A bounded context a nyelvben egyszerűen context. Ez egy domainen belüli határ, amelyen belül egy modell és egy nyelv érvényes. Egy domainben jellemzően több context van. Ha egy domainben csak egy van, érdemes elgondolkodni, valóban önálló domain-e.

A D3I a contextet egyben mikroszerviz-határnak tekinti. Ami egy contexthez tartozik (aggregátum, service, interfész, eseménykezelő, workflow), az ugyanabba a generált szolgáltatásba kerül. A context így a saját adataival, folyamataival, szerződéseivel és integrációival együtt egy felelősségi egység. Kisebb rendszerben több context közösen is telepíthető, de a modellben a határ akkor is megmarad.

domain Sales {

    context OrderManagement {
        aggregate Order { ... }
        service OrderService { ... }
        workflow FulfilOrder { ... }
        interface OrderIF version 1 { ... }
        interface OrderIF version 2 { ... }
    }

    context Tracking {
        eventhandler onOrderPlaced for integration event OrderManagement.OrderIF#1.OrderPlaced#1
        aggregate OrderTrackingEntry { ... }
    }
}
CORE context Base composite BaseEntity id · etag · LastUpdate CUSTOMERMANAGEMENT context Customers = egy mikroszerviz aggregate Customer service CustomerService interface CustomerIF v1 SALES context OrderManagement = egy mikroszerviz aggregate Order OrderHeader, OrderItem service OrderService workflow FulfilOrder foglal → terhel → szállít domain event OrderPlaced belső, verzió nélkül interface OrderIF version 1 DTO-k + OrderPlaced#1 interface OrderIF version 2 DTO-k + OrderPlaced#2 a két verzió egymás mellett él context Tracking = egy mikroszerviz aggregate OrderTrackingEntry eventhandler onOrderPlaced enum TrackingStatuses ref Customer inherits BaseEntity Pontozott keret: domain. Keret: egy context = egy mikroszerviz. Szaggatott nyíl: esemény. Folytonos nyíl: hivatkozás azonosítóval. Vékony szürke: öröklött mezőcsoport.

← az ábra oldalra görgethető →

3. ábraA minta webáruház térképe Három domain, négy context. A Core közös mezőcsoportot ad, az OrderManagement azonosítóval hivatkozik a vevőre, a Tracking pedig a megrendelés publikus eseményére reagál.

A contextek nem hívogatják egymás belsejét. Egymással csak verziózott interfészeken és eseményeken keresztül beszélnek. A Tracking nem tudja, hogyan tárolja az OrderManagement a rendelést, csak azt, amit az OrderManagement magáról kihirdet. Ez elsőre túlzásnak tűnhet egyetlen backendben. De éppen ettől lehet egy contextet úgy átírni, hogy a többi észre se vegye.

Conway törvénye

Melvin Conway 1967-ben figyelte meg, hogy egy rendszer szerkezete a szervezet kommunikációs szerkezetét másolja. Ez a hatás akkor is érvényesül, ha nem tervezünk vele, ezért jobb számolni vele. A D3I számol vele: a domain és a context úgy van kitalálva, hogy természetes csapat- és felelősségi határ legyen. Egy csapat egy contextért felel, és a változásai ott maradnak.

3. fejezet

Az építőkockák

Miről szól ez a fejezet

A contexten belüli elemek: value object, composite, enumeráció, entitás, aggregátum, és az aggregátumok közti hivatkozás. Itt dől el, mi tartozik össze, és hol vannak a konzisztencia határai.

Value object: az érték, aminek nincs személyazonossága

A value objectnek nincs azonosítója. Két cím akkor ugyanaz a cím, ha minden mezőjük megegyezik. A value object nem változik: ha más érték kell, új példány készül. Ez sok állapotkezelési hibát eleve kizár.

A value object lényege, hogy az üzleti fogalom ne essen szét primitív típusokra. Egy postai cím nem négy szöveg, hanem egy cím. Egy pénzösszeg nem egy szám, hanem összeg és pénznem együtt. Ha a modellben így szerepel, nehéz rosszul használni.

valueobject PostalAddress {
    country:string validate len(value) == 2
    postalCode:string validate len(value) > 0
    city:string validate len(value) > 0
    street:string validate len(value) > 0
}

A validate egy kis kifejezésnyelv a mezők üzleti szabályaira. Ismeri az összehasonlítást, a logikai műveleteket, a tartomány- és halmazvizsgálatot, és néhány függvényt, például a len-t és a matches-t. Hivatkozhat a mező saját értékére és a testvérmezőkre is, így egy kezdő- és záródátum viszonya is kifejezhető. A D3I típusosan ellenőrzi: szövegre nem lehet „nagyobb, mint”-et írni. A szabályból olvasható ellenőrző kód lesz a szerveren, és ahol értelmes, a kliensoldali típusok mellé is generálódik validátor. A kliens ellenőrzése a felhasználói élményt szolgálja, a szerver mindig újra ellenőriz.

Composite és enumeráció

A composite újrahasznosítható, nevesített mezőcsoport. Önmagában nem üzleti fogalom, és nincs identitása. Entitások és value objectek öröklik, a mezői pedig beépülnek a befogadó típusba. A minta webáruházban minden tárolt entitás a Core domain BaseEntity mezőcsoportját örökli:

composite BaseEntity {
    @partitionKey
    @system_field
    id:string
    @system_field
    etag:string
    @system_field
    LastUpdate:datetime
}

Az @system_field azt jelzi, hogy ezek a mezők infrastruktúrához tartoznak, nem az üzlethez. A PolyPersistnek szüksége van rájuk, de az emitter kihagyja őket az üzleti felületről.

Az enumeráció zárt értékkészlet: egy rendelés állapota Draft, Released, Finished vagy Canceled, és semmi más. Kiszorítja a „varázsszámokat” és a szétszórt konstansokat. Ha egy enum a kommunikációban is szerepel, a D3I a .proto definíciót és a leképezést is legenerálja hozzá.

Entitás: azonosító és partíciós kulcs

Az entitásnak azonosítója van, és az állapota az élete során változik. Akkor is ugyanaz az ügyfél, ha új címre költözött. Az entitásoknak gyakran partíciós kulcsuk is van, és ez két különböző dolog. Az azonosító megmondja, melyik entitásról van szó. A partíciós kulcs azt, hogy hová tartozik: melyik logikai vagy fizikai csoportba kerül a tárolóban. Egy rendelésnél ez gyakran a vevő azonosítója, egy készletnél a raktáré.

Ez nagy terhelésnél sokat számít, mert az összetartozó adatok együtt kérdezhetők le és együtt skálázhatók. A D3I-ban ez a döntés a modellben látszik, nem az implementáció mélyén: az @partitionKey ott van a mező fölött.

Aggregátum: a konzisztencia határa

Az aggregátum összetartozó entitások és value objectek egysége, amelyet üzletileg mindig együtt kell kezelni. Egyetlen belépési pontja van, az aggregátum gyökere (root entity). Minden módosítás ezen keresztül történik, ezért a gyökér tudja érvényesíteni az üzleti szabályokat. Például azt, hogy egy rendelés végösszege nem lehet negatív, vagy hogy lezárt rendeléshez nem lehet tételt adni.

aggregate Order {

    enum OrderStatuses { Draft, Released, Finished, Canceled }

    valueobject OrderItem {
        productId:string
        productName:string
        quantity:number validate value > 0
        unitPrice:number validate value >= 0
        subTotalPrice:number validate value >= 0
    }

    root entity OrderHeader inherits Core.Base.BaseEntity, SalesDocument {
        # Másik aggregátumra csak azonosítóval hivatkozunk, sosem ágyazzuk be.
        customer:ref CustomerManagement.Customers.Customer

        orderingDate:date
        status:OrderStatuses
        totalPrice:number validate value >= 0
        items:list[OrderItem]

        command place( customerId:string ) emits OrderPlaced
    }
}

Az aggregátum mérete tervezési döntés. A túl nagy aggregátum lassú és nehezen skálázható. A túl kicsi nem tudja megvédeni az üzleti szabályokat. A D3I nem dönti el helyetted, de kikényszeríti, hogy a modellben döntsd el, még a kód előtt.

Hivatkozás aggregátumok között: ref

Egy aggregátum nem tartalmazhat másik aggregátumot. Ha a rendelésbe beágyaznánk a vevőt, a két aggregátum élete összekeveredne: ki felel a vevő adataiért, és mi történik, ha a vevő a rendelés mentése közben változik? Ehelyett a rendelés a vevőre azonosítóval hivatkozik, és ezt a ref kulcsszó fejezi ki.

kívülről csak az aggregátum gyökerén keresztül lehet módosítani AGGREGATE ORDER root entity OrderHeader customer: ref Customer status: OrderStatuses items: list[OrderItem] valueobject OrderItem enum OrderStatuses AGGREGATE CUSTOMER root entity CustomerAccount @gdpr name, email status: CustomerStatuses @optional billingAddress valueobject PostalAddress ref Beágyazni tilos egy aggregátum nem tartalmazhat másikat: a linter itt megállítja a generálást A generált kódban EntityId<CustomerAccount> típus: a vevő és a rendelés azonosítója nem keverhető össze

← az ábra oldalra görgethető →

4. ábraKét aggregátum és a hivatkozás köztük Mindkét aggregátumnak egy gyökere van, a value objectek a gyökérrel együtt élnek. A rendelés a vevőre csak azonosítóval hivatkozik, ebből a generált kódban erősen típusos azonosító lesz.

A ref több dolgot ad egyszerre. Kimondja a szándékot. A linter ellenőrizni tudja, hogy valóban létező aggregátumra mutat, és hogy value objectre vagy aggregátumon belüli entitásra ne mutasson. A generált kódban pedig a hivatkozásból nem nyers szöveg lesz, hanem típusos azonosító.

// generált C#
public EntityId<CustomerManagement.Customers.Customer.CustomerAccount> customer { get; set; }

Egy vevő azonosítója és egy rendelés azonosítója ettől típusszinten különbözik. Ha valaki véletlenül rendelésazonosítót ad át ott, ahol vevőazonosító kell, az már fordításkor kiderül, nem élesben, hajnalban. A hálózaton, a gRPC- és REST-szerződésekben a hivatkozás egyszerű szöveges azonosítóként utazik.

4. fejezet

A határ: service, interface, DTO

Miről szól ez a fejezet

A context belső nyelve és a kifelé ígért szerződés két különböző dolog. A D3I ezt a határt szigorúan őrzi: a domain modell nem szivároghat ki.

Service: a belső nyelv

A service azokat az üzleti műveleteket hordozza, amelyek nem illenek egyetlen entitáshoz sem: több aggregátumot érintenek, vagy folyamatot vezérelnek. A service mindig egy context része, és csak azon belül érhető el. Egy mikroszervizben több service is lehet, mindegyik egy jól körülhatárolt feladattal.

A service felületén domain elemek vannak: entitás, value object, aggregátum, enum, primitív típus. DTO nincs. A service a domain nyelvén beszél, technikai engedmények nélkül.

service OrderService {
    query getOrder( orderId:string ) : Order.OrderHeader
    command placeOrder( order:Order.OrderHeader ) : Order.OrderHeader
}

A command megváltoztatja az állapotot, a query nem. Ez az alapja annak, hogy az olvasási és az írási oldal külön optimalizálható legyen (CQRS). Ebben a view, az olvasásra szabott vetület segít, amely több forrásból is összegyűjthet adatot anélkül, hogy az aggregátumok határát sértené.

Interface: a szerződés

Az interfész a context kifelé látható határa: ezen át éri el más context, más domain, a felhasználói felület vagy egy külső rendszer. Három szabály vonatkozik rá, és mindhármat a linter ellenőrzi:

  1. Kötelező a verzió. Az interfész ígéret a fogyasztóinak, és a verzió mondja meg, mit ígér.
  2. A felületén csak DTO, enum és primitív típus lehet. Entitás, value object, aggregátum soha.
  3. DTO csak interfészen belül definiálható. A DTO kommunikációs adatszerkezet, nem a domain része.
@internal_api(grpc, rest)
@public_api(grpc, rest, collection="BFF")
interface OrderIF version 1 {

    dto OrderItemDTO {
        productId:string
        productName:string
        quantity:number validate value > 0
        unitPrice:number validate value >= 0
        subTotalPrice:number
    }

    dto OrderDTO {
        dto CustomerDataDTO {
            customerId:string
            customerName:string
        }
        @system_field id:string
        orderingDate:string
        orderStatus:OrderStatuses
        totalPrice:number
        customerData:CustomerDataDTO
        items:list[OrderItemDTO]
    }

    query getOrder( orderId:string ) : OrderDTO
    command placeOrder( order:OrderDTO ) : OrderDTO
}

A dekorátorok mondják meg, hogyan érhető el az interfész: belül gRPC-n és REST-en, kívül a BFF-gyűjteményben is. Ugyanabból a definícióból készül a kontroller, a .proto, a belső kliens és a frontend TypeScript-kliense.

Miért nem elég az entitást kiküldeni

Kényelmes volna az OrderHeader entitást egyszerűen visszaadni az interfészen. Pont ez az a kényelem, amely két év múlva megköti a kezed. Ha a belső modell egyben a publikus szerződés, akkor a belső modell nem változhat anélkül, hogy egy külső fogyasztó el ne törjön. A DTO ezt a csatolást vágja el. Azt tartalmazza, amire az adott hívásnak szüksége van. Ugyanahhoz az aggregátumhoz több DTO is tartozhat: egy rövid összesítő a listához, egy részletes a szerkesztőoldalhoz.

Ha valaki mégis entitást tenne az interfészre, a d3i megállítja. Ez a linter valódi üzenete:

Sales.d3(143,47): Severity.Error : An interface may only expose dto, enum and
primitive types on its surface; 'Order.OrderHeader' is a entity.
Map it to a dto for the wire contract.
abort on error is enabled, process is aborted
Kliensek web és mobil (BFF), másik context egy hívás egy szerződésen át interface OrderIF version 1, 2 verziózott szerződés · gRPC és REST átléphet: DTO, enum, primitív entitás itt: linterhiba, nincs generálás Fordítás DTO ↔ domain, kézzel írva itt dől el, mit lát a külvilág service OrderService üzleti logika a domain nyelvén entitás, value object, aggregátum Repository aggregátumot ment és tölt be csak az aggregátum gyökerén át Tárolók (PolyPersist) SQL, dokumentum, kereső, eseménytár… a domain nem tudja, melyik

← az ábra oldalra görgethető →

5. ábraEgy hívás útja a contexten belül A kliens a verziózott interfészt hívja, amely csak DTO-t ismer. A kézzel írt fordítás után a service a domain nyelvén dolgozik, a repository pedig absztrakció mögött ment.

Repository és ACL

A repository a perzisztencia és a domain határa. Aggregátumot ment és tölt be, mindig a gyökéren keresztül, és csak a saját contextjén belül érhető el. A felületén csak domain elemek vannak. Az implementációja a PolyPersistre épül, így a domain nem tudja, és nem is kell tudnia, hogy SQL-be, dokumentumtárba vagy eseménytárba ment. Mivel interfész mögött van, a tesztekben egyszerűen helyettesíthető.

Az anti-corruption layer (ACL) a külső rendszerek felé véd. Ha a webshop egy futárcég rendszeréhez kapcsolódik, annak státuszkódjai és fogalmai nem kerülhetnek be közvetlenül a saját Shipping domainünkbe. Az ACL lefordítja őket a saját nyelvünkre. Üzleti logika nincs benne, csak fordítás. Egyetlen helyen enged meg olyat, amit máshol a D3I tilt: az any típust, mert itt találkozunk még nem tipizált, idegen adattal.

5. fejezet

Események

Miről szól ez a fejezet

Az esemény múltbeli tény, nem parancs. A D3I megkülönbözteti a belső domain eseményt, a publikus integrációs eseményt és az audit rekordot, mert ezek más-más problémát oldanak meg.

Tény, nem kérés

Egy RPC-hívásnak címzettje van, és választ vár. Az eseménynek nincs címzettje, és nem vár semmit. Azt mondja, hogy „ez megtörtént”, és aki érdekelt, reagál rá. A kibocsátó nem tudja, ki hallgatja, és nem is kell tudnia. Ettől lazul a csatolás: új fogyasztó jöhet anélkül, hogy a kibocsátóhoz hozzányúlnánk.

Az esemény szerkezete szigorú. Csak primitív mezőket és enumot tartalmazhat, entitást vagy value objectet nem, és nincs benne viselkedés.

Ki jegyzi fel, ki küldi el

A minta webáruházban a rendelés gyökere deklarálja, hogy a place parancs eseményt termel:

command place( customerId:string ) emits OrderPlaced

A szerepek pontosan el vannak választva. A gyökér feljegyzi a tényt, mert ő érvényesíti azt a szabályt, amelytől a tény igaz. A repository a mentéssel együtt egy outboxba teszi, ugyanabban a tranzakcióban. A kézbesítés a platform dolga. Így nem fordulhat elő, hogy a rendelés elmentődik, de az eseményt valami miatt senki nem kapja meg, vagy fordítva. A generált kód ezt meg is magyarázza:

/// Writes down that 'OrderPlaced' happened. It is NOT sent here: the repository moves it
/// into the outbox inside the same save, and delivery is the platform's problem from there.
protected void Record( OrderPlaced @event )
    => _recordedEvents.Add( new RecordedEvent( @event, id ) );

Belső tény és publikus szerződés

A domain esemény a context belső nyelvén szól. Nincs verziója, és ez szándékos: a fogyasztói ugyanabban a telepítési egységben vannak, együtt változnak vele, ezért ha a szerkezete eltörik, abból fordítási hiba lesz, nem éles incidens.

domain event OrderPlaced {
    orderId:string
    customerId:string
    totalPrice:number
}

Az integrációs esemény ugyanannak a ténynek a publikus megfogalmazása, egy interfészen belül, verzióval. A from megmondja, melyik belső tényből készül. A fordítást szándékosan kézzel kell megírni, mert a publikus nyelv nem azonos a belsővel. Itt például a customerId bent marad, az összeg pedig úgy kap nevet, ahogy a külvilág hívja.

interface OrderIF version 1 {
    integration event OrderPlaced version 1 from Order.OrderPlaced {
        orderId:string
        totalAmount:number
    }
    ...
}

A fogyasztó a publikus eseményre iratkozik fel, verzióra pontosan. A Tracking context így reagál a megrendelésre:

context Tracking {
    eventhandler onOrderPlaced for integration event OrderManagement.OrderIF#1.OrderPlaced#1
    ...
}

Ettől holnap az OrderManagement átnevezhet egy belső mezőt, és a Tracking észre sem veszi. A kezelő a contexthez tartozik, nem egy service-hez, mert a context az, ami reagál. Hogy melyik osztály futtatja, az már megvalósítási részlet.

OrderHeader.place() Record(OrderPlaced) Repository.save() állapot + outbox, egy mentés Fordítás ToOrderPlaced_v1 / _v2 Kafka-csatorna Sales.OrderManagement Tracking onOrderPlaced domain event integration event feldolgozás Domain esemény – belső nincs verziója: a fogyasztói ugyanazzal a kóddal mozognak, ha változik, fordítási hiba lesz belőle Integrációs esemény – publikus verziózott szerződés, kézzel fordítva a belső tényből: a fogyasztó csak ezt látja, a belső mezők bent maradnak audit record bizonyíték, nem esemény: senki nem reagál rá, csak megőrizzük Eseménytár csak hozzáfűzés: üzleti történet és audit egy helyen A gyökér csak feljegyez, a küldés a platform dolga: az outbox miatt nincs olyan mentés, amelyből kimarad az esemény.

← az ábra oldalra görgethető →

6. ábraEgy esemény útja A rendelés gyökere feljegyzi a tényt, a repository az állapottal együtt az outboxba menti, egy kézzel írt fordítás publikus, verziózott eseménnyé alakítja, a Tracking context pedig ezt dolgozza fel.

Audit: bizonyíték, nem esemény

A harmadik fajta a bejelentkezés, a jelszócsere, egy jogosultság módosítása: ki mit csinált és mikor. A korábbi terveimben ez is eseményfajta volt. A nyelvben végül audit record lett, mert az audit nem esemény. Senki nem reagál rá, nem indít folyamatot, csak megőrizzük bizonyítéknak. Ha eseménynek hívnánk, olyan viselkedést ígérnénk, amely nincs. Az ilyen apró névadási döntéseken múlik, hogy a modell évek múlva is igazat mond-e.

Tároláskor viszont mindkettő ugyanaz: megváltoztathatatlan, csak hozzáfűzhető bejegyzések időrendben. Ezért az üzleti eseményfolyam és az audit ugyanabban az eseménytárban él, külön audit-adatbázis nélkül. Ha egy aggregátum eventsourced jelölést kap, az aktuális állapota nem is tárolódik közvetlenül. Az eseményeiből áll össze, és bármikor visszajátszható.

6. fejezet

Verziózás: amit egyszer megígértél

Miről szól ez a fejezet

Egy publikus interfész vagy esemény ígéret. A D3I megköveteli a verziószámot, és azt is ellenőrzi, hogy az új verzió betartja-e, amit a régi ígért.

Két verzió egymás mellett

Az eseményvezérelt rendszerek egyik legrosszabb hibája a csendes törés. A kibocsátó megváltoztatja egy üzenet szerkezetét, a fogyasztó nem tud róla, és hibás adatot dolgoz fel, hibaüzenet nélkül. A legtöbb helyen ezt wikioldal és jó szándék kezeli.

A D3I-ban az interfész új verziója az előző mellé kerül, nem a helyére. A minta webáruházban az OrderIF version 1 és version 2 egyszerre él. A régi fogyasztók maradhatnak a v1-en, az újak átállhatnak a v2-re. A generált kódban mindkettőnek saját osztálya, kontrollere és .proto fájlja van: OrderIF_v1, OrderIF_v2.

A deklaráció olvasható alakot használ (version 2), a hivatkozás tömöret: OrderIF#1.OrderPlaced#1. A hivatkozásban így mindig látszik, melyik verzióra számítasz.

A verziószám ígéret

Erre a minta webáruház fejlesztése közben volt egy példa. A v2 integrációs esemény első változata el akarta hagyni a totalAmount mezőt, és helyette a customerId-t publikálta volna. Az evolúciós linter visszadobta:

Sales.d3(154,12): Severity.Error : 'OrderPlaced' version 2 drops the field
'totalAmount', which version 1 has. A higher version is the same fact restated,
so what it used to say has to survive; if the field really is gone, this is a
different fact and deserves its own name.

A szabály egyszerű. Egy magasabb verzió ugyanannak a ténynek az újrafogalmazása, ezért többet mondhat, de nem mondhat kevesebbet. Aki v1-ről v2-re vált, nem veszíthet el semmit, amit eddig megkapott. Ha egy mező tényleg megszűnik, akkor az már egy másik tény, és másik nevet érdemel.

VERSION 1 integration event OrderPlaced orderId totalAmount VERSION 2 – ELFOGADVA integration event OrderPlaced orderId totalAmount + customerId VERSION 2 – ELUTASÍTVA integration event OrderPlaced orderId totalAmount – hiányzik customerId Bővíthet aki v1-ről v2-re vált, nem veszít semmit Linterhiba a v2 elhagyná, amit a v1 ígért: ez már egy másik tény A két verzió egyszerre él: a régi fogyasztók maradhatnak a v1-en. Hivatkozás verzióval: OrderIF#1.OrderPlaced#1

← az ábra oldalra görgethető →

7. ábraMit szabad egy új verziónak Egy publikus esemény új verziója bővítheti a mezőket, de nem hagyhat el olyat, amit a korábbi verzió ígért. Ezt a linter ellenőrzi.

Végül a v2 mindkét mezőt megtartotta, és hozzátette a harmadikat. Ugyanabból a belső Order.OrderPlaced tényből így két publikus szerződés készül, két külön kézzel írt fordítással. Ez a gyakorlatban azt jelenti, hogy egy interfészt úgy viszünk tovább, hogy a régi hívókat nem törjük el.

7. fejezet

Hosszú folyamatok: workflow

Miről szól ez a fejezet

Vannak üzleti folyamatok, amelyek nem férnek bele egy tranzakcióba: napokig tartanak, félúton elbukhatnak, és vissza kell csinálni, amit már megcsináltak. A D3I ezeket elsőrendű elemként, workflow-ként kezeli.

Amikor egy függvény már nem elég

Egy kezdő webshopban a rendelés lezárása egy státuszfrissítés. Aztán bejön a készletfoglalás. Aztán a kártyás fizetés, a számlázás, a szállítás előkészítése, egy külső futárcég értesítése. Ezek nem egyszerre történnek, és bármelyik elbukhat. Workflow nélkül két rossz út marad. Az egyik, hogy az egész egyetlen, egyre hízó service-függvénybe kerül. A másik, hogy események laza láncává esik szét, amelyet senki nem lát egyben. Mindkettő ott bukik meg, ahol egy lépés félúton elhasal, és vissza kell csinálni a korábbiakat.

A felhasználói regisztráció ugyanez kicsiben: e-mail-ellenőrzés, aktiváló link, jelszóbeállítás, alapjogosultságok. A lépések között órák telhetnek el, és a folyamatnak pontosan onnan kell folytatódnia, ahol abbamaradt.

Lépések és kompenzációk

A D3I workflow-ja leírja, milyen lépések vannak, és melyiket mi teszi vissza. A sorrendet szándékosan nem a modell írja le, mert egy modell becsületesen csak a lépések halmazát tudja kimondani. A sorrendet a fejlesztő írja meg. A generált váz minden végrehajtott lépés kompenzációját feljegyzi, és hiba esetén fordított sorrendben lefuttatja őket.

@retry( 3 )
@timeout( "10m" )
workflow FulfilOrder {

    @start
    command fulfil( order:ref Order, sku:string, quantity:number,
                    amount:number, shippingAddress:string ) : string

    # Nincs visszatérési értéke, tehát jelzés: a hívó nem kap választ.
    command cancel( reason:string )

    # A lekérdezés csak olvas, és szinkron: nem várhat semmire.
    query status() : string

    step reserveStock( order:ref Order, sku:string, quantity:number ) : string compensate releaseStock
    step releaseStock( order:ref Order, sku:string, reservationId:string )

    # A kártyaterhelés nem idempotens: egyetlen próbálkozás, rövid határidő.
    @retry( 1 )
    @timeout( "30s" )
    step chargeCard( order:ref Order, amount:number ) : string compensate refundCard
    step refundCard( order:ref Order, chargeId:string )

    # A szállítást semmi nem teszi vissza: ami elment, az elment.
    step shipOrder( order:ref Order, shippingAddress:string ) : string
}

Ebben a pár sorban sok üzleti döntés van, és mind a modellben látszik. A kártyaterhelés nem idempotens, ezért csak egyszer próbáljuk, és rövid a határideje, hogy a pénz ne ragadjon bizonytalan állapotban. A szállításnak nincs kompenzációja, és ez a hiány maga is állítás. Ezek nem technikai beállítások, hanem üzleti tulajdonságok, ezért a modellbe valók.

workflow FulfilOrder @retry(3) · @timeout(10m) · indítás: fulfil() · jelzés: cancel() · lekérdezés: status() reserveStock készlet lefoglalása chargeCard @retry(1) · @timeout(30s) shipOrder csomag feladása Kész releaseStock foglalás feloldása refundCard terhelés visszautalása compensate compensate nincs compensate: ami elment, az elment hiba a szállításnál a kompenzációk fordított sorrendben futnak Normál út Visszagörgetés

← az ábra oldalra görgethető →

8. ábraA rendelés teljesítése és visszagörgetése Három lépés. Ha a szállítás elbukik, a kártyaterhelés és a készletfoglalás kompenzációja fordított sorrendben fut le. A szállításnak nincs kompenzációja.

Ami a háttérben fut

A workflow mögött a Temporal fut, amely a hosszú folyamatok állapotát tartósan tárolja, újraindítás után folytatja, és látni engedi, hol tart egy folyamat. A D3I a lépésekből Temporal-aktivitásokat generál. Ezek interfészét kell megvalósítani:

public partial interface IFulfilOrderActivities
{
    [Activity]
    Task<string> reserveStock(EntityId<Order.OrderHeader> order, string sku, decimal quantity);
    [Activity]
    Task releaseStock(EntityId<Order.OrderHeader> order, string sku, string reservationId);
    [Activity]
    Task<string> chargeCard(EntityId<Order.OrderHeader> order, decimal amount);
    ...
}

A workflow-k hatókör szerint is elkülönülnek. Van, amelyik egy contexten belül marad, van, amelyik egy domainen belül több contextet hangol össze, és van, amelyik domaineken ível át. Mindegyik önálló szolgáltatásként fut a Kubernetesben, így a skálázása és a figyelése a saját terheléséhez igazodik.

8. fejezet

Mit kapsz a generátortól

Miről szól ez a fejezet

A minta webáruház 375 sor modell. Ebből 54 fájl és nagyjából hatezer sor kód lesz. Megnézzük, mi az, amit a d3i megír, mi az, amit neked kell, és hogyan tartja a kettőt egyben a fordító.

A generált szolgáltatás felépítése

Minden contextből egy egységes felépítésű mikroszerviz lesz. Ha egyszer kiismerted az egyiket, mindegyikben otthon vagy:

  • Interfaces: a publikus, verziózott szerződések, a DTO-k, a gRPC-leképezés és a .proto fájlok.
  • Context: a context magja. A belső service- és ACL-interfészek, a gRPC- és REST-kontrollerek, az eseménykezelők és a workflow-k vázai, valamint az implementációk, ahol az érdemi munka folyik.
  • InternalClient: típusos kliensek, amelyekkel más contextek hívhatják ezt a szolgáltatást a rendszeren belül. A kliens szolgáltatást kér, nem címet.
  • Models: az entitások, value objectek és enumok generált kódja. Kézzel nem módosítjuk, mert újragenerálódik.
  • Services: az indításhoz, konfigurációhoz és telepítéshez szükséges elemek, a SrvKitre építve.

A generált kód és a kézzel írt kód határa

A legfontosabb tervezési döntés az, hogy hol a határ. A d3i minden olyan kódot megír, amely a modellből egyértelműen következik. Te azt írod, ami üzleti döntés. A kettőt C#-ban partial osztályok és metódusok kötik össze: a generált fél deklarálja, mi kell, a te feled adja a törzset.

/// Declared by the model; the body is yours. Without it this does not compile - it does
/// not quietly become a command that is merely never there.
public partial void place(string customerId);

Ezt nevezem biztonsági hálónak. Ha a modellbe új parancs, új eseménykezelő vagy új fordítás kerül, és nem írod meg a törzsét, a projekt nem fordul le, vagyis a hiányzó kód nem maradhat észrevétlen. Ugyanez fordítva: ha a modellből kikerül valami, a hozzá írt kódod hibát jelez. A modell és a kód nem csúszhat szét, mert a fordító a kettő közti szerződést ellenőrzi.

A D3I ÍRJA – ÚJRAGENERÁLÓDIK modellek: Clone, Equals, HashCode, Validate interfész-absztrakciók és DTO-k gRPC- és REST-kontrollerek belső kliensek más contexteknek .proto és TypeScript-típusok, validátorok eseménykiadók, kezelő-regisztráció Temporal-váz a workflow-hoz TE ÍROD – MEGMARAD a service-ek törzse a parancsok törzse, pl. place() belső tény → publikus esemény az eseménykezelők törzse a workflow lépései és sorrendjük Csak üzleti logika. Az infrastruktúra a bal oldalon van. partial osztályok és metódusok a modell kimondja, mi kell; ha a kézzel írt törzs hiányzik, a projekt nem fordul le A minta webáruház: 375 sor modell → 54 fájl, kb. 6000 sor generált kód

← az ábra oldalra görgethető →

9. ábraKi mit ír A generátor minden infrastruktúra-kódot megír és újraír. A fejlesztő csak az üzleti logikát írja, és ha valami hiányzik, a fordító szól.

Néhány generált részlet

Egy value object a modellben öt sor. A generált C# osztály tartalmazza az értékalapú működéshez szükséges klónozást, összehasonlítást és hash-számítást, és a validate szabályokból olvasható ellenőrzést:

public virtual void ValidateInto( IList<IValidationError> errors, string pathPrefix )
{
    if (quantity <= 0)
        errors.Add( new ValidationError { TypeOfEntity = "OrderItem", MemberOfEntity = "quantity",
            Path = pathPrefix + "quantity", ErrorText = "quantity must satisfy: value > 0" } );

    if (unitPrice < 0)
        errors.Add( new ValidationError { TypeOfEntity = "OrderItem", MemberOfEntity = "unitPrice",
            Path = pathPrefix + "unitPrice", ErrorText = "unitPrice must satisfy: value >= 0" } );
}

A hibának útvonala van (items[1].quantity), ezért egy űrlap a megfelelő mezőhöz tudja kötni, nem kell szövegből kitalálnia. Ugyanez a szabály a frontend TypeScript-kódjában is megjelenik:

export function validateOrderItemDTOInto( dto: OrderItemDTO, pathPrefix: string,
                                          errors: ValidationError[] ): void {
    if (Number(dto.quantity) <= 0)
        errors.push({ typeOfEntity: "OrderItemDTO", memberOfEntity: "quantity",
                      path: pathPrefix + "quantity", errorText: "quantity must satisfy: value > 0" });
    ...
}

Az interfészből gRPC-szerződés is készül, egységes válaszborítékkal: státusz, hibalista és az eredmény.

service OrderIF_v1 {
    rpc getOrder(OrderIF_v1_getOrderRequest) returns (OrderIF_v1_getOrderResponse);
    rpc placeOrder(OrderIF_v1_placeOrderRequest) returns (OrderIF_v1_placeOrderResponse);
    ...
}

message OrderIF_v1_getOrderResponse {
    ServiceKit.Protos.Statuses Status = 1;
    repeated ServiceKit.Protos.Error Errors = 2;
    oneof result {
        OrderDTO Value = 3;
    }
}

A modellben írt # kezdetű dokumentációs sorok végigmennek a generált kódon. Ami a modellben magyarázat volt, az a C#-ban XML-dokumentáció, a .proto-ban megjegyzés lesz. A leírás és a kód itt sem válik el.

Tesztelhetőség

A generált struktúra interfészekre és dependency injectionre épül. A service-ek, repositoryk és külső kapcsolatok mind interfész mögött vannak, így az üzleti logika izoláltan tesztelhető, adatbázis és hálózat nélkül. A tesztelhetőséget így nem kell utólag beépíteni.

9. fejezet

A háttérben álló gondolkodás

Miről szól ez a fejezet

Miért mikroszervizek, miért események, miért ennyiféle tároló? Ez a fejezet azt a tapasztalatot foglalja össze, amelyből a D3I szabályai származnak.

Monolit, mikroszerviz, esemény

Sokáig a monolit volt az alapértelmezés: egy futtatható egységben az üzleti logika, az adattárolás, az integrációk és a felület. Kis léptékben egyszerű és jó. Ahogy nő, úgy nő a változtatás kockázata, a skálázás csak egészben lehetséges, és az üzleti területek határai elmosódnak.

A mikroszerviz erre adott válasz: kisebb, önállóan telepíthető egységek, világosabb felelősség, célzott skálázás, párhuzamosan dolgozó csapatok. A komplexitás azonban nem tűnt el, csak átköltözött a szolgáltatások közé: kommunikáció, állapot, folyamatok összehangolása. Erre jött az eseményvezérelt szemlélet, amely a hívások helyett a történésekre figyel, és ebből nőtt ki az event sourcing is.

A D3I ezt az utat követi, és nem enged visszacsúszni egy rejtett monolitba. A contextek mikroszerviz-határok, az együttműködés fő eszköze az esemény, a hosszú folyamatok workflow-k. Ez nem jelenti azt, hogy minden rendszernek maximális bonyolultsággal kell indulnia. Azt jelenti, hogy a szerkezet eleve alkalmas a növekedésre.

Minden adat a megfelelő tárolóba

Csábító egyszerűsítés, hogy „minden megy az SQL-be”. Kis rendszerben működik is. Nagyobb rendszerben az adatok annyira különböző módon keletkeznek, változnak és olvasódnak, hogy egy tároló mindegyiknek csak rosszul tud megfelelni.

PolyPersist egy felület, sokféle tároló Relációs PostgreSQL aktuális állapot, tranzakció Dokumentum MongoDB együtt mozgó adat, pl. termékleírás Kereső OpenSearch szűrés, szabad szöveges keresés Széles oszlop ScyllaDB nagy, folyamatosan hozzáfűzött adat Eseménytár EventStoreDB üzleti történet és audit Analitika ClickHouse riport, statisztika Cache Redis gyakran olvasott, ritkán változó A tároló a felhasználás módjától függ, nem megszokásból választjuk.

← az ábra oldalra görgethető →

10. ábraHétféle tároló, egy felület A PolyPersist egységes felülete mögött hétféle tároló áll, mindegyik más felhasználási mintára.
  • Relációs adatbázis oda, ahol erős konzisztencia és tranzakció kell, és mindig az aktuális állapot számít: rendelési állapot, készlet, pénzügyi nyilvántartás.
  • Dokumentumtár oda, ahol az adat természetes egységet alkot és együtt mozog: egy termék teljes leírása, attribútumai, fordításai.
  • Keresőmotor a gyors, rugalmas kereséshez és szűréshez. Ez nem elsődleges tároló, hanem a lekérdezés kiszolgálója, és leveszi a terhet az operatív adatbázisról.
  • Széles oszlopos tároló a nagy, folyamatosan hozzáfűzött adatokhoz, partíció szerinti eléréssel. Az eseménytár egyik lehetséges háttere.
  • Eseménytár a megváltoztathatatlan, időrendi eseményekhez. Ebből bármikor visszafejthető egy rendelés teljes élete, és ugyanez szolgálja ki az auditot.
  • Analitikai tároló a riportokhoz és statisztikákhoz, feldolgozott eseményekből.
  • Cache a gyakran olvasott, ritkán változó adatokhoz. Teljesítményeszköz, nem forrás.

Ez a sokféleség csak akkor kezelhető, ha a domain nem tud róla. Ezt adja a PolyPersist: a repository egységes felületen ment és tölt be, a tároló kiválasztása pedig a domainen kívüli döntés marad.

Backend a felület szolgálatában

A Backend for Frontend (BFF) minta szerint a felhasználói felületeket nem a domain-szolgáltatások interfészei szolgálják ki közvetlenül, hanem egy kifejezetten az adott felülethez szabott backend réteg. A webes felület, a mobilalkalmazás és a belső adminfelület más adatot, más szerkezetben, más válaszidővel igényel. Ha egyetlen interfész próbálja mindet kiszolgálni, túlterhelt és nehezen verziózható API lesz belőle.

A D3I-ban a BFF ugyanolyan verziózott interfész, mint a többi, csak a dekorátora más: @public_api(rest, collection="BFF"). Ebből a d3i a felületnek szóló TypeScript-klienst és a .NET API-klienst is legenerálja. A domain modell eközben egységes marad.

„Ez túl bonyolult”

A fenti minták első ránézésre sokszor túltervezettnek tűnnek. Ennek megvan az oka: valós, nagy léptékű rendszerek problémáira adnak választ, és amíg egy probléma nem jelentkezik, a megoldása sem tűnik fontosnak. Amikor a rendszer mérete, terhelése vagy üzleti bonyolultsága elér egy szintet, ezekből a döntésekből alapfeltétel lesz. Akkor viszont már késő újratervezni.

A D3I azt ígéri, hogy ezeket a döntéseket egy átlagos, medior csapat is következetesen meg tudja hozni az első naptól, anélkül hogy minden tagja végigjárta volna azokat a drága, elhúzódó projekteket, amelyekből ez a tapasztalat származik. A szabályok a nyelvben vannak, nem a fejekben.

10. fejezet

Mikor érdemes, mikor nem, és hol tart a projekt

Miről szól ez a fejezet

Kinek való a D3I, kinek nem, mi működik már, és mi van még hátra.

Érdemes, ha…

  • a domain összetett: sok context, aggregátum és üzleti szabály, és ezek viszonya számít;
  • több fejlesztő dolgozik rajta, és a modell szinkronban tartása valódi gond;
  • a rendszernek egyszerre kell backend-szolgáltatásként, gRPC-szerződésként és frontend-kliensként megjelennie;
  • a skálázhatóság és az eseményvezérelt működés eleve követelmény, nem utólagos igény;
  • az üzleti modell várhatóan sokat változik, és a változásokat követhetően kell kezelni;
  • egy junior–medior csapatnak kell senior szintű architekturális döntéseket következetesen megvalósítania.

Nem érdemes, ha…

  • egyszerű, rövid életű alkalmazásról van szó. Egy hétvégi CRUD API-hoz ez ágyúval verébre;
  • a csapat nem ismeri a DDD fogalmait. A nyelv formalizálja a DDD-t, de nem tanítja meg;
  • csak dokumentálni szeretnéd a domaint, kódgenerálás nélkül. Ehhez egy jó ábra is elég;
  • stabil, végleges API-t vársz. A projekt aktív fejlesztés alatt áll, a korai átvevőknek számolniuk kell törésekkel.

Hol tart

A D3I nyílt forrású, Apache 2.0 licenccel. Ami ma működik:

  • a teljes nyelv: domain, context, aggregátum, entitás, value object, composite, enum, view, service, interfész, DTO, repository, ACL, a háromféle tény, eseménykezelő és workflow;
  • a validate kifejezésnyelv típusos ellenőrzéssel, szerver- és kliensoldali kódgenerálással;
  • a szemantikai linter a DDD-szabályokkal és a verzióevolúció ellenőrzésével;
  • a .NET backend, a TypeScript kliens, a Protocol Buffers és a JSON emitter;
  • a Temporal-alapú workflow-váz és az outboxos eseménykezelés;
  • VS Code-bővítmény nyelvi szerverrel, és több mint 340 automata teszt.

Ami hátravan: a Java emitter, a @gdpr jelölésből generált törlési és exportálási mechanizmus, és a MicronIQ többi elemének (SrvKit, Orbit) nyilvános kiadása.

Kipróbálni

A D3I-tól nem azt várom, hogy kevesebb kódot kelljen írni. Azt várom tőle, hogy a context-határok, a verzióígéretek és a modellben rögzített döntések betartását a fordító ellenőrizze, és ne a csapat emlékezetén múljon.

Függelék A

Szójegyzék

Magyar kifejezés, angol eredeti, egysoros meghatározás, a magyar kifejezés szerinti betűrendben.

Magyar Angol Meghatározás
Aggregátum aggregate Összetartozó entitások és value objectek egysége, a konzisztencia határa; csak a gyökerén át módosítható.
Aggregátumgyökér aggregate root Az aggregátum egyetlen belépési pontja (root entity), amely az üzleti szabályokat érvényesíti.
Anti-corruption layer anti-corruption layer, ACL Fordítóréteg egy külső rendszer és a saját domain között, hogy az idegen fogalmak ne szivárogjanak be.
Audit rekord audit record Megőrzésre szánt bizonyíték (ki, mit, mikor); nem esemény, mert senki nem reagál rá.
Backend for Frontend BFF Egy adott felhasználói felülethez szabott backend-interfész.
Bounded context bounded context Határ, amelyen belül egy modell és egy nyelv érvényes; a D3I-ban egy context egy mikroszerviz.
Composite composite Újrahasznosítható, nevesített mezőcsoport, amelyet entitások és value objectek örökölnek.
CQRS Command Query Responsibility Segregation Az állapotot módosító parancsok és a csak olvasó lekérdezések szétválasztása.
Dekorátor decorator @-cal kezdődő jelölés, amely kiegészítő információt ad a lintereknek és az emittereknek.
Domain domain Fő üzleti terület (pl. értékesítés, fizetés); üzleti csoportosítás, nem technikai modul.
Domain esemény domain event Egy context belső, verzió nélküli ténye, amelyet a saját telepítési egységén belül dolgoznak fel.
DTO data transfer object Kommunikációs adatszerkezet, csak interfészen belül definiálható; nem része a domain modellnek.
Emitter emitter A d3i modulja, amely a modellből egy adott kimenetet készít (pl. .NET, TypeScript, proto, JSON).
Entitás entity Azonosítóval rendelkező üzleti objektum, amelynek állapota az élete során változik.
Enumeráció enum Zárt, előre megadott értékkészlet, például egy rendelés lehetséges állapotai.
Eseménykezelő event handler A context nevesített reakciója egy eseményre; a D3I-ban az eventhandler kulcsszó.
Event sourcing event sourcing Az állapot nem közvetlenül tárolódik, hanem az eseményekből áll össze és játszható vissza.
Hivatkozás azonosítóval ref Aggregátumok közti kapcsolat; a generált kódban erősen típusos azonosító lesz belőle.
Integrációs esemény integration event Egy belső tény publikus, verziózott megfogalmazása, amelyet más contextek és rendszerek fogyasztanak.
Interfész interface A context kifelé látható, verziózott szerződése; a felületén csak DTO, enum és primitív lehet.
Kompenzáció compensation Egy workflow-lépés hatását visszacsináló lépés; hiba esetén fordított sorrendben futnak.
Közös nyelv ubiquitous language Az üzlet és a fejlesztés által egyaránt használt nyelv, amely a modellben is megjelenik.
Linter linter Ellenőrző, amely a generálás előtt a modell szintaktikai, szemantikai vagy saját szabályait vizsgálja.
Outbox outbox Az aggregátummal azonos mentésben tárolt kimenő események listája, ahonnan a platform kézbesít.
Partíciós kulcs partition key Azt mondja meg, hová tartozik egy entitás a tárolóban; az összetartozó adatok együtt maradnak.
Repository repository Aggregátumokat mentő és betöltő absztrakció, amely elrejti a tároló technológiáját.
Saga saga Több lépésből álló, kompenzációkkal visszagörgethető folyamat; a D3I-ban workflow.
Service service A context belső üzleti műveletei, domain típusokkal; kívülről nem érhető el.
Single source of truth single source of truth Az egyetlen hiteles forrás; a D3I-ban a .d3 modell.
Validálás validate A mező mellé írt üzleti szabály, amelyből szerver- és kliensoldali ellenőrző kód készül.
Value object value object Azonosító nélküli, nem változó érték, amelyet a mezői határoznak meg (pl. cím, összeg).
View view Olvasásra szabott vetület, amely több forrásból gyűjthet adatot, és nem módosítható.
Workflow workflow Elhúzódó, állapottal rendelkező üzleti folyamat lépésekkel és kompenzációkkal.