DDD en la Práctica: De Lenguaje Ubícuo en DSL a Proyecto Completo de Spring Boot con Kotlin

En este artículo exploraremos cómo utilizar ZenWave360 para generar un proyecto completo de Spring Boot con Kotlin a partir de un modelo DSL de Lenguaje Ubicuo.
Ilustración humorística que muestra un coche con el texto “If software engineers had named the automobile, it’d be called PistonCrankshaftGearWheelAssembly”, resaltando la necesidad de un lenguaje común y claro en desarrollo de software.

Índice

  1. DDD en la Práctica: De Lenguaje Ubícuo en DSL a Proyecto Completo de Spring Boot con Kotlin

  2. Importancia del Lenguaje Ubícuo en Domain Driven Design

  3. ZenWave Domain Model Language: Un Lenguaje de Modelado que funciona como Lenguaje Ubícuo

  4. Modelado de Dominio de Negocio con ZenWave 360º

  5. ZenWave 360: Inicio Rápido para los más Impacientes

  6. Caso Práctico: Customers Service – Del Modelo ZDL al Proyecto Spring-Boot

  7. Customización del Código Generado

  8. Proyecto Completo de Spring-Boot con Kotlin

  9. Conclusión

1. Introducción

Domain-Driven Design (DDD) es un enfoque de diseño de software que sigue plenamente vigente tras más de dos décadas. Su objetivo principal es alinear el modelo de software con el modelo mental de los expertos de negocio, creando un lenguaje compartido que facilite la comunicación entre perfiles técnicos y no técnicos.

En este artículo veremos cómo aplicar DDD en la práctica utilizando el Lenguaje Ubicuo como punto de partida y llevándolo hasta un proyecto completo en Spring Boot con Kotlin, apoyándonos en el ecosistema ZenWave 360 y su Domain Model Language (ZDL).

A lo largo del recorrido mostraremos:

  • Cómo descubrir y capturar el Lenguaje Ubicuo del dominio de negocio.
  • Cómo documentarlo y estructurarlo con ZDL, un lenguaje de modelado pensado para DDD y arquitecturas API-First y Event-Driven.
  • Cómo generar automáticamente artefactos de software (código, APIs, tests, documentación) a partir de ese modelo.
  • Un caso práctico: la construcción de un servicio maestro de clientes (Customers Service) y su implementación en un proyecto Spring Boot con Kotlin.

Este enfoque busca cerrar la brecha entre la teoría de DDD y la práctica diaria, facilitando la propagación del conocimiento del dominio a lo largo de todo el ciclo de desarrollo y garantizando consistencia entre el lenguaje de negocio y el software entregado.

2. Importancia del Lenguaje Ubicuo en Domain Driven Design

Domain-Driven Design tiene ya 20 años y sigue estando más vigente que nunca. Sin embargo, debido a la gran amplitud de su propuesta, DDD no es una técnica sencilla.

No se trata de una receta cerrada, sino de un enfoque de diseño que abarca distintos niveles de abstracción:

  • DDD Estratégico: identifica dominios y subdominios, los clasifica según su impacto en la estrategia de negocio (core, supporting o generic) y define bounded contexts y sus interacciones mediante context maps.
  • DDD Táctico: desciende al modelo detallado, trabajando con entidades, agregados, value objects, eventos de dominio, servicios y repositorios.

Aunque ambos niveles interesan a perfiles diferentes, están profundamente interconectados.
Es imprescindible comprender el contexto estratégico para tomar buenas decisiones en el nivel táctico.

Por ejemplo:

  • Para un dominio core, puede ser adecuado aplicar una arquitectura clean/hexagonal, modelando entidades ricas, eventos de dominio y un modelo de persistencia desacoplado, ya que aquí descubrimos reglas de negocio complejas con impacto estratégico.
  • En cambio, para un subdominio genérico o de soporte, la simplicidad y facilidad de mantenimiento son prioritarias, y un modelo de dominio complejo puede ser incluso contraproducente.

La propuesta central de DDD es construir un Lenguaje Ubicuo que una a expertos de negocio y técnicos.
Un lenguaje común que se hable en reuniones, se plasme en diagramas, se use en el código, en las pruebas y en la documentación.

Diagrama triangular de Domain-Driven Design mostrando cómo el Lenguaje Ubicuo conecta a expertos de negocio, expertos técnicos y software con pruebas.
El Lenguaje Ubicuo une a expertos de negocio, expertos técnicos y software, manteniendo coherencia en todo el ciclo de desarrollo.

El problema es que, aunque se intente, el Lenguaje Ubicuo suele perderse durante el ciclo de desarrollo:
hay demasiados puntos de traducción (analistas, arquitectos, desarrolladores, testers…) donde el lenguaje compartido se diluye.

Por eso, además de descubrir y visibilizar el Lenguaje Ubicuo, necesitamos mecanismos automáticos que lo propaguen a lo largo de todo el proceso.

Aquí es donde aparece ZenWave Domain Model Language (ZDL):
un lenguaje compacto y expresivo que permite capturar conceptos de negocio y técnicos en un mismo formato.
ZDL sirve como Lenguaje Ubicuo y, además, puede transformarse en código ejecutable, definiciones de APIs, tests automatizados, documentación y glosarios de negocio.

De esta forma, el conocimiento del dominio fluye de manera consistente desde los expertos de negocio hasta los desarrolladores, manteniendo coherencia en todo el ciclo de vida del software.

3. ZenWave Domain Model Language: Un Lenguaje de Modelado que funciona como Lenguaje Ubicuo

ZenWave Domain Model Language (ZDL) es un lenguaje de modelado diseñado para describir el contenido de servicios backend apoyándose en los principios de Domain-Driven Design (DDD), API-First y Arquitecturas Event-Driven.

