A Domain-Driven Design elméletben kiváló. Az aggregátok, bounded contextok, value objectek, domain eventek, anti-corruption layerek — aki komolyan olvasta Evans könyvét, érti miért fontosak mind. A baj az implementációval van: nehéz következetesen megvalósítani, csapaton belül egységesen alkalmazni, és a domain modellt a kóddal szinkronban tartani. A D3I erre keres választ. Nem könyvtár. Nem framework. Egy DSL és egy fordítóból álló eszköz, ami a domain leírásából generálja a kódot — C#-ban, TypeScriptben, Protocol Buffersben.

Miért létezik a D3I?

A komplex rendszerek döntő többsége nem azért válik kezelhetetlenné, mert rossz technológiát használ, hanem azért, mert a kezdeti üzleti és architekturális döntések nem maradnak következetesek az idő előrehaladtával. A D3I erre a problémára keres rendszerszintű választ.

A tervezés és a megvalósítás szétválik

A hagyományos fejlesztési modellekben a tervezés dokumentumokban és diagramokban él, a megvalósítás a forráskódban. A kettő közötti kapcsolat idővel elvész — a dokumentáció elavul, a kód pedig önmagában nehezen értelmezhető. Két év múlva a kód és a domain leírás már nem ugyanarról szól, csak nagyjából hasonlóról. Ez a drift láthatatlan, amíg nem okoz bajt.

A D3I-ban nincs külön tervezés és megvalósítás: a .d3 fájlok nem magyarázzák a rendszert, hanem meghatározzák azt. A dokumentáció nem egy elkészült rendszer utólagos magyarázata, hanem annak elsődleges forrása. Ez garantálja, hogy az üzleti és technikai nézőpont soha nem csúszik szét.

Az üzleti nyelv elvész a kódban

Nagy rendszereknél az üzleti fogalmak lassan primitív típusokká, DTO-kká és technikai struktúrákká laposodnak. Idővel már csak azok értik a rendszert, akik részt vettek az induláskor. A domain expert vevőkről, rendelésekről, szabályokról és folyamatokról beszél — a kód osztályokról, metódusokról és adatbázistáblákról. Ez a rés folyamatosan fennáll, és folyamatosan hibák forrása.

A D3I célja, hogy az üzleti nyelv elsőrendű maradjon: entitások, value objectek, események és kontextusok explicit módon jelennek meg, az üzleti határok nem kommentekben, hanem modellben vannak rögzítve. A rendszer a domain nyelvén szól — nemcsak a fejlesztők, hanem az üzleti szereplők számára is értelmezhető formában.

A hibák minél korábban kerüljenek felszínre

A legtöbb architekturális hiba nem futás közben, hanem tervezési szinten keletkezik — csak túl késő derül ki. A D3I egyik alapelve a fail-fast szemlélet: a modell már tervezéskor ellenőrzi a fogalmi inkonzisztenciákat, a generált absztrakciókon keresztül fordítási időben jelez, és megakadályozza, hogy hibás szerkezetű rendszer egyáltalán elkészüljön. Ha az absztrakció változása az üzleti logika implementációját is érinti, a mikrószervíz fordításakor azonnal hibaként jelentkezik.

Nem köt be egyetlen technológiába

A D3I nem egy konkrét adatbázishoz, frameworkhöz vagy cloud vendorhoz kötődik. A kommunikáció szerződésalapú és verziózott, az adattárolás absztrakció mögé kerül, az emitterek cserélhetők. Ez lehetővé teszi, hogy a rendszer együtt változzon a technológiai környezettel, ne pedig gátjává váljon annak. Ha holnap Rust emitter kell, egy új Python modul elég — a core engine-hez nem kell nyúlni.

Mikor érdemes, mikor nem?

A D3I nem minden projektre való. A modell, a generátor, a pipeline — ez plusz réteg és plusz tanulási görbe. Ha egy egyszerű CRUD API-t kell felrakni hétvégén, ez valóban „ágyúval verébre" eset lenne.

