1. Domain Model Generator
The Domain Model Generator is a desktop application that automatically generates Codion domain model source code from existing database schemas. Instead of manually writing entity definitions, columns, foreign keys, and attribute configurations, you connect to a database, select a schema, and generate fully-formed Codion domain code. This tool accelerates development, ensures consistency across entities, and serves as a starting point for new projects or when integrating with legacy databases.
1.1. Overview
The generator transforms database metadata into type-safe Codion domain models, supporting:
-
API/Implementation separation - Generates separate interface and implementation files for cleaner client dependencies
-
Combined mode - Single-file output for simpler project structures
-
DTO generation - Optional Java records for data transfer without Entity framework overhead
-
Internationalization - Optional resource bundle generation for captions and descriptions
-
Test generation - Optional JUnit test class for domain model validation
-
Schema customization - Configurable naming conventions, audit columns, and identifier casing
The generated code uses the same patterns as hand-written Codion domain models, including proper primary key generators, foreign key relationships, nullability constraints, and column metadata.
1.2. Architecture
The generator consists of three layered modules:
- codion-tools-generator-domain
-
Code generation engine that uses Palantir JavaPoet to produce syntactically correct Java source code, coordinates generation of API interfaces, implementation classes, DTO records, and i18n properties. Uses codion-framework-domain-db for schema instrospection
- codion-tools-generator-model
-
MVC model layer providing which coordinates schema discovery, entity selection, and real-time code preview. Contains FilterTableModels for schema and entity selection with observable state management.
- codion-tools-generator-ui
-
Swing desktop interface providing schema browser, entity selection, configuration dialogs, and code preview with search.
1.3. Project Setup
To use the generator in your project, create a Gradle (or the Maven equivalent) build configuration similar to the included demo:
plugins {
application
}
dependencies {
runtimeOnly("is.codion:codion-tools-generator-ui:{codion-version}")
// Add your database JDBC driver
runtimeOnly("is.codion:codion-dbms-h2:{codion-version}")
runtimeOnly("com.h2database:h2:{h2-version}")
// Or PostgreSQL:
//runtimeOnly("is.codion:codion-dbms-postgresql:{codion-version}")
// runtimeOnly("org.postgresql:postgresql:{postgresql-driver-version")
// Or Oracle:
//runtimeOnly("is.codion:codion-dbms-oracle:{codion-version}")
// runtimeOnly("com.oracle.database.jdbc:ojdbc:{oracle-driver-version}") { isTransitive = false }
}
application {
mainModule = "is.codion.tools.generator.ui"
mainClass = "is.codion.tools.generator.ui.DomainGeneratorPanel"
applicationDefaultJvmArgs = listOf(
"-Xmx256m",
"-Dcodion.db.url=jdbc:h2:mem:h2db",
"-Dcodion.domain.generator.domainPackage=com.example.domain",
// Output directories (relative or absolute paths)
"-Dcodion.domain.generator.combinedSourceDirectory=combined",
"-Dcodion.domain.generator.apiSourceDirectory=api",
"-Dcodion.domain.generator.implSourceDirectory=impl"
)
}
Run with: ./gradlew :your-generator-module:run
|
Note
|
See Chinook demo for a complete example. |
1.4. Configuration
The generator is configured through system properties set via JVM arguments:
1.4.1. Runtime Configuration
| Property | Description | Default |
|---|---|---|
|
JDBC connection URL |
Required |
|
Base package for generated code |
|
|
Output directory for combined mode (relative or absolute path) |
Current directory |
|
Output directory for API sources when using API/Impl mode (relative or absolute path) |
Current directory |
|
Output directory for implementation sources when using API/Impl mode (relative or absolute path) |
Current directory |
|
Specifies whether the database requires a login |
true |
|
Database user including password |
None |
|
Default database credentials to display in the login dialog |
None |
|
Comma-separated paths to SQL initialization scripts (H2 only) |
None |
-Dcodion.db.url=jdbc:postgresql://localhost:5432/mydb
-Dcodion.domain.generator.domainPackage=com.example.domain
-Dcodion.domain.generator.combinedSourceDirectory=generated
-Dcodion.domain.generator.apiSourceDirectory=api
-Dcodion.domain.generator.implSourceDirectory=impl
|
Tip
|
Use relative paths like "api", "impl", or "../domain-api" for convenience. When using the directory selector in the UI, paths are automatically converted to relative paths from the current working directory.
|
|
Important
|
H2 database does not allow path traversal in init scripts. Use absolute paths as shown the Chinook demo. |
1.4.2. Schema Settings
Schema settings control how database metadata is interpreted and transformed into domain model code. Access via right-click → Settings… on a schema row.
| Setting | Description | Default |
|---|---|---|
Primary Key Column Suffix |
Suffix removed from foreign key column names when creating FK identifiers |
|
View Suffix |
Suffix removed from view names when creating entity type names |
|
View Prefix |
Prefix removed from view names when creating entity type names |
|
Audit Column Names |
Comma-separated list of audit column names to mark as read-only |
|
Hide Audit Columns |
Hide audit columns from UI components |
|
Lowercase Identifiers |
Use lowercase for entity type and attribute names |
|
Primary Key Column Suffix
When database foreign key columns follow a naming convention like CUSTOMER_ID (referencing CUSTOMER.ID), the generator creates a foreign key constant like CUSTOMER_ID_FK. Setting a suffix of "ID" or "_ID" strips this from the FK name, resulting in the cleaner CUSTOMER_FK.
Column<Integer> CUSTOMER_ID = TYPE.integerColumn("customer_id");
ForeignKey CUSTOMER_ID_FK = TYPE.foreignKey("customer_id_fk", CUSTOMER_ID, Customer.ID);
Column<Integer> CUSTOMER_ID = TYPE.integerColumn("customer_id");
ForeignKey CUSTOMER_FK = TYPE.foreignKey("customer_fk", CUSTOMER_ID, Customer.ID);
View Suffix
Databases often suffix view names with _VW, _VIEW, or similar. Setting this suffix removes it from the caption before "View" is appended to the interface name.
|
Important
|
The EntityType identifier always matches the actual database view name. "View" is ALWAYS appended to view interface names to avoid collisions with table names. The suffix configuration only affects whether the suffix is removed from the caption first. |
CREATE VIEW ORDERS_V AS SELECT ...
- Without suffix configured
-
-
EntityType:
DOMAIN.entityType("orders_v") -
Interface:
interface OrdersVView("Orders v" → PascalCase + "View") -
Caption:
"Orders v"
-
- With suffix "_v" configured
-
-
EntityType:
DOMAIN.entityType("orders_v")(unchanged - always matches database) -
Interface:
interface OrdersView(suffix removed → "Orders" → PascalCase + "View") -
Caption:
"Orders"(clean caption without suffix)
-
View Prefix
Databases sometimes prefix view names with VW_, V_, or similar. Setting this prefix removes it from the caption before "View" is appended to the interface name.
|
Important
|
The EntityType identifier always matches the actual database view name. "View" is ALWAYS appended to view interface names to avoid collisions with table names. The prefix configuration only affects whether the prefix is removed from the caption first. |
CREATE VIEW VW_ORDERS AS SELECT ...
- Without prefix configured
-
-
EntityType:
DOMAIN.entityType("vw_orders") -
Interface:
interface VwOrdersView("Vw orders" → PascalCase + "View") -
Caption:
"Vw orders"
-
- With prefix "vw_" configured
-
-
EntityType:
DOMAIN.entityType("vw_orders")(unchanged - always matches database) -
Interface:
interface OrdersView(prefix removed → "Orders" → PascalCase + "View") -
Caption:
"Orders"(clean caption without prefix)
-
|
Note
|
Both view prefix and view suffix can be used simultaneously. The prefix is removed first, then the suffix. This prevents collisions when both a table and view exist with similar names (e.g., orders table and orders_v view).
|
Audit Columns
Many schemas include audit columns like INSERT_USER, INSERT_TIME, UPDATE_USER, UPDATE_TIME. Specifying these column names (comma-separated, case-insensitive) marks them as read-only in generated definitions.
Customer.INSERT_USER.define()
.column()
.readOnly(true)
.hidden(true) // If "Hide Audit Columns" enabled
1.5. User Workflow
-
Launch the application
./gradlew :your-generator-module:run -
Authenticate (if required)
Enter database credentials in the login dialog. Set
codion.domain.generator.userto pre-fill the username field. -
Select a schema
The schema table lists all available database schemas with their catalog names. Click to select.
-
Populate the schema
Double-click the schema row or press Cmd+Enter (macOS) / Ctrl+Enter (other) to load entity definitions. This introspects database metadata and populates the entity table.
-
Configure schema settings (optional)
Right-click the schema row and select Settings… to customize naming conventions and audit column handling. Settings are persisted in user preferences.
-
Review entities
The entity table shows all tables and views discovered in the schema, including their type (TABLE/VIEW) and metadata.
-
Select entities for DTO generation (optional)
Check the DTO column for entities that should generate DTO record classes. Foreign key attributes are only included in the DTO if the referenced entity also has a DTO.
-
Enable generation options
-
DTOs checkbox - Generate DTO records for selected entities
-
i18n checkbox - Generate resource bundle properties files
-
Test checkbox - Generate a JUnit test class for domain validation
-
-
Preview generated code
Select a tab to view generated code:
-
API / Impl - Split view showing separate API interface and implementation files
-
API Source Directory - Where to write the API interface (default: current directory)
-
Implementation Source Directory - Where to write the implementation class (default: current directory)
-
-
Combined - Single-file domain model
-
Combined Source Directory - Where to write the combined file (default: current directory)
-
-
i18n - Resource bundle properties (if enabled)
Use the search field to highlight occurrences of text in the code view. Click the … button next to any directory field to select a different output directory.
-
-
Configure output directories (optional)
Each tab has directory fields showing where files will be written. Click … to select a directory. The generator automatically converts absolute paths to relative paths from your working directory for portability.
-
Save to filesystem
Click Save on the appropriate tab to write files. Existing files trigger an overwrite confirmation dialog. Generated files can be copied to actual module directories as needed.
|
Tip
|
Use keyboard shortcuts Alt+1 through Alt+5 to navigate between major UI sections (schema table, entity table, tabs, etc.). |
1.6. Generated Output
1.6.1. File Structure
The generator writes files to the configured output directories. By default, files are written to simple directories in your working directory (e.g., api/, impl/, combined/), which can later be copied to actual module source directories as needed.
API/Implementation Mode
Generates separate API and implementation files. When configured with directories "api" and "impl":
api/src/main/java/<domain-package>/api/
└── <DomainName>.java # Public API interface
api/src/main/resources/<domain-package>/api/
└── <DomainName>$<EntityName>.properties # i18n resources (if enabled)
impl/src/main/java/<domain-package>/
└── <DomainName>Impl.java # Implementation class
impl/src/test/java/<domain-package>/
└── <DomainName>Test.java # JUnit test (if enabled)
|
Note
|
These files can be copied to separate Gradle/Maven modules (e.g., domain-api and domain-impl) for applications using RMI or HTTP connections where clients only need the API on their classpath.
|
package is.codion.manual.generator.apiimpl.api;
import is.codion.framework.domain.DomainType;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.attribute.Column;
import is.codion.framework.domain.entity.attribute.ForeignKey;
import static is.codion.framework.domain.DomainType.domainType;
public interface Store {
DomainType DOMAIN = domainType(Store.class);
interface Customer {
EntityType TYPE = DOMAIN.entityType("customer");
Column<Integer> ID = TYPE.integerColumn("id");
Column<String> NAME = TYPE.stringColumn("name");
Column<String> EMAIL = TYPE.stringColumn("email");
}
interface Order {
EntityType TYPE = DOMAIN.entityType("order");
Column<Integer> ID = TYPE.integerColumn("id");
Column<Integer> CUSTOMER_ID = TYPE.integerColumn("customer_id");
ForeignKey CUSTOMER_FK = TYPE.foreignKey("customer_fk", CUSTOMER_ID, Customer.ID);
}
}
package is.codion.manual.generator.apiimpl;
import is.codion.framework.domain.DomainModel;
import is.codion.framework.domain.entity.EntityDefinition;
import is.codion.manual.generator.apiimpl.api.Store.Customer;
import is.codion.manual.generator.apiimpl.api.Store.Order;
import static is.codion.framework.domain.entity.attribute.Column.Generator.identity;
import static is.codion.manual.generator.apiimpl.api.Store.DOMAIN;
public final class StoreImpl extends DomainModel {
public StoreImpl() {
super(DOMAIN);
add(customer(), order());
}
static EntityDefinition customer() {
return Customer.TYPE.define(
Customer.ID.define()
.primaryKey()
.generator(identity()),
Customer.NAME.define()
.column()
.caption("Name")
.nullable(false)
.maximumLength(100),
Customer.EMAIL.define()
.column()
.caption("Email")
.maximumLength(255))
.caption("Customer")
.build();
}
static EntityDefinition order() {
return Order.TYPE.define(
Order.ID.define()
.primaryKey()
.generator(identity()),
Order.CUSTOMER_ID.define()
.column(),
Order.CUSTOMER_FK.define()
.foreignKey()
.caption("Customer"))
.caption("Order")
.build();
}
}
Combined Mode
Generates a single class containing both API and implementation. When configured with directory "combined":
combined/src/main/java/<domain-package>/
└── <DomainName>.java # Combined API + implementation
combined/src/main/resources/<domain-package>/
└── <DomainName>$<EntityName>.properties # i18n resources (if enabled)
combined/src/test/java/<domain-package>/
└── <DomainName>Test.java # JUnit test (if enabled)
|
Note
|
This mode is suitable for simpler projects using only local JDBC connections. The generated directory can be copied directly to your application’s source tree. |
package is.codion.manual.generator;
import is.codion.framework.domain.DomainModel;
import is.codion.framework.domain.DomainType;
import is.codion.framework.domain.entity.EntityDefinition;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.attribute.Column;
import is.codion.framework.domain.entity.attribute.ForeignKey;
import static is.codion.framework.domain.DomainType.domainType;
public final class Store extends DomainModel {
public static final DomainType DOMAIN = domainType(Store.class);
public Store() {
super(DOMAIN);
add(customer(), order());
}
public interface Customer {
EntityType TYPE = DOMAIN.entityType("customer");
Column<Integer> ID = TYPE.integerColumn("id");
Column<String> NAME = TYPE.stringColumn("name");
// ...
}
public interface Order {
EntityType TYPE = DOMAIN.entityType("order");
Column<Integer> ID = TYPE.integerColumn("id");
Column<Integer> CUSTOMER_ID = TYPE.integerColumn("customer_id");
ForeignKey CUSTOMER_FK = TYPE.foreignKey("customer_fk", CUSTOMER_ID, Customer.ID);
// ...
}
static EntityDefinition customer() {
return Customer.TYPE.define(/* ... */).build();
}
static EntityDefinition order() {
return Order.TYPE.define(/* ... */).build();
}
}
1.7. Schema Introspection
The codion-framework-domain-db module deals with introspecting database metadata using JDBC DatabaseMetaData and applies schema settings to generate appropriate domain model configurations.
1.7.1. Column Mapping
Database column metadata is transformed into Codion column definitions:
| Database Metadata | Generated Configuration |
|---|---|
Primary key column |
|
Auto-increment column |
|
NOT NULL constraint |
|
VARCHAR(n) size |
|
DECIMAL(p,s) scale |
|
Column default value |
|
Column comment |
|
1.7.2. Foreign Key Detection
Foreign key constraints in the database schema are automatically detected and transformed into ForeignKey constants and definitions:
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id)
);
interface Order {
EntityType TYPE = DOMAIN.entityType("orders");
Column<Integer> ID = TYPE.integerColumn("id");
Column<Integer> CUSTOMER_ID = TYPE.integerColumn("customer_id");
ForeignKey CUSTOMER_FK = TYPE.foreignKey("customer_fk", CUSTOMER_ID, Customer.ID);
}
Order.CUSTOMER_FK.define()
.foreignKey()
.caption("Customer")
Composite foreign keys are supported - the generator detects multi-column foreign key constraints and generates appropriate multi-reference ForeignKey definitions.
1.7.3. View Handling
Database views are automatically marked as read-only entities:
EntityDefinition customerSummary() {
return CustomerSummary.TYPE.define(/* ... */)
.caption("Customer summary")
.readOnly(true) // Automatically added for views
.build();
}
1.7.4. Naming Conventions
The generator applies consistent naming transformations:
| Database Name | Generated Name |
|---|---|
Table: |
EntityType: |
Column: |
Column constant: |
Column: |
ForeignKey constant: |
View: |
EntityType: |
1.8. DTO Generation
Data Transfer Object (DTO) generation creates Java record classes for entities, providing a lightweight alternative to the full Entity framework for data transfer scenarios.
1.8.1. When to Generate DTOs
Enable DTO generation for entities that:
-
Need to be serialized for REST APIs or messaging systems
-
Represent simple data structures without complex Entity behaviors
-
Are frequently transferred between application layers
See Chinook demo
1.8.2. DTO Structure
For each entity with DTO generation enabled, the generator creates a nested Dto record within the entity interface:
interface Customer {
EntityType TYPE = DOMAIN.entityType("customer");
Column<Integer> ID = TYPE.integerColumn("id");
Column<String> NAME = TYPE.stringColumn("name");
Column<String> EMAIL = TYPE.stringColumn("email");
// Generated DTO record
public static record Dto(
Integer id,
String name,
String email) {
public Entity entity(Entities entities) {
return entities.entity(TYPE)
.with(ID, id)
.with(NAME, name)
.with(EMAIL, email)
.build();
}
}
// Conversion method
public static Dto dto(Customer customer) {
return customer == null ? null :
new Dto(
customer.get(ID),
customer.get(NAME),
customer.get(EMAIL));
}
}
1.8.3. Foreign Key DTOs
When an entity with a DTO references another entity via foreign key, the foreign key attribute is included in the DTO only if the referenced entity also has DTO generation enabled. The generator creates nested DTOs for included foreign keys:
interface Order {
Column<Integer> CUSTOMER_ID = TYPE.integerColumn("customer_id");
ForeignKey CUSTOMER_FK = TYPE.foreignKey("customer_fk",
CUSTOMER_ID, Customer.ID);
public static record Dto(
Integer id,
Customer.Dto customer) { // Nested DTO reference (only if Customer has DTO)
public Entity entity(Entities entities) {
return entities.entity(TYPE)
.with(ID, id)
.with(CUSTOMER_FK, customer.entity(entities))
.build();
}
}
}
|
Note
|
Foreign keys are selectively included - if Order references Customer and Customer is marked for DTO generation, Order’s DTO will include Customer.Dto. If Customer is not marked for DTOs, the CUSTOMER_FK attribute is simply excluded from Order’s DTO.
|
1.9. Internationalization
When i18n generation is enabled, the generator creates resource bundle property files for entity and attribute captions and descriptions.
1.9.1. Generated Properties Format
customer=Customer
customer.description=Customer master data
id=Id
name=Name
email=Email
email.description=Customer email address
The generator creates one properties file per entity, using the naming convention <DomainName>$<EntityName>.properties.
1.9.2. i18n Mode vs Literal Mode
The generator supports two caption strategies:
- Literal Mode (i18n disabled)
-
Captions and descriptions are embedded directly in the generated code:
Customer.NAME.define() .column() .caption("Name") .description("Customer full name") - i18n Mode (i18n enabled)
-
Captions and descriptions are loaded from resource bundles:
// EntityType with resource bundle reference EntityType TYPE = DOMAIN.entityType("customer", Customer.class.getName()); // No caption() or description() calls - loaded from properties Customer.NAME.define() .column() .nullable(false)
The framework automatically loads captions and descriptions from the properties file matching the fully qualified class name of the entity interface.
|
Note
|
When using i18n mode, create additional properties files with locale suffixes (e.g., Store$Customer_de.properties, Store$Customer_fr.properties) for internationalization support.
|
1.10. Test Generation
When test generation is enabled, the generator creates a JUnit test class that extends DomainTest to verify domain model integrity. The test class includes a test method per entity that exercises full CRUD operations and validates constraints. The test may need further configuration to run successfully.
For further information see Domain model testing.
1.11. Best Practices
1.11.1. Choosing Output Mode
Use API/Implementation separation when:
-
Building applications with RMI or HTTP connections
-
Multiple client applications share the same domain API
-
You want lighter client classpaths (API only, no implementation)
-
Following strict architectural separation
Use Combined mode when:
-
Building simple local-JDBC applications
-
The domain model is small (<20 entities)
-
You prefer fewer files to maintain
-
Deployment simplicity outweighs architectural purity
1.11.2. Output Directory Strategy
The generator can write to any directory (relative or absolute paths). A simple workflow:
-
Generate to local directories - Use simple relative paths like
"api","impl", or"combined"in your working directory -
Review and customize - Examine the generated code, make any immediate adjustments
-
Copy to modules - If using separate modules, copy the generated directories to your module source trees
This approach keeps the generator configuration simple while supporting any project structure. There’s no need to configure the generator to write directly into complex module hierarchies.
|
Tip
|
Domain generation is typically a one-shot operation. Generating to simple local directories and copying files manually provides more control and flexibility than trying to configure direct module paths. |
1.11.3. DTO Selection Strategy
-
Enable DTOs for entities that cross architectural boundaries (e.g., REST APIs, messaging systems)
-
If you want nested foreign key references in DTOs, enable DTOs for those referenced entities as well
-
Foreign keys to entities without DTOs are simply excluded from the DTO
-
Not every entity needs a DTO - be selective based on your application’s needs
1.11.4. Schema Settings Workflow
-
Connect to database
-
Select schema but don’t populate yet
-
Right-click → Settings to configure naming conventions
-
Now populate schema with Cmd+Enter or double-click
-
Settings are saved in user preferences and reused next time
1.11.5. Version Control
-
Commit generated code - It’s source code, not build artifacts
-
Customize after generation - The generator output is a starting point
-
Don’t regenerate blindly - Manual customizations will be lost
-
Use version control to track changes - Diff generated vs customized code
1.11.6. Customization Pattern
The generator produces standard Codion domain models. After generation, customize as needed:
-
Add EntityValidator implementations for business rules
-
Define derived attributes for calculated values
-
Add denormalized attributes for performance optimization
-
Configure foreign key fetch depth with
referenceDepth() -
Implement custom toString() formatters
-
Add custom condition types for complex queries
|
Tip
|
Generate once, customize as needed, and use version control to preserve your customizations. Don’t treat the generator as a round-trip tool. |
1.11.7. Database Support
The generator works with any JDBC-compliant database. Tested databases include:
-
H2 Database (in-memory and file-based)
-
PostgreSQL 12+
-
Oracle Database 11g+
-
MariaDB 10.3+
-
MySQL 8.0+ (via MariaDB driver)
Each database requires its appropriate JDBC driver on the runtime classpath. See Chinook demo.
|
Important
|
When using H2 with init scripts, H2 does not allow path traversal. Use absolute paths for codion.db.initScripts.
|
1.12. Keyboard Navigation
The generator UI supports keyboard-driven workflows:
| Shortcut | Action |
|---|---|
Alt+1 - Alt+5 |
Navigate between UI sections (schema table, entity table, tabs, etc.) |
Cmd+Enter / Ctrl+Enter |
Populate selected schema |
Ctrl+F |
Focus search field in code view |
Cmd+C / Ctrl+C |
Copy code to clipboard (when code view focused) |
|
Tip
|
The search field in code tabs highlights all occurrences of the search term, making it easy to locate specific entities or attributes in generated code. |
1.13. Examples
For complete working examples:
-
Configuration: Chinook demo
-
Generated Code:
tools/generator/domain/src/test/resources/(Chinook, World, Petstore) -
Hand-crafted Domains: See domain model sections in demo tutorials:
The generator produces code that follows the same patterns as these hand-crafted examples, although the structure may differ, making it easy to compare generated vs manually-written domain models.