Best Practices¶
This guide covers production-tested patterns for structuring and developing applications with Zygarde. Following these conventions will help you maintain clean architecture, leverage code generation effectively, and keep your codebase scalable.
Project Structure¶
Multi-Module Organization¶
A well-organized Zygarde project separates concerns across multiple modules. The recommended layout groups modules by purpose:
my-app/
├── settings.gradle.kts
├── build.gradle.kts
├── bom/ # Bill of Materials for dependency versions
│ └── build.gradle.kts
├── modules/
│ ├── my-core/ # Entities, enums, shared constants
│ │ └── build.gradle.kts
│ ├── my-model-base/ # Base model classes, API model constants
│ │ └── build.gradle.kts
│ ├── my-api/ # Spring Boot application, service implementations
│ │ └── build.gradle.kts
│ ├── codegen/
│ │ ├── my-core-codegen/ # KAPT: generates DAOs, DTOs, entity search, model mapping
│ │ ├── my-api-spec-codegen/ # KAPT: generates API interfaces, controllers, service interfaces
│ │ └── my-model-codegen/ # DSL: model mapping codegen specs
│ ├── codegen-generated/
│ │ ├── my-generated-jpa/ # Generated DAOs and entity search extensions
│ │ ├── my-generated-dto/ # Generated DTOs (from KAPT)
│ │ ├── my-generated-model-mapping/ # Generated DtoBuilders and applyFrom extensions
│ │ ├── my-generated-api-interface/ # Generated API interfaces
│ │ ├── my-generated-api-controller/ # Generated controllers
│ │ ├── my-generated-service-interface/ # Generated service interfaces
│ │ └── my-generated-api-interface-feign/ # Generated Feign clients (optional)
│ └── test-support/ # Shared test utilities
│ └── build.gradle.kts
Key principles:
- Entity module (
my-core) contains JPA entities, enums, and shared types. It does NOT run KAPT itself. - Codegen modules (
codegen/) contain KAPT processors and DSL specs. They read entities and write generated code tocodegen-generated/modules. - Generated modules (
codegen-generated/) contain only generated source code. They can be safely deleted and regenerated. - Application module (
my-api) depends on generated modules and contains hand-written service implementations.
Auto-Discovery with registerModules¶
Use Zygarde's registerModules() pattern in settings.gradle.kts for automatic module discovery:
rootProject.name = "my-app"
fun registerModules(path: String) {
File(rootProject.projectDir, path)
.listFiles { f -> f.isDirectory && File(f, "build.gradle.kts").exists() }
?.forEach { f ->
include(f.name)
project(":${f.name}").projectDir = f
project(":${f.name}").name = f.name
}
}
include("bom")
registerModules("modules")
// Skip codegen modules in CI (they only need to run locally)
if (System.getenv("CI") == null) {
registerModules("modules/codegen")
}
registerModules("modules/codegen-generated")
This approach lets you add new modules by simply creating a directory with a build.gradle.kts file.
Dependency Management¶
BOM Pattern¶
Centralize all dependency versions in a Bill of Materials module:
// bom/build.gradle.kts
apply(plugin = "java-platform")
val zygardeVersion = extra["zygardeVersion"]
dependencies {
constraints {
// Zygarde runtime
"api"("io.github.zygarde-projects:zygarde-core:$zygardeVersion")
"api"("io.github.zygarde-projects:zygarde-jpa:$zygardeVersion")
"api"("io.github.zygarde-projects:zygarde-jpa-envers:$zygardeVersion")
"api"("io.github.zygarde-projects:zygarde-di:$zygardeVersion")
"api"("io.github.zygarde-projects:zygarde-jackson:$zygardeVersion")
"api"("io.github.zygarde-projects:zygarde-model-mapping:$zygardeVersion")
"api"("io.github.zygarde-projects:zygarde-webmvc:$zygardeVersion")
// Zygarde codegen (KAPT)
"api"("io.github.zygarde-projects:zygarde-jpa-codegen:$zygardeVersion")
"api"("io.github.zygarde-projects:zygarde-model-mapping-codegen:$zygardeVersion")
"api"("io.github.zygarde-projects:zygarde-webmvc-codegen:$zygardeVersion")
// Zygarde DSL codegen
"api"("io.github.zygarde-projects:zygarde-model-mapping-codegen-dsl:$zygardeVersion")
// Zygarde test
"api"("io.github.zygarde-projects:zygarde-test-error-handling:$zygardeVersion")
"api"("io.github.zygarde-projects:zygarde-test-feign:$zygardeVersion")
// Other dependencies
"api"("io.kotest:kotest-assertions-core-jvm:5.8.0")
"api"("io.mockk:mockk:1.13.8")
}
}
Then reference the BOM in subprojects:
This means individual modules can declare dependencies without specifying versions:
// In any submodule's build.gradle.kts
dependencies {
api("io.github.zygarde-projects:zygarde-jpa") // Version comes from BOM
}
Root Build Configuration¶
A typical root build.gradle.kts defines shared configuration:
plugins {
id("org.jlleitschuh.gradle.ktlint") version "10.2.0"
id("org.springframework.boot") version "2.7.18"
id("io.spring.dependency-management") version "1.1.3"
kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25" apply false
kotlin("plugin.jpa") version "1.9.25" apply false
kotlin("plugin.allopen") version "1.9.25" apply false
kotlin("kapt") version "1.9.25" apply false
}
allprojects {
group = "com.example"
repositories {
mavenCentral()
maven("https://jitpack.io")
}
extra["zygardeVersion"] = "2.1.2"
}
val kaptProjects = listOf("my-core-codegen", "my-model-base", "my-api-spec-codegen")
val bootableProjects = listOf("my-api")
subprojects {
apply(plugin = "kotlin")
if (kaptProjects.contains(name)) apply(plugin = "kotlin-kapt")
apply(plugin = "kotlin-jpa")
apply(plugin = "kotlin-spring")
apply(plugin = "kotlin-allopen")
apply(plugin = "org.springframework.boot")
apply(plugin = "io.spring.dependency-management")
dependencies {
api(platform(project(":bom")))
implementation("org.jetbrains.kotlin:kotlin-reflect")
testImplementation("io.kotest:kotest-assertions-core-jvm")
}
tasks.withType<Test> { useJUnitPlatform() }
tasks.getByName("bootJar").enabled = bootableProjects.contains(name)
tasks.getByName("jar").enabled = !bootableProjects.contains(name)
}
Entity Design¶
Choosing Base Classes¶
Zygarde provides entity base classes for common ID strategies:
// Auto-increment Int ID (good for most entities)
@Entity
@ZyModel
class Department(
@Column var name: String = "",
) : AutoIntIdEntity()
// Auto-increment Long ID (for entities that may grow very large)
@Entity
@ZyModel
class AuditLog(
@Column var action: String = "",
) : AutoLongIdEntity()
// Audited entity with Envers tracking (createdAt, createdBy, etc.)
@Entity
@Audited
@AuditOverride(forClass = AuditedAutoIntIdEntity::class)
@EntityListeners(AuditingEntityListener::class)
@ZyModel
class Employee(
@Column var name: String = "",
@Column var email: String = "",
) : AuditedAutoIntIdEntity()
Entity Conventions¶
Follow these conventions for entity definitions:
@ZyModel
@Audited
@AuditOverride(forClass = AuditedAutoIntIdEntity::class)
@Entity
@EntityListeners(AuditingEntityListener::class)
@Table(name = "employee")
class Employee(
@Comment("Employee name")
@Column
var name: String = "",
@Comment("Email address")
@Column
var email: String = "",
@Comment("Status")
@Enumerated(EnumType.STRING)
@Column(length = 16)
var status: EmployeeStatus = EmployeeStatus.ACTIVE,
@Comment("Department")
@ManyToOne(targetEntity = Department::class, fetch = FetchType.LAZY)
open var department: Department,
@Comment("Sort order")
@Column(name = "sort_idx")
var sort: Int? = 0,
) : AuditedAutoIntIdEntity()
Tips:
- Use
@Commentfrom Hibernate for database column comments - Always use
FetchType.LAZYfor@ManyToOnerelationships - Use
@Enumerated(EnumType.STRING)for enums (neverORDINAL) - Set reasonable
@Column(length = ...)limits for string fields - Use
openmodifier on relationship fields for Hibernate proxy support
allOpen Configuration¶
Entities must be open for Hibernate proxying. Configure the allOpen plugin in entity modules:
// In the entity module's build.gradle.kts
allOpen {
annotation("javax.persistence.Entity")
annotation("javax.persistence.MappedSuperclass")
annotation("javax.persistence.Embeddable")
}
KAPT Configuration¶
Core Codegen Module¶
The core codegen module runs KAPT to generate DAOs, search extensions, DTOs, and model mappings:
// my-core-codegen/build.gradle.kts
dependencies {
api(project(":my-core"))
api("io.github.zygarde-projects:zygarde-jpa")
api("io.github.zygarde-projects:zygarde-jpa-envers")
api("io.github.zygarde-projects:zygarde-model-mapping")
kapt(platform(project(":bom")))
kapt("io.github.zygarde-projects:zygarde-jpa-codegen")
kapt("io.github.zygarde-projects:zygarde-model-mapping-codegen")
}
// Share source from the entity module so KAPT can see entities
sourceSets {
main {
java {
srcDirs("src/main/kotlin", project(":my-core").file("src/main/kotlin").absolutePath)
}
}
}
kapt {
arguments {
arg("zygarde.codegen.base.package", "com.example.codegen")
arg("zygarde.codegen.dao.inherit", "zygarde.data.jpa.dao.ZygardeEnhancedDao")
arg("zygarde.model_mapping.dto.write_to",
project(":my-generated-dto").file("src/main/kotlin").absolutePath)
arg("zygarde.model_mapping.extension.write_to",
project(":my-generated-model-mapping").file("src/main/kotlin").absolutePath)
arg("zygarde.codegen.jpa.dao.generate_to",
project(":my-generated-jpa").file("src/main/kotlin").absolutePath)
arg("zygarde.codegen.jpa.entity_field.generate_to",
project(":my-generated-jpa").file("src/main/kotlin").absolutePath)
}
}
// Clean generated folders before each KAPT run
tasks.withType(org.jetbrains.kotlin.gradle.tasks.Kapt::class.java).all {
doFirst {
project(":my-generated-dto").file("src/main/kotlin").deleteRecursively()
project(":my-generated-model-mapping").file("src/main/kotlin").deleteRecursively()
project(":my-generated-jpa").file("src/main/kotlin").deleteRecursively()
}
}
// Disable test KAPT (not needed)
tasks.withType<org.jetbrains.kotlin.gradle.internal.KaptTask>()
.matching { it.name == "kaptTestKotlin" }
.configureEach { enabled = false }
API Spec Codegen Module¶
A separate KAPT module generates the API layer (interfaces, controllers, service interfaces):
// my-api-spec-codegen/build.gradle.kts
dependencies {
implementation(project(":my-model-all"))
kapt("io.github.zygarde-projects:zygarde-webmvc-codegen")
}
val apiInterfaceDir = project(":my-generated-api-interface").file("src/main/kotlin")
val controllerDir = project(":my-generated-api-controller").file("src/main/kotlin")
val serviceDir = project(":my-generated-service-interface").file("src/main/kotlin")
kapt {
val apiGenFile = File.createTempFile("api-gen", ".json")
apiGenFile.writeText(
groovy.json.JsonOutput.toJson(
mapOf(
"generateApiInterfacesTo" to apiInterfaceDir.absolutePath,
"generateControllersTo" to controllerDir.absolutePath,
"generateServiceInterfacesTo" to serviceDir.absolutePath
)
)
)
arguments {
arg("zygarde.codegen.base.package", "com.example.codegen")
arg("zygarde.webmvc_codegen.api_config_json", apiGenFile.absolutePath)
}
}
// Clean before regeneration
tasks.withType(org.jetbrains.kotlin.gradle.tasks.Kapt::class.java).all {
doFirst {
apiInterfaceDir.deleteRecursively()
controllerDir.deleteRecursively()
serviceDir.deleteRecursively()
}
}
Disabling KAPT in CI¶
Since generated code is committed to version control, skip KAPT in CI to speed up builds:
// In settings.gradle.kts
if (System.getenv("CI") == null) {
registerModules("modules/codegen")
}
// Or in the codegen module's build.gradle.kts
if (System.getenv("CI") != null) {
tasks.withType(org.jetbrains.kotlin.gradle.tasks.Kapt::class.java).all {
enabled = false
}
}
Model Mapping DSL¶
Structure with CodegenDtoSimple¶
The recommended pattern for DSL-based model mapping uses an enum class implementing CodegenDtoSimple to define DTO names:
class EmployeeModelSpec : ModelMappingCodegenSpec({
Models.EmployeeDto {
fromAutoIntId(Employee::id)
from(
Employee::name,
Employee::email,
Employee::status,
)
}
Models.EmployeeDetailDto {
fromAutoIntId(Employee::id)
from(
Employee::name,
Employee::email,
Employee::status,
)
fromRef("department", DepartmentModelSpec.Models.DepartmentDto)
fromExtra(
EmployeeModelSpec::hireDate,
EmployeeModelSpec::performanceScore,
)
}
Models.CreateEmployeeReq {
applyTo(
Employee::name,
Employee::email,
)
field<Int>("departmentId") {
comment = "Department ID"
}
}
Models.UpdateEmployeeReq {
applyTo(
Employee::name,
Employee::email,
Employee::status,
)
}
Models.SearchEmployeeReq {
fieldNullable(
Employee::name,
Employee::status,
)
}
}) {
enum class Models : CodegenDtoSimple {
EmployeeDto,
EmployeeDetailDto,
CreateEmployeeReq,
UpdateEmployeeReq,
SearchEmployeeReq {
override fun superClass() = PagingAndSortingRequest::class
},
}
// Extra fields for EmployeeDetailDto
lateinit var hireDate: LocalDate
var performanceScore: Double = 0.0
}
Key patterns:
enum class Models : CodegenDtoSimple- Defines DTO class names. Using an enum prevents typos and enables IDE autocomplete.superClass()override - Makes a DTO extend a base class (e.g.,PagingAndSortingRequestfor search requests).- Extra fields as class properties - Define properties on the spec class itself, then reference them with
fromExtra().
Mapping Entity Fields to DTOs¶
// Map auto-increment ID
fromAutoIntId(Employee::id) // for Int IDs
fromAutoLongId(Employee::id) // for Long IDs
// Map single field
from(Employee::name)
// Map multiple fields at once (preferred)
from(
Employee::name,
Employee::email,
Employee::status,
)
Mapping Relationships¶
// Reference to another DTO (many-to-one)
fromRef("department", DepartmentModelSpec.Models.DepartmentDto)
// Nullable reference
fromRef("department", DepartmentModelSpec.Models.DepartmentDto, nullable = true) {
comment = "Employee's department"
}
// Collection of referenced DTOs (one-to-many)
fromRefCollection("employees", EmployeeModelSpec.Models.EmployeeDto)
// Nullable collection reference
fromRefCollection(
fieldName = "employees",
dtoRef = EmployeeModelSpec.Models.EmployeeDto,
nullable = true
)
Extra/Computed Fields¶
Use fromExtra() for fields that are computed or come from sources other than the entity:
class BookModelSpec : ModelMappingCodegenSpec({
Models.BookDetailDto {
fromAutoIntId(Book::id)
from(Book::title, Book::price)
fromExtra(
BookModelSpec::averageRating,
BookModelSpec::reviewCount,
)
// Inline extra with explicit type
fromExtra<String>("publisherName")
}
}) {
enum class Models : CodegenDtoSimple {
BookDetailDto,
}
var averageRating: Double = 0.0
var reviewCount: Int = 0
}
Request DTO Fields¶
For request DTOs, use applyTo(), field(), and related methods:
Models.CreateBookReq {
// Fields that map directly to entity properties
applyTo(
Book::title,
Book::description,
Book::price,
)
// Extra field not on the entity
field<Int>("authorId") {
comment = "Author ID"
}
// Nullable field
fieldNullable(
Book::isbn,
)
// Reference to another DTO in the request
fieldRef("publisher", PublisherModelSpec.Models.PublisherDto)
// Collection of references
fieldRefCollection("tags", TagModelSpec.Models.TagDto)
}
Grouping Shared Mappings¶
Use group() when multiple DTOs share the same field mappings:
// Both DTOs will get the same name and status fields
group(Models.EmployeeListDto, Models.EmployeeSearchResultDto) {
fromAutoIntId(Employee::id)
from(
Employee::name,
Employee::status,
)
}
// Individual DTOs can still have their own additional fields
Models.EmployeeListDto {
fromExtra(EmployeeModelSpec::departmentName)
}
Nested Extra Classes¶
When different DTOs need different extra fields, define nested classes:
class EmployeeModelSpec : ModelMappingCodegenSpec({
Models.ManagerDto {
fromAutoIntId(Employee::id)
from(Employee::name)
fromExtra(ManagerExtra::teamSize)
}
Models.ContractorDto {
fromAutoIntId(Employee::id)
from(Employee::name)
fromExtra(ContractorExtra::contractEndDate)
}
}) {
enum class Models : CodegenDtoSimple {
ManagerDto,
ContractorDto,
}
class ManagerExtra {
var teamSize: Int = 0
}
class ContractorExtra {
lateinit var contractEndDate: LocalDate
}
}
DSL Execution Configuration¶
Configure the DSL model codegen module:
// my-model-codegen/build.gradle.kts
apply(plugin = "application")
dependencies {
implementation(project(":my-core"))
implementation(project(":my-generated-dto")) // for cross-referencing existing DTOs
implementation("io.github.zygarde-projects:zygarde-model-mapping")
implementation("io.github.zygarde-projects:zygarde-model-mapping-codegen-dsl")
}
configure<JavaApplication> {
mainClass.set("zygarde.codegen.dsl.ModelMappingDslMainKt")
applicationDefaultJvmArgs = listOf(
"-Dzygarde.codegen.dsl.model-mapping.write-to=${
project(":my-next-generated-model-mapping").file("src/main/kotlin").absolutePath
}",
"-Dzygarde.codegen.dsl.dto.write-to=${
project(":my-next-generated-dto").file("src/main/kotlin").absolutePath
}",
"-Dzygarde.codegen.dsl.model-mapping.dto-package=com.example.dto",
"-Dzygarde.codegen.dsl.model-mapping.extension-package=com.example.dto.extensions"
)
}
Run the codegen with:
Service Implementation¶
Using Generated DAOs¶
Generated DAOs are Spring-managed beans. Inject them directly:
@Service
@Transactional
class EmployeeServiceImpl(
private val employeeDao: EmployeeDao,
private val departmentDao: DepartmentDao,
) : EmployeeService {
// ...
}
Entity-to-DTO with DtoBuilder¶
The generated DtoBuilder objects convert entities to DTOs:
override fun getEmployee(id: Int): EmployeeDto {
return employeeDao.getReferenceById(id)
.let { EmployeeDtoBuilder.build(it) }
}
For DTOs with extra fields, pass them as arguments:
override fun getEmployeeDetail(id: Int): EmployeeDetailDto {
val employee = employeeDao.getReferenceById(id)
val department = employee.department
return EmployeeDetailDtoBuilder.build(
employee,
DepartmentDtoBuilder.build(department), // fromRef
employee.hireDate, // fromExtra: hireDate
calculatePerformanceScore(employee), // fromExtra: performanceScore
)
}
DTO-to-Entity with applyFrom¶
Generated applyFrom extensions apply request DTO fields to an entity:
override fun createEmployee(req: CreateEmployeeReq): EmployeeDetailDto {
val department = departmentDao.getReferenceById(req.departmentId)
return Employee(department = department)
.applyFromCreateEmployeeReq(req)
.let(employeeDao::saveAndFlush)
.let { getEmployeeDetail(it.getId()) }
}
override fun updateEmployee(id: Int, req: UpdateEmployeeReq): EmployeeDetailDto {
return employeeDao.getReferenceById(id)
.applyFromUpdateEmployeeReq(req)
.let(employeeDao::saveAndFlush)
.let { getEmployeeDetail(it.getId()) }
}
Note
Use saveAndFlush instead of save when you need the ID immediately after creation, or when subsequent operations depend on the persisted state.
Search with Pagination¶
The searchPage + toPageDto pattern provides clean pagination:
override fun searchEmployees(req: SearchEmployeeReq): PageDto<EmployeeDto> {
return employeeDao.searchPage(req) {
req.name?.let { name() contains it }
req.status?.let { status() eq it }
}.toPageDto { EmployeeDtoBuilder.build(it) }
}
The searchPage method accepts a PagingAndSortingRequest and a search DSL lambda. The toPageDto method maps each entity in the result page to a DTO.
Search with Relationship Filtering¶
Navigate relationships in search queries naturally:
override fun searchEmployeesByDepartment(
departmentId: Int,
req: SearchEmployeeReq
): PageDto<EmployeeDto> {
return employeeDao.searchPage(req) {
department().id() eq departmentId
req.name?.let { name() contains it }
req.status?.let { status() eq it }
}.toPageDto { EmployeeDtoBuilder.build(it) }
}
Count Queries¶
Use searchCount for counting without fetching entities:
val activeCount = employeeDao.searchCount {
status() eq EmployeeStatus.ACTIVE
department().id() eq departmentId
}
Delete Operations¶
Module Dependency Flow¶
Understanding the module dependency graph is critical for build correctness:
Entity Module (my-core)
Contains: @Entity classes, enums, shared types
Dependencies: zygarde-jpa, zygarde-model-mapping, Spring Data JPA
↓ source shared via srcDirs
Core Codegen (my-core-codegen) [KAPT]
Runs: zygarde-jpa-codegen, zygarde-model-mapping-codegen
Writes to: my-generated-jpa, my-generated-dto, my-generated-model-mapping
API Spec Codegen (my-api-spec-codegen) [KAPT]
Runs: zygarde-webmvc-codegen
Writes to: my-generated-api-interface, my-generated-api-controller,
my-generated-service-interface
Model Codegen (my-model-codegen) [DSL]
Runs: ModelMappingDslMain
Writes to: my-next-generated-dto, my-next-generated-model-mapping
↓ generated code modules
Application (my-api)
Depends on: all generated modules + zygarde runtime modules
Contains: Service implementations, custom business logic, configuration
Strict separation rules:
- Codegen modules never depend on each other - each reads from entity modules and writes to generated modules independently.
- Generated modules contain only generated code - never add hand-written code to these modules.
- The application module never runs KAPT - it only consumes generated code.
- Runtime modules never depend on codegen modules - the framework's runtime is lightweight and independent.
Two-Phase Generation¶
Zygarde supports using both KAPT and DSL codegen in the same project. This is useful when:
- KAPT handles entity-level generation (DAOs, search DSL, annotation-driven DTOs)
- DSL handles additional DTO mappings that are more complex or require cross-entity references
The two approaches complement each other:
| Aspect | KAPT | DSL |
|---|---|---|
| Trigger | Compile-time annotation processing | Runtime script execution |
| Input | @ZyModel, @ApiProp, @ZyApi annotations | Kotlin DSL spec classes |
| Best for | DAOs, search extensions, simple DTOs | Complex DTOs, cross-entity mappings |
| Regeneration | Requires recompilation | Run script independently |
Workflow¶
- Define entities with
@ZyModeland@ApiPropannotations - Run KAPT:
./gradlew :my-core-codegen:kaptKotlin - Define DSL specs referencing KAPT-generated DTOs
- Run DSL:
./gradlew :my-model-codegen:run - Build the full project:
./gradlew build
Testing¶
Integration Tests with Feign Clients¶
Test generated APIs end-to-end using Feign clients:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@ActiveProfiles("test")
class EmployeeApiTest {
@Test
fun `should create and retrieve employee`() {
val api = api<EmployeeApi>()
// Create
val created = api.createEmployee(
CreateEmployeeReq(name = "John Doe", email = "john@example.com", departmentId = 1)
)
created.name shouldBe "John Doe"
// Retrieve
val retrieved = api.getEmployee(created.id)
retrieved.email shouldBe "john@example.com"
// Update
api.updateEmployee(created.id, UpdateEmployeeReq(name = "Jane Doe"))
api.getEmployee(created.id).name shouldBe "Jane Doe"
// Delete
api.deleteEmployee(created.id)
}
}
Unit Tests with MockK¶
Test service implementations in isolation:
class EmployeeServiceTest {
private val employeeDao: EmployeeDao = mockk()
private val departmentDao: DepartmentDao = mockk()
private val service = EmployeeServiceImpl(employeeDao, departmentDao)
@Test
fun `should search employees by department`() {
// given
val employee = Employee(name = "John", department = mockk()).apply { id = 1 }
every { employeeDao.searchPage(any(), any<EnhancedSearch<Employee>.() -> Unit>()) } returns
PageImpl(listOf(employee))
// when
val result = service.searchEmployees(SearchEmployeeReq())
// then
result.items.size shouldBe 1
}
}
Common Pitfalls¶
Never Edit Generated Code¶
Generated files in codegen-generated/ modules are overwritten on each codegen run. Always modify the DSL spec or entity annotations instead.
Keep Codegen Modules Out of CI¶
Codegen modules only need to run during development. Generated code should be committed to version control so CI can build without KAPT.
Use saveAndFlush for Immediate Persistence¶
When returning a DTO from a create operation, use saveAndFlush to ensure the entity is persisted and its ID is available:
// Correct
Employee().applyFromCreateEmployeeReq(req).let(employeeDao::saveAndFlush)
// May cause issues if ID is accessed before flush
Employee().applyFromCreateEmployeeReq(req).let(employeeDao::save)
Configure allOpen for All Entity Annotations¶
Missing allOpen configuration causes Hibernate proxy issues. Always include all three:
allOpen {
annotation("javax.persistence.Entity")
annotation("javax.persistence.MappedSuperclass")
annotation("javax.persistence.Embeddable")
}
Use Lazy Fetching for Relationships¶
Always use FetchType.LAZY for @ManyToOne and @OneToMany to avoid loading the entire entity graph:
@ManyToOne(targetEntity = Department::class, fetch = FetchType.LAZY)
open var department: Department
Next Steps¶
- Code Generation - Detailed code generation guide
- Search DSL - Type-safe query patterns
- Model Mapping - DTO mapping reference
- Common Patterns - Additional patterns