ZDL se caracteriza por ser:

  • Compacto: su sintaxis permite expresar conceptos de negocio y técnicos de forma concisa.
  • Legible: tanto expertos técnicos como de negocio pueden comprenderlo.
  • Expresivo: captura comandos, eventos, entidades, agregados y políticas de negocio en un mismo lenguaje.

Gracias al ecosistema de Plugins de ZenWave SDK, un modelo definido en ZDL puede convertirse automáticamente en diferentes artefactos de software y documentación, propagando así el Lenguaje Ubicuo a través de todo el proceso de desarrollo.

3.1. Origen e inspiración

ZDL toma como base la sintaxis de JHipster Domain Language (JDL), extendiéndola más allá de la definición de entidades y relaciones típicas de aplicaciones CRUD.

Aporta capacidades adicionales como:

  • Definición de servicios, comandos y eventos de dominio.
  • Integración con especificaciones API-First: OpenAPI, AsyncAPI y Avro.
  • Soporte directo para arquitecturas Event-Driven y microservicios.

3.2. De Event Storming a ZDL

ZDL está especialmente pensado para recoger los descubrimientos de una sesión de Event Storming a nivel de diseño.

En este contexto, un Bounded Context se convierte en el espacio ideal para:

  • Documentar comandos de entrada.
  • Describir los eventos de dominio que se producen.
  • Modelar agregados, entidades y value objects.
  • Especificar reglas y políticas de negocio.

De esta manera, ZDL actúa como un formato de Lenguaje Ubicuo, a la vez comprensible para humanos y procesable por máquinas.

Diagrama que muestra cómo los comandos, agregados, eventos de dominio, políticas y sistemas externos de Event Storming se convierten en entidades, servicios y eventos en un modelo ZDL.
ZDL permite transformar directamente los descubrimientos de Event Storming en un modelo estructurado de comandos, eventos y agregados.

3.3. Beneficios clave

  • Representar comandos de entrada al bounded context (APIs REST, eventos asíncronos, disparadores temporales).
  • Documentar y propagar eventos internos y externos generados por el dominio.
  • Modelar agregados, entidades, value objects y relaciones.
  • Enlazar estos elementos con especificaciones técnicas OpenAPI, AsyncAPI o Avro.

En el repositorio oficial de ZenWave encontrarás ejemplos de modelos ZDL listos para usar, tanto en contextos de microservicios como en monolitos modulares.

4. Modelado de Dominio de Negocio con ZenWave 360º

Un buen modelo es aquel que permite acomodar cambios en la dirección y perspectiva de los expertos de negocio.

El enfoque de Modelado de Dominio de Negocio con ZenWave 360º se basa en tres pilares fundamentales:

  1. Comprender el modelo mental de los expertos de negocio.
  2. Usar el Lenguaje de Modelado ZDL para capturar ese modelo mental en un Lenguaje Ubicuo.
  3. Transformar el modelo ZDL con ZenWave SDK en diferentes artefactos de software que aceleren el desarrollo.
Diagrama del ciclo de retroalimentación de Domain-Driven Design con ZenWave 360º, mostrando cómo los modelos ZDL se transforman en artefactos de software mediante ZenWave SDK.
El enfoque de ZenWave 360º convierte el modelo mental de negocio en un Lenguaje Ubicuo con ZDL y lo transforma en software ejecutable mediante ZenWave SDK.

4.1. Comprender el Modelo Mental de los expertos de negocio

DDD pone el foco en entender el dominio de negocio antes de escribir código.
Sin esta comprensión, ninguna cantidad de patrones técnicos (Código Limpio, Arquitectura Hexagonal, etc.) garantizará un software mantenible y de calidad.

Una técnica clave para ello es Event Storming, ya que:

  • Tiene baja barrera de entrada para los expertos de negocio.
  • Es ideal para arquitecturas orientadas a eventos.
  • Facilita descubrir procesos, interacciones y reglas clave del dominio.
Diagrama de Event Storming mostrando cómo los comandos generan eventos de dominio que alimentan modelos de lectura, interfaces de usuario y políticas en un sistema basado en DDD.
Event Storming facilita descubrir comandos, eventos y políticas del dominio, conectando la visión de negocio con la arquitectura técnica.

El lenguaje ZDL permite documentar el modelo mental de los expertos de negocio de forma estructurada, reteniendo su vocabulario.
Con él, los desarrolladores pueden narrar de vuelta la historia del dominio a los expertos en su propio lenguaje, validando que el modelo es correcto.

4.2. Convertir el Lenguaje Ubicuo en diferentes Artefactos de Software con ZenWave SDK

La mejor manera de validar un modelo conceptual es implementarlo.

ZenWave SDK facilita convertir los modelos ZDL en software funcional y pruebas automatizadas.
Esto permite:

  • Validar rápidamente las ideas con retroalimentación temprana.
  • Propagar el Lenguaje Ubicuo del ZDL a código, APIs, tests, documentación y diagramas.
  • Reducir el riesgo de pérdida de conocimiento entre negocio y desarrollo.
Diagrama del ciclo de retroalimentación rápida mostrando cómo ZenWave SDK convierte modelos ZDL en código, APIs y pruebas automatizadas para validar el dominio.
ZenWave SDK acelera la validación de modelos conceptuales al convertir ZDL en software funcional y pruebas, cerrando el ciclo de retroalimentación entre negocio y tecnología.

El software generado se convierte en el medio más efectivo para evaluar la validez del modelo conceptual y refinarlo de manera iterativa, alineando negocio y tecnología.

