Architecture¶
This guide explains Zygarde's module architecture, design principles, and how the components work together.
Module Structure¶
Zygarde is organized into focused modules, each serving a specific purpose:
zygarde/
├── modules-core/ # Core utilities
├── modules-jpa/ # JPA extensions, DAOs, search DSL
├── modules-web/ # Web/REST utilities
├── modules-model-mapping/ # Object mapping DSL
├── modules-codegen-support/# Code generation infrastructure
├── modules-test-support/ # Shared test fixtures
├── modules-bom/ # Bill of Materials
├── samples/ # Example applications
└── doc/ # Design documentation
Module Categories¶
Core Modules (modules-core/)¶
Foundation utilities used across the framework:
- zygarde-core: Error handling, dependency injection helpers, common extensions
- zygarde-jackson: JSON serialization configuration
- zygarde-jwt: JWT authentication utilities
- zygarde-mail: Email sending utilities
- zygarde-codegen-support: Base classes for code generation
JPA Modules (modules-jpa/)¶
JPA enhancements and type-safe querying:
- zygarde-jpa: Base DAOs, entity base classes, search DSL runtime
- zygarde-jpa-codegen: KAPT annotation processor for generating DAOs
- zygarde-jpa-envers: Audit entity base classes with Envers
Web Modules (modules-web/)¶
Web and REST utilities:
- zygarde-webmvc: Spring WebMVC utilities, exception handlers
- zygarde-webflux: Spring WebFlux utilities
- zygarde-webmvc-codegen-dsl: DSL for generating controllers
Model Mapping Modules (modules-model-mapping/)¶
Object mapping and DTO generation:
- zygarde-model-mapping-core: Core mapping abstractions
- zygarde-model-mapping-codegen-dsl: DSL for generating model mappings
Supporting Modules¶
- modules-bom/: Gradle Bill of Materials for version management
- modules-test-support/: Shared test utilities and fixtures
- samples/: Working example applications
Module Conventions¶
Standard Layout¶
Each module follows a consistent structure:
module-name/
├── build.gradle.kts # Module build configuration
├── src/
│ ├── main/
│ │ └── kotlin/ # Production code
│ │ └── zygarde/ # Package structure
│ └── test/
│ └── kotlin/ # Test code
└── README.md # Module documentation (optional)
Codegen Module Pattern¶
Modules with code generation capabilities follow this pattern:
- Runtime module (
*-runtimeor base name): Contains base classes, interfaces, and runtime support - Codegen module (
*-codegen): Contains annotation processors or DSL code generators
Example: zygarde-jpa (runtime) + zygarde-jpa-codegen (KAPT processor)
Automatic Module Registration¶
Modules are automatically discovered by settings.gradle.kts through the registerModules() function:
fun registerModules(prefix: String) {
file(prefix).listFiles()?.forEach { dir ->
if (dir.isDirectory && file("${dir.path}/build.gradle.kts").exists()) {
include(":${dir.name}")
project(":${dir.name}").projectDir = dir
}
}
}
registerModules("modules-core")
registerModules("modules-jpa")
registerModules("modules-web")
// ...
Adding a new module: Simply create a directory under modules-*/ with a build.gradle.kts file.
Dependency Relationships¶
Core Dependency Graph¶
┌─────────────────────────────────────────────────────┐
│ Application │
└──────────────┬──────────────────────────────────────┘
│
┌───────┴─────────┬─────────────────┐
│ │ │
┌──────▼──────┐ ┌───────▼────────┐ ┌────▼──────┐
│ zygarde-web │ │ zygarde-jpa │ │ Model │
│ │ │ │ │ Mapping │
└──────┬──────┘ └────────┬───────┘ └─────┬─────┘
│ │ │
│ ┌────────▼────────┐ │
│ │ zygarde-core │ │
└─────────► ◄────────┘
└─────────────────┘
Module Dependency Rules¶
- Core modules have no dependencies on other Zygarde modules
- JPA modules depend only on core modules
- Web modules may depend on core and JPA modules
- Codegen modules depend on their corresponding runtime modules
- Runtime modules never depend on codegen modules
This ensures: - Clean separation of concerns - No circular dependencies - Lightweight runtime libraries
Code Generation Architecture¶
Zygarde employs a two-tier code generation strategy:
1. KAPT-Based Generation¶
Purpose: Generate DAOs and search DSL from annotated entities
Workflow:
Key Components: - @ZyModel annotation marks entities for generation - KAPT processor in zygarde-jpa-codegen - Generated code: DAOs, search DSL methods, aggregated Dao class
Configuration: Via KAPT arguments in build.gradle.kts
2. DSL-Based Generation¶
Purpose: Generate application layer code (mappings, controllers, services)
Workflow:
Key Components: - ModelMappingCodegenSpec for model mappings - WebMvcDslCodegen for REST controllers - Standalone code generation scripts
Configuration: Via system properties and DSL definitions
Generation Isolation¶
Generated code is strictly isolated from hand-written code:
project/
├── src/main/kotlin/ # Hand-written code
└── build/generated/source/
└── kapt/main/ # Generated code (gitignored)
For multi-module projects:
my-app/
├── my-app-domain/ # Entities with @ZyModel
├── my-app-codegen/ # Generated code target
└── my-app-service/ # Uses generated code
Design Principles¶
1. Composition Over Configuration¶
Extend base classes without modifying core generation logic:
// Define custom base DAO
abstract class MyEnhancedDao<T, ID> : ZygardeEnhancedDao<T, ID> {
// Add custom methods
}
// Configure KAPT to use it
kapt {
arguments {
arg("zygarde.codegen.dao.inherit", "com.example.MyEnhancedDao")
}
}
2. Type Safety Through Generics¶
Extensive use of reified generics and bounded type parameters:
3. DSL as Configuration¶
Both KAPT options and DSL codegen use configuration-as-code:
// KAPT configuration
kapt {
arguments {
arg("zygarde.codegen.base.package", "com.example.codegen")
}
}
// DSL configuration
MyDto {
fromAutoIntId(Entity::id)
from(Entity::description)
}
4. Layered Isolation¶
Clear boundaries between layers:
- Framework layer: Core utilities, base classes
- Generated layer: KAPT/DSL generated code
- Application layer: Business logic using generated code
5. Convention Over Configuration¶
Sensible defaults with override capability:
- Default DAO suffix:
Dao(configurable viazygarde.codegen.dao.suffix) - Default base DAO:
JpaRepository(configurable viazygarde.codegen.dao.inherit) - Default search DSL operators based on field types
Extension Points¶
Custom Search Actions¶
Extend search DSL with custom actions:
class CustomAction<ROOT, CURRENT, FIELD>(
/* ... */
) : ConditionAction<ROOT, CURRENT, FIELD>(/* ... */) {
infix fun customOperator(value: FIELD): ROOT {
// Custom predicate logic
return root
}
}
Custom Value Providers¶
Create custom value transformation logic:
class CustomValueProvider<FROM, TO> : ValueProvider<FROM, TO> {
override fun invoke(from: FROM): TO {
// Transformation logic
}
}
Custom Exception Mappers¶
Handle domain-specific exceptions:
@Component
class MyExceptionMapper : ExceptionToBusinessExceptionMapper<MyException> {
override val exceptionClass = MyException::class.java
override fun map(exception: MyException) = BusinessException(
message = exception.message,
code = "MY_ERROR"
)
}
Architectural Insights¶
Why Two Code Generation Approaches?¶
- KAPT for entity-driven generation: Captures entity metadata at compile-time
- DSL for application-layer generation: Flexible, regenerable independently
This separation allows: - Entity metadata captured without runtime overhead - Application patterns remain flexible - Independent regeneration cycles
JPA Criteria Abstraction¶
The search DSL abstracts JPA Criteria complexity:
- Type-safe field references: Generated extensions ensure fields exist
- Action objects: Fluent interface avoids string-based queries
- Automatic join resolution: Relationship traversal handled automatically
Example of automatic join resolution:
Module Dependency Pattern¶
Strict separation ensures: - Codegen modules depend on runtime modules (for base classes) - Runtime modules never depend on codegen modules - Framework remains lightweight without generation infrastructure
Next Steps¶
- Code Generation → - Learn both generation approaches
- JPA Extensions → - Explore JPA features
- Common Patterns → - Best practices