Érdemes D3I-t választani, ha:

  • A domain összetett — sok bounded context, aggregátum, üzleti szabály, és ezek egymáshoz való viszonya számít.
  • A csapat több fejlesztőből áll, és a modell szinkronban tartása komoly kihívás.
  • A rendszernek több platformon kell megjelennie — backend, frontend kliensek, gRPC interfészek egyszerre.
  • A skálázhatóság és az eseményvezérelt architektúra eleve tervezési kritérium, nem utólagos igény.
  • Az üzleti modell várhatóan sokat változik, és ezeket a változásokat követhetően, visszakereshetően kell kezelni.
  • Egy junior/medior csapatnak senior-szintű architekturális döntéseket kell konzisztensen megvalósítania.

Nem érdemes D3I-t választani, ha:

  • Egyszerű, rövid életű alkalmazás — a generátor plusz lépés, ami ennél a skálánál nem térül meg.
  • A csapat nem ismeri a DDD fogalmait — a DSL sem pótol DDD-ismeretet, csak formalizálja azt.
  • Nincs igény kódgenerálásra — ha a cél csak a domain dokumentálása, elég egy diagram vagy egy markdown fájl.
  • Az aktív fejlesztési státusz miatt korai adoptáció esetén számítani kell API törésekre.

A DSL szintaxisa

A D3I saját DSL-t definiál ANTLR 4 grammatikával. A kulcsszavak DDD fogalmak: domain, context, aggregate, root entity, entity, valueobject, composite, view, interface, service, repository, acl, event, enum. Ezek nem osztályok, nem annotációk, nem kommentek — szintaxis. A fordító tudja, mi az aggregate és mi a value object. A különbség nem konvencióban van, hanem a grammatikában.

Íme egy teljes, valósághű D3I modell egy webshop domainből:

domain WebShop {

    context Customers {

        aggregate Customer {

            enum CustomerKind {
                PrivatePerson,
                Company
            }

            valueobject Address {
                @required
                countryCode: string
                city:        string
                zipCode:     string
                street:      string
            }

            root entity Customer {
                @id
                id:      string
                @required
                name:    string
                address: Address
                kind:    CustomerKind
            }

            entity CustomerLogo {
                @id
                id:         string
                customerId: string
                data:       bytes
            }
        }

        interface CustomerIF version 1 {
            valueobject CustomerError {
                statusCode: integer
                message:    string
            }

            @post
            command createCustomer(name: string, countryCode: string)
                : @status(201) Customer
                : @status(400) CustomerError

            @get
            query getCustomer(id: string)
                : @status(200) Customer
                : @status(404) CustomerError
        }
    }

    context Orders {

        aggregate Order {

            valueobject OrderItem {
                product:  string
                quantity: number
                price:    number
            }

            root entity OrderHeader {
                @id
                id:         string
                @required
                customerId: string
                items:      List[OrderItem]
            }
        }

        view OrderSummary {
            orderId:      string
            customerName: string
            totalAmount:  number
            status:       string
        }

        repository OrderRepository {
            save(order: Order)
            findById(id: string): Order
            findByCustomer(customerId: string): List[Order]
        }

        service OrderService {
            command placeOrder(customerId: string, items: List[OrderItem]): Order
            query   getPendingOrders(): List[OrderSummary]
        }

        event OrderCreated version 1 {
            orderId:    string
            customerId: string
            totalAmount: number
        }

        event OrderCreated version 2 extends version 1 {
            currency: string
        }

        interface PublicIF version 1 {
            @post
            command createOrder(customerId: string, items: List[OrderItem])
                : @status(201) Order
                : @status(404) CustomerError
        }
    }

    context Shipping {

        acl from Orders {
            translate OrderHeader as ShipmentRequest {
                id         -> shipmentRef
                customerId -> recipientId
            }
        }

        aggregate Shipment {
            root entity Shipment {
                @id
                id:           string
                shipmentRef:  string
                recipientId:  string
                deliveryDate: date
            }
        }
    }
}

Ez egy egyetlen fájl — és belőle jön az összes generált kód minden platformra.

A típusrendszer

A D3I beépített primitív típusokat ismer: integer, number, float, string, boolean, date, time, dateTime, bytes, stream, any és az internacionalizáláshoz i18nstring. Generikus kollekciók: List[T] és Map[K,V]. Kereszt-context hivatkozások minősített névvel: Customers.Customer — és a fordító ellenőrzi, hogy a hivatkozott típus ténylegesen létezik.