5. ZenWave 360: Inicio Rápido para los más Impacientes

Esta sección está pensada para quienes quieren probar ZenWave 360 de extremo a extremo sin rodeos, generando un proyecto completo de Spring Boot a partir de un modelo ZDL en cuestión de minutos.

En las siguientes secciones entraremos en detalle sobre el modelado del dominio, pero aquí encontrarás una guía directa para empezar.

5.1. Pasos para la generacion rapida

  1. Clona el proyecto base
    Descarga el proyecto base.
    También puedes iniciar desde Spring Initializr o desde un arquetipo corporativo propio, ya que ZenWave genera únicamente código de negocio y no interfiere con la estructura base.

    Nota: en este caso deberás configurar manualmente el OpenAPI Maven Generator y el ZenWave SDK Maven Plugin para AsyncAPI.

  2. Instala las herramientas necesarias
    Sigue las instrucciones de Getting Started para instalar:

    • ZenWave SDK CLI
    • ZenWave Domain Model Editor for ZDL (plugin para IntelliJ)
  3. Ejecuta los plugins
    En IntelliJ, abre el archivo zenwave-scripts.zw y ejecuta cada plugin individualmente con el botón verde de Play en el margen izquierdo.

  4. Plugins que se ejecutaran

    • ZDLToOpenAPIPlugin: genera una definicion OpenAPI a partir del modelo ZDL.
    • ZDLToAsyncAPIPlugin: genera una definicion AsyncAPI v3 a partir del modelo ZDL.
    • BackendApplicationDefaultPlugin: genera el core de la aplicacion Spring Boot (sin controladores ni adaptadores).
    • OpenAPIControllersPlugin: genera los controladores REST.
    • SpringWebTestClientPlugin: genera pruebas de integracion para los controladores.
    • OpenAPIKaratePlugin: genera pruebas de aceptacion basadas en KarateDSL.
Captura de pantalla del editor de ZenWave Domain Model Language en IntelliJ, mostrando un archivo ZDL con entidades, glosario de campos, validaciones y un diagrama de agregados.
El editor de ZDL en IntelliJ facilita el modelado de dominios con glosarios y diagramas visuales generados automáticamente.
Ver fichero

Text
zenwave-scripts.zw

(click para expandir)

Java
// This is a ZenWave Scripts File (.zw)
config {

 zdlFile "zenwave-model.zdl"

 plugins {
   ZDLToOpenAPIPlugin {
     idType integer
     idTypeFormat int64
     targetFile "src/main/resources/public/apis/openapi.yml"
   }
   ZDLToAsyncAPIPlugin {
     asyncapiVersion v3
     idType integer
     idTypeFormat int64
     targetFile "src/main/resources/public/apis/asyncapi.yml"
   }
   BackendApplicationDefaultPlugin {
     useLombok true
     includeEmitEventsImplementation true
   }
   OpenAPIControllersPlugin {
     openapiFile "src/main/resources/public/apis/openapi.yml"
   }
   SpringWebTestClientPlugin {
     openapiFile "src/main/resources/public/apis/openapi.yml"
   }
   OpenAPIKaratePlugin {
     openapiFile "src/main/resources/public/apis/openapi.yml"
   }
 }
}

5.2. Revision manual necesaria

Aunque el proyecto generado es completamente funcional, algunas areas requieren tu atencion:

  • Mappers de MapStruct → verificar conversiones complejas.
  • Tests automatizados → completar la logica y datos de prueba.
  • Implementacion de servicios → adaptar a la logica de negocio real (mas alla de CRUD).

5.3. Ventajas clave

  • Genera solo codigo de negocio limpio, sin dependencias exoticas.
  • Compatible con cualquier starter o arquetipo corporativo.
  • Integra tecnologias estandar: Spring Boot, Spring Cloud, MapStruct, Lombok.
  • Permite tener un proyecto completo en minutos:
    • API-First con OpenAPI y AsyncAPI
    • Arquitectura por capas o Clean/Hexagonal
    • Tests unitarios, de integracion y de aceptacion listos para extender

6. Caso Práctico: Customers Service – Del Modelo ZDL al Proyecto Spring-Boot

Ahora que has experimentado las capacidades de ZenWave 360, vamos a modelar paso a paso un servicio sencillo para la gestión de un Maestro de Clientes.

Desde el punto de vista estratégico, se trata de un subdominio genérico o de soporte: no aporta valor competitivo directo a la empresa, pero resulta crucial para el funcionamiento de otros subdominios más críticos.

Este servicio maestro de clientes gestionará:

  • La creación, actualización y eliminación de clientes.
  • La notificación de cambios de estado a otros servicios interesados mediante colas de mensajería.

Aunque se trate de un subdominio genérico, es fundamental modelarlo utilizando el lenguaje de los expertos de negocio que interactuarán con él.
Por ejemplo, si los expertos se refieren a los clientes como “Customer” en lugar de “Client” o “Prospect”, debemos adoptar ese término exacto en el modelo para evitar ambigüedades y asegurar que el lenguaje ubicuo permee todo el sistema.

En este caso práctico, y basándonos en el modelo mental de los expertos, utilizaremos Customer como entidad principal con sus atributos clave.

Aunque es un subdominio de soporte, es fundamental modelarlo usando el lenguaje de los expertos de negocio.
Por ejemplo, si los expertos dicen Customer y no Client o Prospect, debemos reflejar exactamente ese termino en el modelo.

6.1. Creando un ZDL para nuestro Customers Service

Comenzaremos creando un archivo con extensión .zdl para nuestro modelo.

Los archivos ZDL suelen comenzar con:

  1. Una sección de documentación global, que describe el propósito del modelo.
  2. Una sección de configuración, donde se definen propiedades globales utilizadas por todos los plugins de ZenWave SDK.

