ZDL Domain Modeling Language
ZDL is a domain specific language (DSL) for modeling Event-Driven microservices.
Among all approaches to software development, Domain-Driven Design is the only one that focused on language as the key tool for a deep understanding of a given domain’s complexity. - Alberto Brandolini in Event Storming
Inspired by JHipster JDL, ZDL is a language for describing DDD Bounded Contexts, including domain entities and their relationships, services, commands, events and business policies... for Event-Driven Architectures.
It's designed to be compact, readable and expressive. Business friendly, developer friendly, and machine friendly.
ZDL works well as an Ubiquitous Language format.

File Structure
ZDL files follow a structured format with two main sections:
File Types and Extensions
ZenWave supports two file types for different purposes:
.zw
files - Plugin configuration files that can be executed directly from IntelliJ using the play button in the gutter.zdl
files - Domain model files that can include both model-related configuration and domain definitions
1. Prolog Section (Optional)
Contains metadata and configuration used by ZenWave tooling:
global javadoc
- File-level documentationconfig
- Plugin configuration and generation settings (for.zw
files) or model-related configuration (for.zdl
files)apis
- API definitions (OpenAPI, AsyncAPI references)
2. Domain Definition Section
Contains the actual domain model declarations:
entities
- Domain entities and aggregatesenums
- Enumeration typesservices
- Business operations and commandsinputs/outputs
- Data transfer objectsevents
- Domain eventspolicies
- Business rules and policies
Structure Rules
- Files can be split into plugin configuration (
.zw
) and model configuration (.zdl
) - Prolog elements (
global javadoc
,config
,apis
) must appear at the top if present - Domain elements can be declared in any order and quantity
- All prolog elements are optional
Syntax Pattern
[<global javadoc>][<config>][<apis>][<entity> | <enum> | <aggregate> | <policies> | <service> | <input> | <output> | <event>]*