A dekorátorok a generált kód viselkedését vezérlik: @id az aggregátum azonosítóját jelöli, @required kötelező mezőt, @post és @get HTTP metódust, @status(201) a válaszkódot, @internal_api és @public_api a hozzáférhetőséget. A @projected view dekorátor jelzi, hogy a read model melyik aggregátból van levetítve.

Az emitterek — ami a generátorból kijön

A D3I négy natív emitterrel érkezik. Ugyanaz a forrásmodell mind a négybe belefolyik, és négy különböző platformhoz generál kódot.

C# emitter — a leggazdagabb. Domain modell immutable sealed recordokkal és private setterű entitásokkal, IAggregateRoot és IValueObject markerekkel. gRPC controllerek és kliensek, REST controllerek, service interfészek, repository interfészek, ACL implementációk. A generált kód production-kész váz, nem sablon:

// Generált C# — value object
namespace WebShop.Orders {
    public sealed record OrderItem(
        string  Product,
        decimal Quantity,
        decimal Price
    ) : IValueObject;
}

// Generált C# — aggregate root
namespace WebShop.Orders {
    public sealed class OrderHeader : IAggregateRoot {
        [AggregateId]
        public string Id { get; private set; }
        [Required]
        public string CustomerId { get; private set; }
        public IReadOnlyList<OrderItem> Items { get; private set; }
        private OrderHeader() {}
    }
}

// Generált C# — repository interfész
namespace WebShop.Orders {
    public interface IOrderRepository {
        Task SaveAsync(Order order);
        Task<Order> FindByIdAsync(string id);
        Task<IReadOnlyList<Order>> FindByCustomerAsync(string customerId);
    }
}

// Generált C# — service interfész
namespace WebShop.Orders {
    public interface IOrderService {
        Task<Order> PlaceOrderAsync(string customerId, IEnumerable<OrderItem> items);
        Task<IReadOnlyList<OrderSummary>> GetPendingOrdersAsync();
    }
}

TypeScript emitter — kliens oldali kód REST API kliensekkel és type definíciókkal:

// Generált TypeScript — típusok
export interface OrderItem {
  product:  string;
  quantity: number;
  price:    number;
}

export interface OrderHeader {
  id:         string;
  customerId: string;
  items:      OrderItem[];
}

// Generált TypeScript — API kliens
export interface PublicOrdersApi {
  createOrder(
    customerId: string,
    items: OrderItem[]
  ): Promise<OrderHeader | CustomerError>;
}

Protocol Buffers emitter — gRPC service definíciók és event sémák:

// Generált .proto — event séma, versioned
syntax = "proto3";
package webshop.orders;

message OrderCreatedV1 {
  string order_id     = 1;
  string customer_id  = 2;
  string total_amount = 3;  // number → string a proto3-ban
}

message OrderCreatedV2 {
  OrderCreatedV1 base     = 1;
  string         currency = 2;
}

// Generált .proto — service
service PublicOrdersService {
  rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
}

JSON emitter — a teljes szemantikus modell JSON-ba exportálva. Ez az alap saját emitterek írásához, integrációs tesztekhez, vagy dokumentációgeneráláshoz.

Event verziózás — ami máshol nincs

Az event-driven architektúra egyik legnehezebb problémája a breaking change. Ha a producer verziót vált, de a consumer nem tud róla, csendben hibás adatot dolgoz fel. A legtöbb rendszerben ezt kommentekben kezelik, wiki oldalon dokumentálják, vagy elfeledkeznek róla.

A D3I-ban a verzió első osztályú szintaxis. Az event OrderCreated version 2 extends version 1 explicit öröklést definiál — a v2 tartalmazza a v1 összes mezőjét, és hozzáad új mezőket. A fordító ellenőrzi a konzisztenciát. Ha v2 törölni próbál egy v1-es mezőt, fordítási hiba. A generált Protobuf szintén verzionált üzeneteket hoz létre — a fogyasztó kód tudja, melyik verziót várja.

event OrderCreated version 1 {
    orderId:     string
    customerId:  string
    totalAmount: number
}

event OrderCreated version 2 extends version 1 {
    currency: string   // Csak az új mezőt kell felsorolni
}

Anti-Corruption Layer — generált kódként

Az ACL az egyik legtöbbet emlegetett és legkevésbé implementált DDD elem. Elméletben mindenki tudja, hogy kellene — de a legtöbb implementáció egy kézzel írt adapter osztályban végzi, amit senki nem karbantart.