Estas dos secciones son opcionales, pero si se incluyen, deben aparecer juntas al inicio del archivo ZDL.

Java
/**
 * Servicio Maestro de Clientes.
 * Este modelo describe un servicio maestro de clientes que gestiona los datos de los clientes...
 */

config {
    title "Servicio Maestro de Clientes"
    basePackage "io.zenwave360.example"
    persistence jpa
    databaseType postgresql

    // ...
}

6.2. Modelando Comandos y Eventos de Dominio

Existen múltiples enfoques para comenzar a modelar un servicio: desde el modelo interno, los comandos de entrada, la API REST o los eventos publicados.

En este caso, comenzaremos modelando los comandos de entrada y los eventos de dominio que forman parte de este bounded context, es decir, su interfaz pública.

Agruparemos los comandos en un servicio y documentaremos a través de qué APIs dicho servicio expone sus comandos y eventos.

En ZDL, cada servicio gestiona siempre uno o varios agregados.
Como aún no hemos definido ninguno, crearemos una entidad vacía Customer y la utilizaremos como raíz del agregado.

Creamos lo siguiente:

  • Un servicio CustomerService para gestionar los clientes (el agregado Customer).
  • Un comando de entrada enrollCustomer que:

    • Recibe los datos de entrada de un CustomerInput.
    • Devuelve el agregado recién creado Customer.
    • Emite un evento de dominio CustomerEnrolled.

Nota: Los comandos también pueden recibir como parámetro de entrada la propia entidad agregada, en este caso Customer.

Java
@aggregate
entity Customer { }

/** ReadModel: contains the information needed to create a customer */
input CustomerInput {
    /** Customer name */
    name String required maxlength(254)

    email String required maxlength(254) pattern(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/)
}

service CustomerService for (Customer) {
    /** Enroll Customer Command: creates a new customer */
    enrollCustomer(CustomerInput) Customer withEvents CustomerEnrolled
}

event CustomerEnrolled {
    customerId String
    customer Customer
}

6.3. Conectando el Servicio con el Exterior vía APIs

Ahora vamos a documentar cómo nuestro CustomerService se comunicará con el mundo exterior a través de APIs REST y asíncronas.

Para ello utilizaremos ciertas anotaciones (decoradores) que permiten describir la forma en que el servicio expone sus comandos y eventos:

Java
@rest("/customers")
service CustomerService for (Customer) {
  @post
  enrollCustomer(CustomerInput) Customer withEvents CustomerEnrolled
}

@asyncapi({ channel: "CustomerEvents", topic: "customer.events" })
event CustomerEnrolled {
  //...
}

Estas anotaciones no solo documentan cómo el servicio se comunica con el exterior, sino que además, gracias a los plugins de ZenWave SDK (ZDLToOpenAPIPlugin y ZDLToAsyncAPIPlugin), permiten generar automáticamente una versión preliminar de dichas APIs.

El formato ZDL ha evolucionado como un lenguaje IDL capaz de definir, de manera casi completa, tanto APIs de tipo OpenAPI como AsyncAPI:

6.4. Convertir ZDL a OpenAPI

6.3.1. Convertir ZDL a OpenAPI

La sintaxis heredada de JHipster JDL permite definir el payload de las peticiones y respuestas de manera compacta:

  • Nombre de los campos: utilizando el lenguaje ubicuo del dominio.
  • Tipos de datos: String, Integer, Long, Boolean, etc.
  • Validaciones: required, maxlength, pattern, min, max, etc.
  • Relaciones y entidades anidadas: para estructuras de datos complejas.

Para definir los distintos endpoints utilizamos las siguientes anotaciones en los servicios y comandos:

Configuración del servicio:

  • @rest("/<path>"): define la ruta base del servicio.

Métodos HTTP:

  • @post, @get, @put, @delete, @patch: especifican el método HTTP para cada comando.

Configuración avanzada de endpoints:

  • @put("/{customerId}"): define rutas con parámetros en el path.
  • @post({ path: "/search", params: {search: String}, status: 200 }):

    • Permite incluir query params con su tipo correspondiente.
    • Soporta códigos de respuesta personalizados (por ejemplo, un POST por defecto genera 201, pero en el caso de una búsqueda lo más adecuado es 200).
  • @paginated: indica que el endpoint devuelve resultados paginados.
  • @filedownload("documentData.data"): define un endpoint de descarga binaria de archivos, donde documentData.data es el campo que contiene los datos.
  • @fileupload("myfile"): define un endpoint multipart con el campo myfile de tipo binario para la subida de archivos.

El siguiente ejemplo muestra un servicio completo con operaciones CRUD:

Java
@rest("/customers")
service CustomerService for (Customer) {
  @post
  createCustomer(Customer) Customer withEvents CustomerEvent

  @get("/{id}")
  getCustomer(id) Customer?

  @put("/{id}")
  updateCustomer(id, Customer) Customer? withEvents CustomerEvent

  @delete("/{id}")
  deleteCustomer(id) withEvents CustomerEvent

  @post({ path: "/search", status: 200 }) @paginated
  searchCustomers(CustomerSearchCriteria) Customer[]
}

El siguiente modelo ZDL, al utilizar el plugin ZDLToOpenAPIPlugin, generaría esta definición OpenAPI.

Para más detalles sobre cómo definir APIs REST en ZDL, consulta la documentación: Exponiendo una API REST.

6.5. Convertir ZDL a AsyncAPI

Definir APIs AsyncAPI con ZDL es igualmente sencillo y directo.

