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
// - CleanArchitectureProjectLayout
layout 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 CleanHexagonalProjectLayout
layout.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 aggregates
    • inbound (primary ports) and outbound (secondary ports) interfaces
    • and the internal implementation of inbound interfaces, which uses the outbound interfaces to interact with external services.

Β 

CleanHexagonalProjectLayout (πŸ‘‡ view source)
/**
* 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>
*/
πŸ”—Navigate to source

Β 

ZenWave SDK Modeling Languages

Notes about Generated Clean/Hexagonal Architecture

To balance pragmatism with architectural purity, this implementation makes two practical compromises:

  1. Domain entities directly use JPA or SpringData MongoDB annotations
  2. 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 (πŸ‘‡ view source)
/**
* 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>
*/
πŸ”—Navigate to source

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 (πŸ‘‡ view source)
/**
* 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 project
configPackage = "{{basePackage}}.config";
commonPackage = "{{basePackage}}"; // set to "{{basePackage}}.common" in modular projects
modulesPackage = "{{basePackage}}.modules";
// module specific
moduleBasePackage = "{{basePackage}}";
moduleConfigPackage = "{{moduleBasePackage}}.config";
// domain entities and events
entitiesPackage = "{{moduleBasePackage}}.domain";
πŸ”—Navigate to source

HexagonalProjectLayout

This layout follows a stricter naming convention for hexagonal architecture, with domain, ports, application core, and adapters.

HexagonalProjectLayout (πŸ‘‡ view source)
/**
* 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>
*/
πŸ”—Navigate to source

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 configuration
config {
title "Customer Module"
layout CleanHexagonalProjectLayout
layout.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 modules
config {
title "Modular Monolith"
basePackage "io.zenwave360.example"
// Packages shared between modules
layout.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:

  1. Locate the original template in the ZenWave SDK repository
  2. Copy the template to .zenwave/templates/[original-path] in your project root folder
  3. 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 (πŸ‘‡ view source)
// .zenwave/templates/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/outbound/jpa/imperative/EntityRepository.java.hbs
package {{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
@MyCustomRepositoryAnnotation
public 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.