A D3I-ban az ACL szintaxis. A translate blokk explicit mező-leképezést definiál: az Orders context OrderHeader-jéből a Shipping context ShipmentRequest-je jön létre, meghatározott mező-konverzióval. Ezt a fordítást a generátor egy valódi adapter osztállyá alakítja — nem kézi kód, nem konvenció, hanem a modellből következő implementáció.

context Shipping {
    acl from Orders {
        translate OrderHeader as ShipmentRequest {
            id         -> shipmentRef
            customerId -> recipientId
        }
    }
}
// Generált C# ACL adapter
namespace WebShop.Shipping.ACL {
    public class OrdersACL {
        public ShipmentRequest Translate(WebShop.Orders.OrderHeader order) =>
            new ShipmentRequest(
                ShipmentRef: order.Id,
                RecipientId: order.CustomerId
            );
    }
}

View — a CQRS read modelje

A CQRS-ben a read model nem az aggregate belső állapotát tükrözi — hanem azt, amire a lekérdező oldalnak szüksége van. Ez a kettő ritkán ugyanaz. A D3I view kulcsszava ezt explicit teszi: a view önálló típus, nem az aggregate alias-a, és a generátor külön query handler vázat generál hozzá.

view OrderSummary {
    orderId:      string
    customerName: string  // Joins-ból jön — nem az OrderHeader mezője
    totalAmount:  number
    status:       string
}

A customerName mezőt a view tartalmazza, de az OrderHeader aggregát nem. Ez szándékos. A view megmutatja, hogy a read modell egy denormalizált projekció — és a fordítónak ez nem okoz problémát, mert a view más típusszabályok alatt él, mint az entitások.

Repository és Service — explicitté tett kontraktok

A D3I a repository és a service fogalmát is első osztályú elemként kezeli. Nem annotált interfészek — dedikált szintaxis, saját kulcsszóval.

repository OrderRepository {
    save(order: Order)
    findById(id: string):               Order
    findByCustomer(customerId: string): List[Order]
}

service OrderService {
    command placeOrder(customerId: string, items: List[OrderItem]): Order
    query   getPendingOrders(): List[OrderSummary]
}

A command és query kulcsszavak a CQRS szándékot teszik explicitté a service szinten is. A generátor az ezekből létrehozott C# interfészekben ezt az elválasztást megtartja — a command handlerek és a query handlerek külön interfészen kapnak helyet.

A fordítóhoz hasonló pipeline

A D3I belső architektúrája egy komplett fordítóra épül. Öt fázis, sorban:

Lexer és Parser (ANTLR 4) — a .d3 fájlokat tokenizálja, majd parse tree-t épít. A grammatika explicit DDD fogalmakat kezel first-class keywordként. A szintaktikai hibák itt derülnek ki — hiányzó kapcsos zárójel, ismeretlen kulcsszó, rossz dekorátor.

ElementBuilder — visitor pattern alapján a parse tree-ből szemantikus element tree épül. Minden DSL elem saját Python objektum: Domain, Context, Aggregate, RootEntity, ValueObject, Event és így tovább. Ez az AST az, amivel az emitterek dolgoznak.

Engine — az import rendszer feldolgozása, több fájl összefésülése, szülő-hivatkozások felépítése. A D3I modellek fájlokra bonthatók és importálhatók — egy nagy rendszer több .d3 fájlból áll össze.

SemanticChecker — a szemantikai validáció. Létezik-e a hivatkozott típus? Van-e az aggregate-nek pontosan egy root entityje? Konzisztensek-e az event verziók? Hivatkozik-e az ACL definiált contextre? Léteznek-e a qualified name hivatkozások (Customers.Customer)? Ezek fordítási időben jönnek elő — nem futásidőben, nem code review-ban.

Emitterek — a szemantikusan validált element tree-ből célnyelvi kód generálódik. Az emitter réteg pluggable: a core engine adja az AST-t, az emitter Python modul transzformálja célnyelvi kóddá. Új platform hozzáadása egyetlen új Python modul írása — a grammatika és a semantic checker érintése nélkül.

Import rendszer — moduláris modellek

Egy valódi rendszer domain modellje nem fér el egy fájlban. A D3I import rendszere lehetővé teszi, hogy a modell fájlokra legyen bontva — és ezek a fájlok egymásra hivatkozhassanak:

// customers.d3
domain WebShop {
    context Customers { /* ... */ }
}

// orders.d3
import "customers.d3"

domain WebShop {
    context Orders {
        interface PublicIF version 1 {
            @post
            command createOrder(customerId: string)
                : @status(201) Order
                : @status(404) Customers.CustomerError  // cross-file hivatkozás
        }
    }
}

Az Engine fázis feloldja az importokat, összefésüli a domain modelleket, és ellenőrzi a kereszthivatkozásokat. Ha a Customers.CustomerError nem létezik — fordítási hiba, nem runtime hiba.

A CLI

A D3I parancssori eszközként is használható — CI/CD pipeline-ba illeszthető, pre-commit hookba rakható:

# Telepítés
pip install d3i

# Futtatás — több emitter egyszerre
d3i -i domain/webshop.d3 \
    -e dotnet \
    -e protobuf \
    -e typescript \
    -o generated/ \
    --config d3i.config.json

# Csak linting — kódgenerálás nélkül
d3i -i domain/webshop.d3 -l

# Strict mód — warning is leállít
d3i -i domain/webshop.d3 -e dotnet -o out/ -aoe -aow

A konfiguráció JSON fájlban tartható — generált fájl fejléc, default using-ok, JSON behúzás mélysége:

{
    "dotnet.file_header_lines": [
        "// <auto-generated>",
        "// This file was generated by D3I. Do not edit manually.",
        "// </auto-generated>"
    ],
    "dotnet.default_usings": [
        "System",
        "System.Collections.Generic",
        "System.Threading.Tasks"
    ],
    "json.indent": "4"
}

IDE integráció — Language Server Protocol

A D3I tartalmaz egy LSP szervert, ami VS Code-ban valós idejű szemantikai ellenőrzést ad. Nem kell lefuttatni a generátort ahhoz, hogy látszódjon, ha egy típushivatkozás hibás, ha az aggregate root entity hiányzik, vagy ha egy event verziója inkonzisztens. Az IDE folyamatosan ellenőrzi a modellt, ahogy írod.

A VS Code extension a .d3 kiterjesztésű fájlokat ismeri fel, szintaxiskiemelést ad, és a Python-alapú LSP szerveren keresztül diagnosztikát publikál. A C# alapú LSP szerver párhuzamos fejlesztés alatt áll — OmniSharp Extensions alapon, MediatR command patternnel.

Összehasonlítás más megközelítésekkel

A D3I nem az első kódgeneráló eszköz. De az alternatívák más problémát oldanak meg.

Az OpenAPI / Swagger API-t definiál, nem domain modellt. Nem ismer aggregátot, bounded contextet, ACL-t, domain eventet. Amit generál, az HTTP interfész — nem üzleti logika.

A protoc Protocol Buffer sémából generál kódot — de a séma nem DDD fogalmakban gondolkodik. Egy proto fájlban nincs aggregate és nincs ACL.

A Entity Framework / Hibernate annotációk a perzisztenciát kötik a domain modellhez — pontosan az ellentéte annak, amit a DDD javasol.

A D3I a domain modellt tekinti forrásnak, és belőle generálja az API-t, a perzisztencia interfészeket és a service kontraktokat — egyszerre, minden platformra.

A projekt állapota

A D3I aktívan fejlesztett, 0.1.0 verzióban. A core pipeline — lexer, parser, semantic checker, C# emitter, TypeScript emitter, Protobuf emitter, JSON emitter — production-kész állapotban van, tesztkészlettel. Az LSP szerver prototípus fázisban, a VS Code extension alapszinten működik. A roadmap Java és Rust emittereket tartalmaz.

A projekt nyílt forráskódú. Hozzájáruláshoz nincs szükség semmi másra, mint Python ismeretre és DDD iránti érdeklődésre.

A három projekt viszonya

A D3I, a UniContract és a PolyPersist nem véletlenül kapcsolódnak egymáshoz. A D3I definiálja a domain modellt és generálja a service interfészeket és repository kontraktokat. A UniContract ezeket az interfészeket tartja konzisztensen több platformon. A PolyPersist absztrahálja az adathozzáférést a generált repository interfészek mögött. Együtt egy teljes rétegrendszert alkotnak — a domain modell tetejétől az adattárás aljáig, egységes, gépből jövő kontraktokkal.