El proceso consiste en crear entidades de tipo event que siguen las mismas reglas de sintaxis que las entidades entity: nombres de campos, tipos de datos, validaciones, documentación, anotaciones y entidades anidadas.

Una vez definidos los eventos, los anotamos con @asyncapi para especificar:

  • channel: Nombre del canal donde se publican los eventos. Múltiples eventos pueden compartir el mismo canal, especialmente útil para eventos de tipo AVRO
  • topic: Nombre del tópico específico para la publicación. En AsyncAPI v3 corresponde al campo address de la definición del canal

Ejemplo práctico:

Java
@copy(Customer)
@asyncapi({ channel: "CustomersChannel", topic: "customers" })
event CustomerEvent {
    id Long required
    version Integer
    // Todos los campos de Customer se copian aquí, excepto las relaciones
    paymentMethods PaymentMethod[]
}

El mismo modelo ZDL, al utilizar el plugin ZDLToAsyncAPIPlugin, generaría una definición completa de AsyncAPI que incluye todos los esquemas de eventos y las configuraciones de canales correspondientes.

6.6. Modelando el Agregado de Clientes

Ahora definiremos la estructura interna del agregado Customer, aplicando los principios de Domain-Driven Design (DDD):

  • Lenguaje ubicuo: utilizamos la estructura, los nombres de campos y las descripciones (javadoc) para capturar y documentar el vocabulario específico del dominio.
  • Agregado: marcamos la entidad Customer con @aggregate para indicar que es la raíz del agregado.
    Por simplicidad, en este ejemplo empleamos un agregado centrado en datos, aunque podríamos modelar un agregado más rico que incorpore comportamiento específico del dominio.
Java
/**
* Customer entity: represents a customer in our system with their basic information and contact details
*/

@aggregate
entity Customer {
    /** Customer name */
    name String required maxlength(254)
    /** Customer familiar name is how we refer to the customer in a friendly way */
    familiarName String required maxlength(254)
    /** Customer email is used to contact the customer */
    email String required maxlength(254) pattern(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/)

    /** Customer can have multiple addresses, but at least one is required and max 5 */
    @json addresses Address[] minlength(1) maxlength(5) {
        street String required maxlength(254)
        city String required maxlength(254)
    }
}

/** PaymentMethod entity: represents a payment method associated with a customer */
entity PaymentMethod {
    type PaymentMethodType required
    cardNumber String required
}

enum PaymentMethodType { VISA(1), MASTERCARD(2) }

relationship OneToMany {
    /** Customer has multiple payment methods, but at least one is required and max 3 */
    Customer{paymentMethods required maxlength(3)} to PaymentMethod{customer required}
}

6.6.1 Sobre Modelos Relacionales y/o Documentales

ZDL soporta tanto el diseño de modelos relacionales (heredado de JHipster JDL) como de modelos documentales, utilizando entidades anidadas para lograr mayor expresividad y eficiencia.

Ejemplos de entidades anidadas:

Los dos siguientes esquemas son equivalentes:

Java
@aggregate
entity Customer {
  @json addresses Address[] {
    street String
  }
}
Java
@aggregate
entity Customer {
  address Address
}
@embedded
entity Address {
  street String
}
Java
@aggregate
entity Customer {
  address Address {
    street String
  }
}

Este esquema muestra una colección de entidades anidadas que reutiliza la definición anterior, ya que en ZDL cada entidad debe tener un nombre único.

Java
@aggregate
entity Customer {
  address Address {
    street String
  }
  manyAddresses Address[]
}

6.8. Fichero de Scripts ZenWave SDK

El archivo .zw define la configuración y plugins a ejecutar.

Ejemplo:

Java
config {
  zdlFile "zenwave-model.zdl"

  plugins {
    ZDLToOpenAPIPlugin {
      idType integer
      idTypeFormat int64
      targetFile "src/main/resources/public/apis/openapi.yml"
    }
    ZDLToAsyncAPIPlugin {
      asyncapiVersion v3
      targetFile "src/main/resources/public/apis/asyncapi.yml"
    }
    BackendApplicationDefaultPlugin {
      includeEmitEventsImplementation true
    }
  }
}

Ventajas:

  • Configuración compartida en equipo.
  • Ejecución con un clic desde IntelliJ.
  • Reproducibilidad y trazabilidad.
Captura de pantalla del editor IntelliJ mostrando la ejecución de plugins de ZenWave SDK mediante un archivo .zw, con configuraciones de OpenAPI y Backend Application.
El editor de ZDL en IntelliJ permite ejecutar plugins de ZenWave SDK directamente desde archivos .zw, simplificando la configuración y garantizando reproducibilidad.

7. Customizacion del Codigo Generado

ZenWave SDK surge de la necesidad personal de contar con una herramienta de análisis y diseño capaz de generar proyectos —inicialmente en Java y Spring Boot— con la suficiente flexibilidad para adaptarse a las necesidades de cada proyecto, equipo y cliente.

7.1. Tipos de Customizacion Disponibles

ZenWave SDK ofrece diferentes niveles de customización que se ajustan a distintos escenarios y grados de complejidad:

  • Customización Básica

    • Layouts de arquitectura: elige entre diferentes estilos arquitectónicos (Layered, Clean/Hexagonal, Simple packaging, etc.).
    • Configuración de paquetes: personaliza la estructura de los packages generados.
    • Monolitos modulares: configura múltiples módulos compartiendo clases base comunes.
  • Customización Intermedia

    • Plantillas personalizadas: modifica plantillas específicas para pequeños ajustes (anotaciones, starters propios, etc.).
  • Customización Avanzada

    • Extensión del classpath: añade dependencias y plugins personalizados vía JBang.
    • Customización total: ejemplo de sustitución completa, reemplazando Java por Kotlin.
    • Creación de plugins: desarrolla plugins completamente nuevos para casos específicos.