The following Complete ZDL Examples demonstrate a full application with aggregates, REST API adapters, and domain event publishing:
ZenWave Scripts (.zw)
// This is a ZenWave Scripts File (.zw)//// This file defines ZenWave SDK plugins and their configurations for SDK execution and code generation.//// Usage:// - Execute plugins using the Play button in IntelliJ IDEA ZenWave Model Editor// - Or Copy the generated command line hovering the play button on each plugin// - Or Run commands directly from terminal using the generated command line//// Plugin: https://plugins.jetbrains.com/plugin/22858-zenwave-domain-model-editor-for-zdl//// There are global properties that apply to all plugins, and plugin specific properties.// Configuration Precedence (highest to lowest):// 1. Command line arguments always have the highest precedence// 2. Plugin-specific properties (defined in this file)// 3. Global properties (defined in this file)// 4. Properties from referenced .zdl files//config {// This is a global configuration that applies to all plugins.zdlFile "zenwave-model.zdl"// these should match the values of openapi-generator-maven-plugin// used by the OpenAPIControllersPlugin and SpringWebTestClientPluginopenApiApiPackage "{{basePackage}}.adapters.web"openApiModelPackage "{{basePackage}}.adapters.web.model"openApiModelNameSuffix DTOplugins {/** Generates an OpenAPI 3.0 specification from the ZDL model. */ZDLToOpenAPIPlugin {idType integeridTypeFormat int64targetFile "src/main/resources/public/apis/openapi.yml"}/** Generates an AsyncAPI 3.0 specification from the ZDL model. */ZDLToAsyncAPIPlugin {asyncapiVersion v3idType integeridTypeFormat int64targetFile "src/main/resources/public/apis/asyncapi.yml"// includeKafkaCommonHeaders true}/** Generates a Backend Application from the ZDL model. (Headless Core) */BackendApplicationDefaultPlugin {useLombok trueincludeEmitEventsImplementation true// --force // overwite all files}/** Generates Spring MVC controllers from the OpenAPI specification (Web Adapters). */OpenAPIControllersPlugin {openapiFile "src/main/resources/public/apis/openapi.yml"}SpringWebTestClientPlugin {openapiFile "src/main/resources/public/apis/openapi.yml"}SpringWebTestClientPlugin {openapiFile "src/main/resources/public/apis/openapi.yml"groupBy businessFlowbusinessFlowTestName CreateUpdateDeleteCustomerIntegrationTestoperationIds createCustomer,updateCustomer,deleteCustomer,getCustomer}OpenAPIKaratePlugin {openapiFile "src/main/resources/public/apis/openapi.yml"}OpenAPIKaratePlugin {openapiFile "src/main/resources/public/apis/openapi.yml"groupBy businessFlowbusinessFlowTestName CreateUpdateDeleteCustomerKarateTestoperationIds createCustomer,updateCustomer,deleteCustomer,getCustomer}}}
ZenWave Models (.zdl)
/*** Sample ZenWave Model Definition.** This model describes a simple Customer entity with a one-to-many relationship with PaymentMethod.** Use zenwave-scripts.zw to generate your code from this model definition.*/config {title "ZenWave SDK - Mongodb BaseLine"basePackage "io.zenwave360.example"persistence jpadatabaseType postgresql// you can choose: DefaultProjectLayout, LayeredProjectLayout, SimpleDomainProjectLayout// CleanHexagonalProjectLayout, HexagonalProjectLayout, CleanArchitectureProjectLayoutlayout LayeredProjectLayout}/*** Customer entity*/@aggregate@auditing // adds auditing fields to the entityentity Customer {name String required maxlength(254) /** Customer name */email String required maxlength(254) pattern(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/)/** Customer Addresses can be stored in a JSON column in the database. */@json addresses Address[] minlength(1) maxlength(5) {street String required maxlength(254)city String required maxlength(254)}}@auditingentity PaymentMethod {type PaymentMethodType requiredcardNumber String required}enum PaymentMethodType { VISA(1), MASTERCARD(2) }relationship OneToMany {@eagerCustomer{paymentMethods required maxlength(3)} to PaymentMethod{customer required}}// you can create 'inputs' as dtos for your service methods, or use entities directlyinput CustomerSearchCriteria {name Stringemail Stringcity Stringstate String}@rest("/customers")service CustomerService for (Customer) {@postcreateCustomer(Customer) Customer withEvents CustomerEvent@get("/{id}")getCustomer(id) Customer?@put("/{id}")updateCustomer(id, Customer) Customer? withEvents CustomerEvent@delete("/{id}")deleteCustomer(id) withEvents CustomerEvent@post("/search")@paginatedsearchCustomers(CustomerSearchCriteria) Customer[]}@copy(Customer)@asyncapi({ channel: "CustomersChannel", topic: "customers" })event CustomerEvent {id Longversion Integer// all fields from Customer are copied here, but not relationshipspaymentMethods PaymentMethod[]}
Entities and Aggregates
Entities describe your domain model and form the core of your Bounded Context. They can be grouped into aggregates using either the @aggregate
annotation on the aggregate root (recommended) or by defining rich domain aggregates with command handlers and domain events using aggregate
objects.
Entity declaration syntax:
[<entity javadoc>][<entity annotation>*]entity <entity name> [(<table name>)] {[<field javadoc>][<field annotation>*]<field name> <field type> [<validation>*] [<field suffix javadoc>] [,]}
ZDL entities are compatible with JHipster JDL entities with additional extensions:
- Annotations with values: single/double-quoted strings, numbers, booleans, and JSON objects
- Nested entities directly on fields
- Field types that can reference other entities or custom types
- Arrays as field types
Annotations
Annotations are decorators that add metadata to entities, similar to Java or TypeScript. They are optional and can be applied to entities, enums, fields, relationships, services, commands, and events.
Supported parameter types: keywords, strings (single/double quoted), numbers, booleans, and JSON objects.
@aggregate@extends(BaseEntity)entity ParkingLot {}
SDK plugins interpret certain annotations for code generation:
@extends
- Java class extension@copy
- Copy fields without inheritance
Fields
entity A {/** the name field */name String required uniqueage Integer min(16) /** the age field *//*** the favorite meal field*/favoriteMeal String maxlength(255)anotherField StringstringWithValue String = "initial value" requiredenumTypeWithValue MyEnum = "MyEnum.VALUE1"}
Field Types and Validations
Supported field types:
- Primitives:
String
,Integer
,Long
,int
,long
,BigDecimal
,Float
,float
,Double
,double
,Boolean
,boolean
- Date/Time:
LocalDate
,LocalDateTime
,ZonedDateTime
,Instant
,Duration
- Binary:
byte
,byte[]
,Blob
,AnyBlob
,ImageBlob
,TextBlob
- Other:
UUID
,Enum
- Custom: Any other entity, enum, or custom type
Available validations:
required
,unique
min(value)
,max(value)
minlength(value)
,maxlength(value)
pattern(/expression/)
Documentation Comments
ZDL supports standard comment styles:
// line comment (ignored)
/* block comment (ignored) */
/** documentation comment (preserved) */
Fields support inline suffix documentation: fieldName Type /** inline docs */
Relationships
Relationships are mostly compatible with JDL Relationships with some differences:
- ZDL does not support
display field
orwith builtInEntity
- Options are more flexible and can be any valid annotation name/value pair
Supported relationship types: OneToOne
, OneToMany
, ManyToOne
, and ManyToMany
Syntax:
relationship (OneToMany | ManyToOne | OneToOne | ManyToMany) {[<from relationship javadoc>][<from relationship annotation>*]<from entity>[{<relationship field name> [required]}] to[<to relationship javadoc>][<to relationship annotation>*] <to entity>[{<relationship field name> [required]}]}
Relationships can be grouped under the same relationship
keyword or declared separately:
relationship ManyToMany {EntityA{fieldPointingB} to EntityB{fieldPointingToA}EntityC{fieldPointingD required maxlength(10)} to EntityD{fieldPointingToC}}relationship OneToOne {EntityParent{fieldPointingChild} to @MapsId EntityChild{fieldPointingToParent}}
ZenWave SDK follows DDD rules for aggregates, cascading persistence from aggregate root to dependent entities and generating unit tests to validate these cascades work as expected.
Relationships Between Aggregates
From a DDD perspective, aggregates should be self-contained units of consistency. Relationships between aggregates should typically be modeled by reference/id rather than rich object relationships.
However, modeling relationships by id alone loses information about the relationship being modeled. ZenWave SDK supports modeling rich relationships between aggregates while still generating code that uses reference/id for the actual relationship, plus a read-only rich relationship object for accessing the related aggregate.
This option should be explicitly activated by using addRelationshipsById
when using BackendApplicationDefaultPlugin
.
@aggregateentity Customer {name String required}@aggregateentity CustomerOrder {orderDate ZonedDateTime required/* customerId Integer */ // modeling by id loses relationship information}relationship ManyToOne {// This generates both CustomerOrder.[get/set]CustomerId() methods// and a read-only CustomerOrder.getCustomer() methodCustomerOrder{customer} to Customer}
Nested Entities
When working with large object graphs, it's often more expressive to nest entities directly on fields rather than defining them separately.
Nested entities are supported for entities, inputs, outputs, and events.
The following two mappings are equivalent:
| -> |
|
NOTE: For entities, the generated code depends on the persistence technology used:
- MongoDB generates nested objects, while
- JPA generates embedded entities with all columns in the same table. Nested arrays are not supported in JPA.
Enums
Enums are compatible with JDL enums with some differences:
- Enum keys are not required to be uppercase
- Enum values can only be numbers
enum <enum name> {<ENUM KEY> [(<enum value>)]}
Aggregate Objects
Aggregate object combines entities, command handlers and domain events for rich domain aggregates.
They look very similar to services
, and for simple use cases you can use services
and an entity annotated @aggregate
as the aggregate root.
[<aggregate javadoc>][<aggregate annotation>*]aggregate <aggregate name> (<aggregate root entity>) {[<command javadoc>][<command annotation>*]<command name>([<CommandInput>]) [withEvents <DomainEvent>*]}
ZDL Example:
aggregate DeliveryAggregate (Delivery) {createDelivery(DeliveryInput) withEvents DeliveryStatusUpdatedonOrderStatusUpdated(OrderStatusUpdated) withEvents DeliveryStatusUpdatedupdateDeliveryStatus(DeliveryStatusInput) withEvents DeliveryStatusUpdated}
Services and Commands
Services and commands are used to describe the operations, beyond CRUD, that can be performed on your domain aggregates.
Services follow this structure:
[<service javadoc>][<service annotation>*]service <service name> for (<aggregate>[,<aggregate>]*)] {[<command javadoc>][<command annotation>*]<command name>([<id>], [<CommandInput>]) [<CommandOutput>] [withEvents <DomainEvent>*]}
Service Commands
Service commands are transactional units of work that perform operations on aggregate entities.
They represent the functionality of your inner hexagon and should connect to the outside world through APIs, Adapters, and Mappers. Commands can be documented with annotations (like @rest
or @asyncapi
) that indicate which adapters will expose them. The SDK interprets these annotations for code generation, but they should not be confused with actual public APIs.
Some SDK plugins can generate draft OpenAPI and AsyncAPI definitions from these annotations, but the final API specifications (OpenAPI, AsyncAPI) should be considered the source of truth.
Service commands resemble Java methods but have important differences. They support only two parameter types:
id
- Indicates the command operates on a specific aggregate entity instanceCommandInput
- Points to anentity
orinput
type containing the command data
Command Output:
- Can be any
entity
oroutput
type, or omitted entirely - If an entity type, indicates the entity will be created or updated
- Can be marked optional with
?
if the command may not return output
Events:
- Use
withEvents
to specify domain events published after command execution
/*** Service for Order Attachments.*/@rest("/order-attachments")service AttachmentService for (CustomerOrder) {@postuploadFile(id, AttachmentFileInput) CustomerOrder? withEvents [AttachmentFileUploaded|AttachmentFileUploadFailed]@get("/{orderId}")listAttachmentFiles(id) AttachmentFileOutput[]@get("/{orderId}/{attachmentFileId}")downloadAttachmentFile(AttachmentFileId) AttachmentFileOutput}entity CustomerOrder {}input AttachmentFileInput {}output AttachmentFileOutput {}
Service CRUD Commands
Service methods matching a CRUD pattern are treated as special by the SDK and an according CRUD implementation would be generated:
service CustomerService for (Customer) {createCustomer(CustomerOrCustomerInput) Customer?updateCustomer(id, CustomerOrCustomerInput) Customer?getCustomer(id) Customer?listCustomers() Customer[]deleteCustomer(id)}
CRUD commands may emit any domain events.
Command parameter type can be an aggregate entity or an input type. In some cases, and depending on configured settings, ZenWave SDK may generate input dtos even if entity types are used, following hexagonal/clean/onion architecture principles.
Business Policies
Policies documents business decisions and rules. They can be associated with a particular aggregate and then referenced with the @policy(policy_code)
annotation.
policies (Customer) {policy001 "Describe here the content of this business rule"}service CustomerService for (Customer) {@policy(policy001)createCustomer(Customer) Customer withEvents CustomerUpdated}
Inputs
Inputs follow the same structure as entities, but they are not persistent. They belong to the outer ring/hexagon of your application, along with the Mappers. They are used to pass data to service commands.
They can reference other entities and enums but not they other way around. They also support nested entities.
input AttachmentFileInput {name String requiredfile Blob requiredmimetype AttachmentFileType required}
Outputs
Outputs follow the same structure as entities, but they are not persistent. They belong to the outer ring/hexagon of your application, along with the Mappers. They are used to pass data to service commands.
They can reference other entities, inputs and enums but not they other way around. They also support nested entities.
input AttachmentFileOutput {customerOrderId Integer requiredname String requiredfile Blob requiredmimetype AttachmentFileType required}
Domain Events
Domain events are used to describe the events that are published by your domain aggregates.
[<event javadoc>][<event annotation>*]event <event name> [(<channel name>)] {[<field javadoc>][<field annotation>*]<field name> <field type> [<validation>*] [<field suffix javadoc>] [,]}
Configuration Section
SDK plugin options can be configured in the config
section.
This config options are inherited by all the SDK plugins, but each plugin can also define its own options.
config {basePackage "com.example.myapp"persistence mongodb}
IMPORTANT NOTE: config
and apis
sections should be at the top of the file, before any other declaration and after global javadoc
.
SDK Plugins
Plugins configured in the config section can be executed directly from ZenWave Editor (IntelliJ IDEA).
In addition to their own configuration, they will inherit the following options:
zdlFile
pointing to the current file,targetFolder
to the project folder (not the file folder)- and all configuration from
config
section.
ZenWave Scripts (.zw)
// This is a ZenWave Scripts File (.zw)//// This file defines ZenWave SDK plugins and their configurations for SDK execution and code generation.//// Usage:// - Execute plugins using the Play button in IntelliJ IDEA ZenWave Model Editor// - Or Copy the generated command line hovering the play button on each plugin// - Or Run commands directly from terminal using the generated command line//// Plugin: https://plugins.jetbrains.com/plugin/22858-zenwave-domain-model-editor-for-zdl//// There are global properties that apply to all plugins, and plugin specific properties.// Configuration Precedence (highest to lowest):// 1. Command line arguments always have the highest precedence// 2. Plugin-specific properties (defined in this file)// 3. Global properties (defined in this file)// 4. Properties from referenced .zdl files//config {// This is a global configuration that applies to all plugins.zdlFile "zenwave-model.zdl"// these should match the values of openapi-generator-maven-plugin// used by the OpenAPIControllersPlugin and SpringWebTestClientPluginopenApiApiPackage "{{basePackage}}.adapters.web"openApiModelPackage "{{basePackage}}.adapters.web.model"openApiModelNameSuffix DTOplugins {/** Generates an OpenAPI 3.0 specification from the ZDL model. */ZDLToOpenAPIPlugin {idType integeridTypeFormat int64targetFile "src/main/resources/public/apis/openapi.yml"}/** Generates an AsyncAPI 3.0 specification from the ZDL model. */ZDLToAsyncAPIPlugin {asyncapiVersion v3idType integeridTypeFormat int64targetFile "src/main/resources/public/apis/asyncapi.yml"// includeKafkaCommonHeaders true}/** Generates a Backend Application from the ZDL model. (Headless Core) */BackendApplicationDefaultPlugin {useLombok trueincludeEmitEventsImplementation true// --force // overwite all files}/** Generates Spring MVC controllers from the OpenAPI specification (Web Adapters). */OpenAPIControllersPlugin {openapiFile "src/main/resources/public/apis/openapi.yml"}SpringWebTestClientPlugin {openapiFile "src/main/resources/public/apis/openapi.yml"}SpringWebTestClientPlugin {openapiFile "src/main/resources/public/apis/openapi.yml"groupBy businessFlowbusinessFlowTestName CreateUpdateDeleteCustomerIntegrationTestoperationIds createCustomer,updateCustomer,deleteCustomer,getCustomer}OpenAPIKaratePlugin {openapiFile "src/main/resources/public/apis/openapi.yml"}OpenAPIKaratePlugin {openapiFile "src/main/resources/public/apis/openapi.yml"groupBy businessFlowbusinessFlowTestName CreateUpdateDeleteCustomerKarateTestoperationIds createCustomer,updateCustomer,deleteCustomer,getCustomer}}}
APIs Section
APIs are used to document APIs exposed by your application (provider) or Third Party APIs consumed by you (client).
Use it to document the API uri
along with any other field/value that you want to include. It also supports javadoc comments.
apis {<api type>([provider|client]) <api name> {uri <api uri>(<field> <value>)*}}
Example:
apis {asyncapi(provider) MyEventsApi {uri "src/main/resources/asyncapi.yml"}openapi(provider) MyRestApi {uri "src/main/resources/openapi.yml"}asyncapi(client) ThirdPartyEventsApi {uri "https://.../asyncapi.yml"}openapi(client) ThirdPartyRestApi {uri "https://.../openapi.yml"}}