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.