7.2. Customizacion Basica

7.2.1. Layouts de Arquitectura

Podemos elegir el layout arquitectónico más adecuado al proyecto:

Java
config {
  title "Mi Aplicacion Backend"
  basePackage "io.zenwave360.example"
  persistence jpa
  databaseType postgresql

  // Layouts disponibles:
  // DefaultProjectLayout, LayeredProjectLayout, SimpleDomainProjectLayout
  // CleanHexagonalProjectLayout, HexagonalProjectLayout, CleanArchitectureProjectLayout
  layout CleanHexagonalProjectLayout
}

Ejemplos de layouts en el repositorio de ZenWave SDK:

7.2.2. Configuracion de Paquetes

Puedes personalizar cualquier ruta de paquete en el layout elegido:

Java
config {
  layout CleanHexagonalProjectLayout

  // Sobrescribir paquetes concretos
  layout.entitiesPackage "{{basePackage}}.core.model"
  layout.openApiApiPackage "{{basePackage}}.web"
  layout.openApiModelPackage "{{basePackage}}.web.dtos"
}

7.2.3. Monolitos Modulares

En un principio, ZenWave SDK fue diseñado para generar microservicios, aunque nada impide utilizarlo también para generar módulos dentro de un monolito.

La principal particularidad a la hora de construir monolitos modulares es la necesidad de compartir clases base entre los distintos módulos, con el fin de evitar la duplicación de código.

Por lo tanto, es necesario configurar el nombre del paquete base del módulo, que será el paquete raíz para dicho módulo (equivalente a basePackage en microservicios).

Java
// Configuración especifica del módulo
config {
  title "Modulo de Clientes"
  layout CleanHexagonalProjectLayout
  layout.moduleBasePackage "io.zenwave360.example.modules.customers"
}

Y además configurar los paquetes base comunes para compartir clases entre módulos, ya que probablemente se encuentren a un nivel superior al de los propios módulos.

Java
// Configuración común para todos los módulos (fichero.zw)
config {
    title "Monolito Modular"
    basePackage "io.zenwave360.example"

    // Paquetes compartidos entre módulos
    layout.commonPackage "{{basePackage}}.common"
    layout.infrastructureRepositoryCommonPackage "{{commonPackage}}"
    layout.adaptersWebMappersCommonPackage "{{commonPackage}}.mappers"
}

7.3. Customizacion Intermedia

7.3.1. Plantillas Personalizadas

Para realizar modificaciones específicas sin necesidad de crear un plugin completo, puedes sobrescribir plantillas individuales.

Proceso:

  1. Localiza la plantilla original en el repositorio de ZenWave SDK.
  2. Copia la plantilla a la ruta .zenwave/templates/[ruta-original] dentro de tu proyecto.
  3. Modifica el contenido según tus necesidades.

Ejemplo: personalizar repositorios de Spring Data para que extiendan una clase base de un starter propio:

Bash
.zenwave/templates/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/
 src/main/java/core/outbound/jpa/imperative/EntityRepository.java.hbs

Limitaciones: este enfoque permite modificar plantillas, pero no permite añadir nuevos helpers ni cambiar la lógica interna de generación.

7.4. Customizacion Avanzada

7.4.1. Extension del Classpath con JBang

Para customizaciones avanzadas que requieren nuevas dependencias o plugins, puedes utilizar la magia de JBang.
Por ejemplo, creando un archivo jbang-catalog.json en la raíz de tu proyecto, es posible sobrescribir el classpath de ZenWave SDK cuando se invoque desde ese directorio.

Text
{
  "catalogs": {},
  "aliases": {
    "zw": {
      "script-ref": "io.zenwave360.sdk:zenwave-sdk-cli:RELEASE",
      "dependencies": [
        "org.slf4j:slf4j-simple:1.7.36",
        "<tu-dependencia-personalizada-en-formato-maven>",
        // ... dependencias estándar de ZenWave
      ],
      "main": "io.zenwave360.sdk.Main"
    }
  }
}

Nota importante: las dependencias añadidas al inicio tendrán prioridad y sobrescribirán las clases incluidas en ZenWave SDK.

7.4.2. Customizacion total: Ejemplo de sustitucion completa reemplazando Java por Kotlin

ZenWave SDK permite una customización total.
De hecho, la propia distribución incluye una implementación que reemplaza completamente Java por Kotlin.

Puedes consultar el código fuente de la customización en Kotlin para ver cómo está implementada.

En términos prácticos, para utilizar la customización en Kotlin, basta con configurar la opción templates en aquellos plugins que lo soporten, asignándole la clase correspondiente:

Java
config {
  zdlFile "zenwave-model.zdl"

  plugins {
    BackendApplicationDefaultPlugin {
      templates "new io.zenwave360.sdk.plugins.kotlin.BackendApplicationKotlinTemplates()"
    }
    OpenAPIControllersPlugin {
      openapiFile "src/main/resources/public/apis/openapi.yml"
      templates "new io.zenwave360.sdk.plugins.kotlin.OpenAPIControllersKotlinTemplates()"
    }
    SpringWebTestClientPlugin {
      openapiFile "src/main/resources/public/apis/openapi.yml"
      templates "new io.zenwave360.sdk.plugins.kotlin.SpringWebTestClientKotlinTemplates()"
    }
  }
}

7.4.3. Creacion de Plugins Personalizados

Para necesidades específicas, puedes crear plugins completamente nuevos que generen cualquier tipo de artefacto: código, APIs, documentación, tests, etc.

