Customizing Generated Code
ZenWaveSDK adapts to your team's coding style, preferences and custom libraries.
ZenWave SDK arose from a personal need to have an analysis and design tool that could generate projects, initially Java and Spring-Boot, with sufficient flexibility to adapt to the needs of each project, team, and client.
By default, ZenWaveSDK generates a complete SpringBoot backend application following industry best practices, with a minimal dependency set: Spring-Data JPA and MongoDB, SpringMVC, SpringCloudStreams, MapStruct and optional Lombok integration.
You can customize the generated code in several ways:
Basic Customization:
- Architecture layouts: Choose between different architectural styles (layers, clean/hexagonal, simple packaging)
- Package configuration: Customize the structure of generated packages
- Modular monoliths: Configure multiple modules sharing common base classes
Intermediate Customization:
- Custom templates: Modify specific templates for small adjustments (annotations, custom starters, etc.)
Advanced Customization:
- Classpath extension: Add custom dependencies and plugins via JBang
- Total customization: Example of complete substitution replacing Java with Kotlin
- Plugin creation: Develop completely new plugins for specific cases
NOTE: The less customization you need, the less effort you'll spend creating and maintaining it.
Configuring different Project Layouts
By default, ZenWave SDK generates a Spring Boot/Spring Cloud backend application using a flexible hexagonal/clean architecture approach. However, the toolkit offers several alternative layouts to match your preferred architectural style:
config {title "My Backend Application"basePackage "com.example.my.backend"// Available layout options:// - DefaultProjectLayout// - CleanHexagonalProjectLayout// - LayeredProjectLayout// - SimpleDomainProjectLayout// - HexagonalProjectLayout// - CleanArchitectureProjectLayoutlayout CleanHexagonalProjectLayout}
You can customize any package path in your chosen layout through properties in the config section or as parameters in the CLI plugin:
config {//...layout CleanHexagonalProjectLayoutlayout.entitiesPackage "{{basePackage}}.core.model"layout.openApiApiPackage "{{basePackage}}.web"layout.openApiModelPackage "{{basePackage}}.web.dtos"}
For a complete customization, you can create your own implementation of the ProjectLayout class.
Tip: You can organize your application into multiple modules, each using a different layout.
CleanHexagonalProjectLayout
This is the default project layout, which the source code templates are designed around. It implements a pragmatic hexagonal/clean architecture with:
- A central
core
package containing domain entities and aggregatesinbound
(primary ports) andoutbound
(secondary ports) interfaces- and the internal
implementation
ofinbound
interfaces, which uses theoutbound
interfaces to interact with external services.
Β
CleanHexagonalProjectLayout
/*** Hexagonal Architecture (also called Ports and Adapters) with a Clean separation of concerns, following Domain-Driven Design (DDD) and Event-Driven Architecture (EDA) principles.** <pre>* π¦ {{basePackage}}* π¦ adapters* ββ web* | ββ RestControllers (spring mvc)* ββ events* ββ *EventListeners (spring-cloud-streams)* π¦ core* ββ π¦ domain* | ββ (entities and aggregates)* ββ π¦ inbound* | ββ dtos/* | ββ ServiceInterface (inbound service interface)* ββ π¦ outbound* | ββ mongodb* | | ββ *RepositoryInterface (spring-data interface)* | ββ jpa* | ββ *RepositoryInterface (spring-data interface)* ββ π¦ implementation* ββ mappers/* ββ ServiceImplementation (inbound service implementation)* π¦ infrastructure* ββ mongodb* | ββ CustomRepositoryImpl (spring-data custom implementation)* ββ jpa* ββ CustomRepositoryImpl (spring-data custom implementation)* </pre>*/
Β

Notes about Generated Clean/Hexagonal Architecture
To balance pragmatism with architectural purity, this implementation makes two practical compromises:
- Domain entities directly use JPA or SpringData MongoDB annotations
- Spring Data Repository interfaces serve as outbound adapters/secondary ports, while their implementations remain outside the core domain
These design choices significantly reduce the complexity of your codebase by minimizing the number of mappers and DTOs needed, while still maintaining the key benefits of hexagonal/clean architecture - clear separation between core domain and external systems.
For testing, ZenWave SDK provides in-memory implementations of Spring Data Repository interfaces, eliminating the need for a database or mocking frameworks. This demonstrates that your core business logic remains decoupled from specific persistence technologies.
If you believe your project genuinely requires the ability to swap persistence implementations (a rare need in most applications), and you're willing to accept the additional complexity of extra mapping layers, you can create custom templates as described in the Full Source Code Customization section.
LayeredProjectLayout
This is the classical layered architecture with domain
, repository
, events
, commands
, service
and web
layers.
LayeredProjectLayout
/*** Simple domain project layout.** <pre>* π¦ {{basePackage}} # Root package* π¦ config # Spring Boot configuration, security, etc.* π¦ domain # Domain Layer (Business Entities and Events)* ββ *Entities* ββ events/* ββ *DomainEvents* π¦ repository # Repository Layer (Persistence and Data Access)* ββ {{persistence}}/* | ββ *RepositoryInterface # Persistence interface (Spring Data, etc.)* | ββ *RepositoryImpl # Repository implementation* π¦ events # Events Layer (Internal and Async API Events)* ββ *EventListeners # Event listeners* π¦ commands # Command Layer (Command Handlers)* ββ *CommandHandlers # Command handlers (e.g., CQRS commands)* π¦ service # Service Layer (Business Logic and DTOs)* ββ dtos/* | ββ *DTOs # Data Transfer Objects* ββ impl/* | ββ *ServiceImplementation # Service implementations* ββ impl/mappers/* ββ *Mappers # Object mappers for transformations* π¦ web # Web Layer (Controllers and API)* ββ *RestControllers # REST controllers (Spring MVC, etc.)* ββ mappers/* ββ *WebMappers # Mappers for web layer transformations* </pre>*/
SimpleDomainProjectLayout
This is a very simple and flat structure, with no particular layering. It's useful for small services or modules with just one entity or aggregate.
SimpleDomainProjectLayout
/*** Simple domain project layout.** <pre>* π¦ {{basePackage}}* ββ π¦ config* ββ π¦ model (entities and aggregates)* ββ π¦ dtos* ββ π¦ events* ββ π¦ mappers* ββ *EventListeners (spring-cloud-streams)* ββ *RestControllers (spring mvc)* ββ ServiceImplementation* ββ *RepositoryInterface* </pre>*/public class SimpleDomainProjectLayout extends ProjectLayout {{basePackage = "{{basePackage}}";// in case of modular projectconfigPackage = "{{basePackage}}.config";commonPackage = "{{basePackage}}"; // set to "{{basePackage}}.common" in modular projectsmodulesPackage = "{{basePackage}}.modules";// module specificmoduleBasePackage = "{{basePackage}}";moduleConfigPackage = "{{moduleBasePackage}}.config";// domain entities and eventsentitiesPackage = "{{moduleBasePackage}}.domain";
HexagonalProjectLayout
This layout follows a stricter naming convention for hexagonal architecture, with domain, ports, application core, and adapters.
HexagonalProjectLayout
/*** Hexagonal project layout.** <pre>* π¦ {{basePackage}}* π¦ domain # Domain model (Entities, Aggregates, Value Objects)* ββ *Entities* |* π¦ ports # Port interfaces* ββ inbound # Primary ports (driving adapters)* | ββ UserServicePort # Interface for business logic (input)* ββ outbound # Secondary ports (driven adapters)* ββ UserRepositoryPort # Interface for persistence (output)* |* π¦ application # Application core (business logic services)* ββ services # Service implementations* | ββ UserServiceImpl # Implements UserServicePort, uses UserRepositoryPort* ββ mappers # Optional: Mapping between entities and DTOs* |* π¦ adapters # Interface adapters (controllers, repositories, listeners)* ββ web # Web adapter (e.g., REST)* | ββ UserController # Calls UserServicePort* ββ persistence # Persistence adapters* | ββ mongodb/* | | ββ MongoUserRepository (implements UserRepositoryPort)* | ββ jpa/* | ββ JpaUserRepository (implements UserRepositoryPort)* ββ events # Event-driven adapters* ββ UserEventListener # Listens to events, calls UserServicePort* |* π¦ config # Spring Boot configurations* </pre>*/
Modular Monoliths
Initially, ZenWave SDK was designed to generate microservices, although nothing prevents using it to generate modules of a monolith.
The main peculiarity when building modular monoliths is sharing base classes between different modules to avoid code duplication.
For monolithic projects, it's necessary to configure the module's base package name, which will be the root package of all packages generated by ZenWave SDK (equivalent to basePackage
in microservices).
// Module-specific configurationconfig {title "Customer Module"layout CleanHexagonalProjectLayoutlayout.moduleBasePackage "io.zenwave360.example.modules.customers"}
And also configure the base packages to share classes between modules, as they will probably be at a higher level than the modules themselves.
// Common configuration for all modulesconfig {title "Modular Monolith"basePackage "io.zenwave360.example"// Packages shared between moduleslayout.commonPackage "{{basePackage}}.common"layout.infrastructureRepositoryCommonPackage "{{commonPackage}}"layout.adaptersWebMappersCommonPackage "{{commonPackage}}.mappers"}
See:
For two examples of modular monoliths generated with ZenWave SDK, in Java and Kotlin, respectively.
Intermediate Customization
Custom Templates
For specific modifications without creating a complete plugin, you can override individual templates:
Process:
- Locate the original template in the ZenWave SDK repository
- Copy the template to
.zenwave/templates/[original-path]
in your project root folder - Modify the content according to your needs
Example: Customize Spring Data repositories with file like this:
.zenwave/templates/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/outbound/jpa/imperative/EntityRepository.java.hbs
// .zenwave/templates/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/outbound/jpa/imperative/EntityRepository.java.hbspackage {{layout.outboundRepositoryPackage}};{{~assign 'aggregate' (findEntityAggregate entity.name)}}import {{layout.entitiesPackage}}.*;import java.math.*;import java.time.*;import java.util.*;import org.springframework.data.jpa.repository.*;import org.springframework.stereotype.Repository;/*** Spring Data JPA repository for the {{entity.className}} entity.*/@Repository@MyCustomRepositoryAnnotationpublic interface {{entity.className}}Repository extends MyCustomBaseRepository<{{entity.className}}, {{idJavaType}}> {{{~#if aggregate}}default Optional<{{aggregate}}> find{{aggregate}}ById({{idJavaType}} id) {return findById(id).map({{aggregate}}::new);}{{~/if}}{{~#if (naturalIdFields entity)}}{{{naturalIdsRepoMethodSignature entity}}};{{/if}}}
Notice @MyCustomRepositoryAnnotation
and extends MyCustomBaseRepository
added to the template.
Limitations: This approach allows modifying templates but not adding new helpers or changing generation logic.
Advanced Customization
Classpath Extension with JBang
For customizations requiring new dependencies or plugins, you can use JBang's magic, for example by creating a jbang-catalog.json
file in the project root, which would override ZenWave SDK's classpath when invoked from that directory:
{"catalogs": {},"aliases": {"zw": {"script-ref": "io.github.zenwave360.zenwave-sdk:zenwave-sdk-cli:RELEASE","dependencies": ["org.slf4j:slf4j-simple:1.7.36","<your-custom-dependency>",// ... standard ZenWave dependencies],"main": "io.zenwave360.sdk.Main"}}}
Important note: Dependencies added at the beginning will override ZenWave SDK classes.
Total customization: Example of complete substitution replacing Java with Kotlin
ZenWave SDK allows total customization. In fact, it includes a complete customization that replaces Java with Kotlin.
You can check the Kotlin customization source code to see how it's implemented.
In practical terms, to use the Kotlin customization, simply configure the templates
option with the corresponding class in the following plugins:
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()"}}}
Creating Custom Plugins
For specific needs, you can create completely new plugins that generate any type of artifact: code, APIs, documentation, tests...
Available tools:
- ZDL Parser
- OpenAPI/AsyncAPI Parser with reference resolution
- Java code formatters (Google, Palantir, Spring) and Kotlin (ktfmt)
- Handlebars template engine
You can base your work on any existing plugin and/or use the ZdlToJsonPlugin
Plugin to inspect the ZDL model structure and understand the transformations needed to achieve your goal.
jbang zw -p ZdlToJsonPlugin zdlFile=zenwave-model.zdl
The best way to start is to fork an existing plugin and modify it to suit your needs. You can find all available plugins in the ZenWave SDK repository.