Herramientas disponibles:

  • Parser de ZDL.
  • Parser de OpenAPI/AsyncAPI con resolución de referencias.
  • Formatters de código: Java (Google, Palantir, Spring) y Kotlin (ktfmt).
  • Motor de plantillas Handlebars.

Puedes basarte en cualquiera de los plugins existentes y utilizar el plugin ZdlToJsonPlugin para inspeccionar la estructura del modelo ZDL y comprender las transformaciones necesarias para alcanzar tu objetivo.

Bash
jbang zw -p ZdlToJsonPlugin zdlFile=zenwave-model.zdl

8. Proyecto Completo de Spring-Boot con Kotlin

Como demostración práctica de lo explicado, hemos desarrollado un proyecto completo que implementa los principios de DDD utilizando ZenWave SDK:

📌 Repositorio de ejemplo: Kustomer with Address and Payment Methods in JPA

8.1. Stack Tecnologico

Hemos elegido un stack tecnológico sencillo pero completo, que puede servir como base sólida para cualquier microservicio empresarial moderno:

  • Spring Boot 3.5.x
  • Hibernate / Spring Data JPA
  • MapStruct
  • OpenAPI Generator
  • AsyncAPI Generator (ZenWave SDK)
  • Avro + Avro Compiler
  • Spring Cloud Streams
  • Spring Security
  • TestContainers con Docker Compose

8.2. Funcionalidad y Modelo de Dominio

El proyecto implementa la gestión completa del aggregate Customer con las siguientes características:

8.2.1. Entidades y Value Objects

  • Customer → raíz del agregado.
  • Address → colección de direcciones persistida como JSON en la base de datos.
  • PaymentMethod → relación OneToMany gestionada por JPA/Hibernate.
  • PaymentMethodType → enumerado persistido como entero con conversor personalizado.

8.2.2. Operaciones Disponibles

  • CRUD completo vía API REST.
  • 🔍 Búsqueda paginada con filtros.
  • 📡 Eventos de dominio publicados en Kafka para cada operación.
  • 🔐 Autenticación con Spring Security.

8.3. Modelos de ZenWave SDK

El proyecto se construyó utilizando los siguientes modelos de ZenWave SDK:

Ver Modelo ZDL (click para expandir)
Java
/**
 * Sample ZenWave Model Definition.
 * Use zenwave-scripts.zdl to generate your code from this model definition.
 */

config {
    title "Project Name"
    basePackage "io.zenwave360.examples.kotlin"
    persistence jpa
    databaseType postgresql

    // you can choose: DefaultProjectLayout, CleanHexagonalProjectLayout, LayeredProjectLayout, SimpleDomainProjectLayout, HexagonalProjectLayout, CleanArchitectureProjectLayout
    layout LayeredProjectLayout

    // these should match the values of openapi-generator-maven-plugin
    // used by the OpenAPIControllersPlugin and SpringWebTestClientPlugin
    layout.openApiApiPackage "{{basePackage}}.web"
    layout.openApiModelPackage "{{basePackage}}.web.dtos"
    openApiModelNameSuffix DTO
}

/**
* Customer entity
*/

@aggregate
@auditing // adds auditing fields to the entity
entity 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)
    }
}

@auditing
entity PaymentMethod {
    type PaymentMethodType required
    cardNumber String required
}

enum PaymentMethodType { VISA(1), MASTERCARD(2) }

relationship OneToMany {
    @eager
    Customer{paymentMethods required maxlength(3)} to PaymentMethod{customer required}
}

// you can create 'inputs' as dtos for your service methods, or use entities directly
input CustomerSearchCriteria {
    name String
    email String
    city String
    state String
}

@rest("/customers")
service CustomerService for (Customer) {
    @post
    createCustomer(Customer) Customer withEvents CustomerEvent
    @get("/{customerId}")
    getCustomer(id) Customer?
    @put("/{customerId}")
    updateCustomer(id, Customer) Customer? withEvents CustomerEvent
    @delete("/{customerId}")
    deleteCustomer(id) withEvents CustomerEvent
    @post({path: "/search", status: 200}) @paginated
    searchCustomers(CustomerSearchCriteria) Customer[]
}

@copy(Customer)
@asyncapi({ channel: "CustomersChannel", topic: "customers" })
event CustomerEvent {
    id Long
    version Integer
    // all fields from Customer are copied here, but not relationships
    paymentMethods PaymentMethod[]
}
Ver Scripts ZDL (click para expandir)
Java
@import("io.zenwave360.sdk.plugins.customizations:kotlin-backend-application:2.1.0-SNAPSHOT")

config {

    zdlFile "zenwave-model.zdl"

    plugins {

        ZDLToOpenAPIPlugin {
            idType integer
            idTypeFormat int64
            targetFile "src/main/resources/public/apis/openapi.yml"
        }

        ZDLToAsyncAPIPlugin {
            asyncapiVersion v3
            idType integer
            idTypeFormat int64
            targetFile "src/main/resources/public/apis/asyncapi.yml"
            // includeKafkaCommonHeaders true
        }

        BackendApplicationDefaultPlugin {
            templates "new io.zenwave360.sdk.plugins.kotlin.BackendApplicationKotlinTemplates()"
            includeEmitEventsImplementation true
            haltOnFailFormatting false
            --force // overwite all files
        }

        OpenAPIControllersPlugin {
            openapiFile "src/main/resources/public/apis/openapi.yml"
            templates "new io.zenwave360.sdk.plugins.kotlin.OpenAPIControllersKotlinTemplates()"
        }

        SpringWebTestClientPlugin {
            openapiFile "src/main/resources/public/apis/openapi.yml"
            templates "new io.zenwave360.sdk.plugins.kotlin.SpringWebTestClientKotlinTemplates()"
        }

        SpringWebTestClientPlugin {
            openapiFile "src/main/resources/public/apis/openapi.yml"
            templates "new io.zenwave360.sdk.plugins.kotlin.SpringWebTestClientKotlinTemplates()"
            groupBy businessFlow
            businessFlowTestName CreateUpdateDeleteCustomerIntegrationTest
            operationIds createCustomer,updateCustomer,deleteCustomer,getCustomer
        }

        OpenAPIKaratePlugin {
            openapiFile "src/main/resources/public/apis/openapi.yml"
        }

        OpenAPIKaratePlugin {
            openapiFile "src/main/resources/public/apis/openapi.yml"
            groupBy businessFlow
            businessFlowTestName CreateUpdateDeleteCustomerKarateTest
            operationIds createCustomer,updateCustomer,deleteCustomer,getCustomer
        }
    }
}

 
O visita los ficheros directamente:

8.4. Ejecucion del Proyecto

Sigue las instrucciones del README.md para ejecutar el proyecto:

Text
# Levantar infraestructura (Kafka, PostgreSQL)
docker-compose up -d

# Ejecutar la aplicación
mvn spring-boot:run -Dspring-boot.run.profiles=local

Puedes acceder a la funcionalidad a través de:

Conclusión

En este artículo exploramos cómo ZenWave 360º cierra la brecha entre la teoría de Domain-Driven Design (DDD) y la implementación práctica. Demostramos cómo ZenWave Domain Language (ZDL) funciona como un Lenguaje Ubícuo efectivo, capaz de capturar el modelo mental de los expertos de negocio y propagarlo automáticamente a lo largo de todo el proceso de desarrollo.

Puntos clave:

  • De Event Storming a código: ZDL mapea de forma efectiva los descubrimientos de Event Storming en un formato estructurado que preserva el lenguaje de negocio y permite la generación automática de código.
  • Lenguaje Ubícuo en la práctica: A diferencia de las implementaciones tradicionales de DDD, donde el lenguaje común a menudo se pierde en la traducción, ZDL asegura consistencia desde las conversaciones de negocio hasta el código ejecutable.
  • Generación completa de proyectos: Con ZenWave SDK es posible generar un proyecto Spring Boot con Kotlin completamente funcional, que incluye arquitectura limpia, APIs REST, eventos de dominio y pruebas exhaustivas.
  • Flexibilidad de customización: El framework ofrece múltiples niveles de personalización, desde simples sobreescrituras de plantillas hasta transformaciones arquitectónicas completas.

La ventaja de ZenWave 360º radica en su capacidad para mantener la coherencia del conocimiento del dominio a lo largo de todo el ciclo de vida del desarrollo. Los expertos de negocio pueden reconocer su lenguaje en el código generado, los arquitectos pueden garantizar una implementación adecuada de DDD y los desarrolladores pueden enfocarse en la lógica de negocio en lugar de en la infraestructura repetitiva.

Al combinar el pensamiento estratégico de DDD con la implementación táctica a través de la generación automática de código, ZenWave 360º permite a los equipos construir software que refleja fielmente el dominio de negocio, manteniendo al mismo tiempo una alta calidad de código y consistencia arquitectónica.

¿Listo para comenzar? Visita www.zenwave360.io y explora el proyecto completo de ejemplo en Kotlin para comprobar cómo ZenWave 360º puede acelerar tu próximo proyecto DDD.

Comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

He leído y acepto la política de privacidad

Información básica acerca de la protección de datos

  • Responsable: IZERTIS S.A.
  • Finalidad: Envío información de carácter administrativa, técnica, organizativa y/o comercial sobre los productos y servicios sobre los que se nos consulta.
  • Legitimación: Consentimiento del interesado
  • Destinatarios: Otras empresas del Grupo IZERTIS. Encargados del tratamiento.
  • Derechos: Acceso, rectificación, supresión, cancelación, limitación y portabilidad de los datos.
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad

Iván García Sáinz es un Arquitecto de Software con amplia experiencia en tecnologías como Java, Spring Boot y Spring Cloud. Especializado en Domain-Driven Design (DDD) y arquitecturas dirigidas por eventos, Iván se dedica a crear soluciones de software que destacan por su claridad y eficiencia. Además de su labor profesional, Iván forma parte del Comité Técnico de Dirección de la especificación AsyncAPI, contribuyendo activamente al desarrollo y promoción de estándares para APIs asíncronas. Es el fundador de ZenWave 360º, una plataforma orientada a facilitar la adopción de enfoques API-First mediante herramientas innovadoras como el ZenWave SDK.

¿Quieres publicar en Adictos al trabajo?

Te puede interesar

02/03/2026

José Antonio Sánchez Segovia

Zephyr es un RTOS open source respaldado por la Linux Foundation que permite desarrollar dispositivos embebidos conectados, eficientes y escalables, facilitando el paso de prototipo a producto final con una arquitectura mantenible.

23/02/2026

Enrique Casado Díez

LoRa y LoRaWAN son tecnologías clave en el ecosistema IoT cuando se requiere largo alcance y bajo consumo energético. En este artículo analizamos su funcionamiento, Spreading Factor, link budget, arquitectura de red, frecuencias y clases de dispositivos, con un caso práctico real.

19/02/2026

Juan José Díaz Antuña

Copilot Chat es la forma más sencilla y segura de empezar a usar IA en Microsoft 365. En este artículo vemos cómo funciona, cómo activarlo y en qué se diferencia de Microsoft 365 Copilot, Copilot Studio y los Agentes Inteligentes, con ejemplos prácticos y una comparativa clara.