Domain-Driven Design in Practice: Type-Safe Entity Modeling
How Codion transforms business domains into executable, testable, maintainable code
Beyond ORM: Entity-Relationship Thinking
Most frameworks approach domain modeling through Object-Relational Mapping (ORM), treating the database as a persistence detail and forcing object-oriented thinking onto relational data. Codion takes a different approach: embrace the relational model and make it type-safe.
Instead of hiding database concepts behind object abstractions, Codion makes entities, attributes, foreign keys, and queries first-class citizens in your domain model. Code directly expresses business relationships while maintaining compile-time safety and runtime performance. Discover the philosophy behind this approach →
The Domain Architecture
Domain Types: Namespacing Business Contexts
Every Codion domain starts with a domain type that provides namespace isolation:
// From Chinook demo - music store domain
public interface Chinook {
DomainType DOMAIN = domainType(Chinook.class);
}
// From World demo - geographic data domain
public interface World {
DomainType DOMAIN = DomainType.domainType(World.class);
}
// From Petclinic demo - veterinary clinic domain
public final class Petclinic extends DomainModel {
public static final DomainType DOMAIN = domainType("Petclinic");
}
Domain types enable multiple domain models to coexist in the same application without namespace collisions. Each domain encapsulates its own business context and rules.
Entity API: Declarative Structure Definition
Entity APIs define the structure and relationships of your business entities using a declarative syntax:
// From Petclinic demo - simple entity definition
public interface Vet {
EntityType TYPE = DOMAIN.entityType("petclinic.vet");
Column<Integer> ID = TYPE.integerColumn("id");
Column<String> FIRST_NAME = TYPE.stringColumn("first_name");
Column<String> LAST_NAME = TYPE.stringColumn("last_name");
}
This isn’t just metadata - it’s executable code that defines type-safe accessors for entity attributes. The framework uses these definitions to generate all CRUD operations, validations, and UI components.
Type-Safe Column Definitions
Codion provides strongly-typed column definitions that map directly to database types:
// From Chinook demo - various column types
interface Customer {
EntityType TYPE = DOMAIN.entityType("chinook.customer");
Column<Long> ID = TYPE.longColumn("id");
Column<String> FIRSTNAME = TYPE.stringColumn("firstname");
Column<String> EMAIL = TYPE.stringColumn("email");
Column<LocalDate> BIRTHDATE = TYPE.localDateColumn("birthdate");
Column<BigDecimal> CREDITLIMIT = TYPE.bigDecimalColumn("creditlimit");
Column<Boolean> ACTIVE = TYPE.booleanColumn("active");
Column<byte[]> AVATAR = TYPE.byteArrayColumn("avatar");
}
Each column type provides compile-time safety and appropriate UI component generation. No runtime type checking or casting required.
Foreign Keys as First-Class Citizens
Declarative Relationship Definition
Unlike ORMs that treat relationships as afterthoughts, Codion makes foreign keys central to domain modeling:
// From Petclinic demo - many-to-many relationship
public interface VetSpecialty {
EntityType TYPE = DOMAIN.entityType("petclinic.vet_specialty");
Column<Integer> VET = TYPE.integerColumn("vet");
Column<Integer> SPECIALTY = TYPE.integerColumn("specialty");
ForeignKey VET_FK = TYPE.foreignKey("vet_fk", VET, Vet.ID);
ForeignKey SPECIALTY_FK = TYPE.foreignKey("specialty_fk", SPECIALTY, Specialty.ID);
}
Foreign keys define the relationship structure declaratively. The framework automatically handles:
- Join queries for navigation
- Referential integrity validation
- Cascade operations
- UI filtering and combo box population
Automatic Entity Loading
Foreign key relationships enable automatic loading of related entities:
// From Chinook demo - complex entity relationships
interface Album {
EntityType TYPE = DOMAIN.entityType("chinook.album");
Column<Long> ARTIST_ID = TYPE.longColumn("artist_id");
ForeignKey ARTIST_FK = TYPE.foreignKey("artist_fk", ARTIST_ID, Artist.ID);
}
// Usage - artist automatically loaded with album
Entity album = connection.selectSingle(Album.ID.equalTo(42));
Entity artist = album.get(Album.ARTIST_FK); // No separate query needed
String artistName = artist.get(Artist.NAME);
The framework optimizes queries to fetch related entities efficiently, eliminating N+1 query problems.
Reference Depth Control
Control how deep the framework follows foreign key chains:
// From Chinook demo - controlling relationship depth
Album.ARTIST_FK.define()
.foreignKey()
.attributes(Artist.NAME) // Fetch artist name with album
.referenceDepth(1); // Don't follow artist's relationships
Track.ALBUM_FK.define()
.foreignKey()
.referenceDepth(2); // Track → Album → Artist (follow chain)
This provides fine-grained control over query performance and data loading patterns.
Custom Types and Domain Modeling
Value Objects
Codion supports custom value types that represent domain concepts:
// From World demo - geographic location value type
record Location(double latitude, double longitude) implements Serializable {
@Override
public String toString() {
return "[" + latitude + "," + longitude + "]";
}
}
// Column definition with custom type
interface City {
EntityType TYPE = DOMAIN.entityType("world.city");
Column<Location> LOCATION = TYPE.column("location", Location.class);
}
Custom types provide domain-specific abstractions while maintaining database mapping flexibility.
Type Converters
Bridge between domain types and database representations:
// From World demo - location stored as string in database
City.LOCATION.define()
.column()
.caption("Location")
.converter(String.class, new LocationConverter())
.comparator(new LocationComparator())
Converters handle the transformation between domain objects and database storage formats transparently.
Enum Support
Type-safe enum integration with database storage:
// From Petclinic demo - enum column definition
public interface Owner {
Column<PhoneType> PHONE_TYPE = TYPE.column("phone_type", PhoneType.class);
enum PhoneType {
MOBILE, HOME, WORK
}
}
Enums provide type safety and enable automatic UI generation (combo boxes, radio buttons).
Entity Implementation: Configuration over Code
Declarative Entity Configuration
Entity implementations configure behavior through declarative builders:
// From Petclinic demo - vet entity implementation
private EntityDefinition vet() {
return Vet.TYPE.define(
Vet.ID.define()
.primaryKey(),
Vet.FIRST_NAME.define()
.column()
.caption("First name")
.searchable(true)
.maximumLength(30)
.nullable(false),
Vet.LAST_NAME.define()
.column()
.caption("Last name")
.searchable(true)
.maximumLength(30)
.nullable(false))
.keyGenerator(identity())
.caption("Vets")
.stringFactory(StringFactory.builder()
.value(Vet.LAST_NAME)
.text(", ")
.value(Vet.FIRST_NAME)
.build())
.orderBy(ascending(Vet.LAST_NAME, Vet.FIRST_NAME))
.smallDataset(true)
.build();
}
This configuration drives:
- UI component generation and validation
- Search and filtering behavior
- Display formatting and ordering
- Performance optimizations (smallDataset hint)
Advanced Column Configuration
Columns support rich metadata that controls behavior throughout the application:
// From Chinook demo - employee entity with comprehensive configuration
Employee.EMAIL.define()
.column()
.searchable(true) // Include in search operations
.nullable(false) // Validation constraint
.maximumLength(60) // Length validation
.validator(new EmailValidator()) // Custom validation
Column metadata automatically applies to:
- Form field generation and validation
- Table column behavior and formatting
- Search and filter operations
- Report generation
Derived Attributes and Calculations
Subquery Columns
Define calculated columns using database subqueries:
// From Chinook demo - calculated artist statistics
Artist.NUMBER_OF_ALBUMS.define()
.subquery("""
SELECT COUNT(*)
FROM chinook.album
WHERE album.artist_id = artist.id""");
Artist.NUMBER_OF_TRACKS.define()
.subquery("""
SELECT COUNT(*)
FROM chinook.track
JOIN chinook.album ON track.album_id = album.id
WHERE album.artist_id = artist.id""");
Subqueries provide efficient calculated values without complex object mapping.
Derived Attributes
Calculate values from other entity attributes:
// From World demo - speaker count calculation
CountryLanguage.NO_OF_SPEAKERS.define()
.derived(CountryLanguage.COUNTRY_FK, CountryLanguage.PERCENTAGE)
.provider(new NoOfSpeakersProvider())
.caption("No. of speakers")
.numberFormatGrouping(true);
class NoOfSpeakersProvider implements DerivedAttribute.Provider<Integer> {
@Override
public Integer get(SourceValues values) {
Double percentage = values.get(CountryLanguage.PERCENTAGE);
Entity country = values.get(CountryLanguage.COUNTRY_FK);
if (percentage != null && country != null) {
Integer population = country.get(Country.POPULATION);
return (int)(population * (percentage / 100));
}
return null;
}
}
Derived attributes automatically recalculate when source values change.
Denormalized Attributes
Access denormalized values from related entities:
// From World demo - fetch capital population from city
Country.CAPITAL_POPULATION.define()
.denormalized(Country.CAPITAL_FK, City.POPULATION);
Denormalized attributes provide performance optimizations for commonly accessed related data.
Domain Validation and Business Rules
Entity-Level Validation
Implement business rules through domain validators:
// From World demo - cross-entity validation
class CityValidator extends DefaultEntityValidator {
@Override
public <T> void validate(Entity city, Attribute<T> attribute) {
super.validate(city, attribute);
if (attribute.equals(City.POPULATION)) {
Integer cityPopulation = city.get(City.POPULATION);
Entity country = city.get(City.COUNTRY_FK);
Integer countryPopulation = country.get(Country.POPULATION);
if (countryPopulation != null && cityPopulation > countryPopulation) {
throw new ValidationException(City.POPULATION, cityPopulation,
"City population can not exceed country population");
}
}
}
}
Validators enforce business rules across entity relationships with full type safety.
Column-Level Constraints
Define constraints at the column level for automatic validation:
// From World demo - column constraints
Country.INDEPYEAR.define()
.column()
.caption("Indep. year")
.valueRange(-2000, 2500); // Automatic range validation
Country.SURFACEAREA.define()
.column()
.nullable(false) // Required field
.numberFormatGrouping(true)
.maximumFractionDigits(2); // Decimal precision
Column constraints automatically apply throughout the application without duplication.
Domain Functions and Procedures
Database Function Integration
Expose database functions through the domain model:
// From Chinook demo - business operation functions
interface Track {
FunctionType<EntityConnection, RaisePriceParameters, Collection<Entity>> RAISE_PRICE =
functionType("chinook.raise_price");
record RaisePriceParameters(Collection<Long> trackIds, BigDecimal priceIncrease)
implements Serializable {}
}
// From World demo - calculation function
interface Country {
FunctionType<EntityConnection, String, Double> AVERAGE_CITY_POPULATION =
functionType("average_city_population");
}
Domain functions provide type-safe access to database procedures and business operations.
Custom Condition Types
Define parameterized query conditions for complex business queries:
// From Chinook demo - custom query conditions
interface Track {
ConditionType NOT_IN_PLAYLIST = TYPE.conditionType("not_in_playlist");
}
// Usage with parameters
List<Long> classicalPlaylistIds = List.of(42L, 43L);
Condition noneClassical = Track.NOT_IN_PLAYLIST.get(Playlist.ID, classicalPlaylistIds);
List<Entity> tracks = connection.select(noneClassical);
Custom conditions encapsulate complex business queries with type safety.
Advanced Domain Patterns
Multi-Domain Applications
Applications can include multiple domain models:
// Combine multiple business contexts
SwingEntityApplicationModel applicationModel = new SwingEntityApplicationModel(
connectionProvider,
List.of(
new CustomerManagementModel(connectionProvider), // CRM domain
new InventoryModel(connectionProvider), // Inventory domain
new AccountingModel(connectionProvider) // Financial domain
),
VERSION);
Each domain maintains its own namespace and business rules while sharing infrastructure.
Domain Model Composition
Large domains can be composed from smaller modules:
// From Chinook demo - modular domain construction
public final class ChinookImpl extends DomainModel implements Chinook {
public ChinookImpl() {
super(DOMAIN);
add(artist(), album(), employee(), customer(), genre(),
track(), invoice(), invoiceLine(), playlist(), playlistTrack());
add(Customer.REPORT, classPathReport(ChinookImpl.class, "customer_report.jasper"));
add(Track.RAISE_PRICE, new RaisePriceFunction());
add(Invoice.UPDATE_TOTALS, new UpdateTotalsFunction());
}
}
Domain models compose entity definitions, functions, and reports into cohesive business contexts.
Cross-Entity Queries
Complex business queries span multiple entities:
// From World demo - lookup tables for reporting
interface Lookup {
EntityType TYPE = DOMAIN.entityType("world.country_city_lookup");
// Denormalized view combining country and city data
Column<String> COUNTRY_CODE = TYPE.stringColumn("country.code");
Column<String> COUNTRY_NAME = TYPE.stringColumn("country.name");
Column<String> CITY_NAME = TYPE.stringColumn("city.name");
Column<Integer> CITY_POPULATION = TYPE.integerColumn("city.population");
Column<Location> CITY_LOCATION = TYPE.column("city.location", Location.class);
}
Lookup entities provide optimized access patterns for complex queries and reporting.
Configuration and Metadata
Entity Metadata
Rich metadata controls framework behavior:
// From World demo - comprehensive entity configuration
.keyGenerator(sequence("world.city_seq")) // Key generation strategy
.validator(new CityValidator()) // Business rule validation
.orderBy(ascending(City.NAME)) // Default ordering
.stringFactory(City.NAME) // Display representation
.description("Cities of the World") // Documentation
.caption("City") // UI labels
.build();
Metadata drives UI generation, validation, and data access patterns throughout the application.
Performance Optimizations
Domain definitions include performance hints:
// From Petclinic demo - performance optimizations
.smallDataset(true) // Hint to the UI: safe for a combo box
.selected(false) // Lazy load expensive columns
.readOnly(true) // Optimize for read-only access
Performance hints guide framework optimizations without affecting business logic.
Testing Domain Models
Domain-First Testing
Test business logic directly against the domain model:
// Test entity relationships and validation
Entity vet = entities.builder(Vet.TYPE)
.value(Vet.FIRST_NAME, "James")
.value(Vet.LAST_NAME, "Carter")
.build();
Entity specialty = entities.builder(Specialty.TYPE)
.value(Specialty.NAME, "Cardiology")
.build();
// Test foreign key relationships
Entity vetSpecialty = entities.builder(VetSpecialty.TYPE)
.value(VetSpecialty.VET_FK, vet)
.value(VetSpecialty.SPECIALTY_FK, specialty)
.build();
assertTrue(vetSpecialty.get(VetSpecialty.VET_FK).equals(vet));
Domain models provide a testable foundation independent of UI or persistence details.
Integration Testing
Test complete domain behavior with in-memory databases:
// From demo test patterns
@Test
void testCityValidation() {
Entity country = connection.insertSelect(
entities.builder(Country.TYPE)
.value(Country.CODE, "IS")
.value(Country.NAME, "Iceland")
.value(Country.POPULATION, 350000)
.build());
Entity city = entities.builder(City.TYPE)
.value(City.NAME, "Reykjavik")
.value(City.COUNTRY_FK, country)
.value(City.POPULATION, 400000) // Exceeds country population
.build();
assertThrows(ValidationException.class, () -> connection.insert(city));
}
Integration tests verify business rules and relationships with real database semantics.
Benefits of Domain-Driven Design
Business Logic Centralization
Domain models centralize business rules and relationships:
- Single source of truth - Entity definitions drive all application behavior
- Consistency - Business rules apply uniformly across UI, API, and batch operations
- Maintainability - Changes in one place affect the entire application
- Testability - Business logic is independent of infrastructure concerns
Type Safety Throughout
Compile-time safety eliminates entire classes of runtime errors:
- No magic strings - All database access is type-checked
- Refactoring support - IDE refactoring works across the entire codebase
- Early error detection - Mismatched types caught at compile time
- IntelliSense support - Full IDE completion for all domain operations
Performance by Design
Domain definitions enable framework optimizations:
- Query optimization - Framework generates efficient queries based on metadata
- Small datasets - Combo box based by default in a UI
- Lazy loading - Expensive columns loaded only when needed
- Batch operations - Framework batches related operations automatically
Framework Integration
Domain models integrate seamlessly with all framework layers:
- UI generation - Forms and tables generated from entity definitions
- Validation - Business rules automatically enforced in UI
- Reporting - Reports query domain entities directly
- API exposure - REST/GraphQL APIs generated from domain models
Conclusion: Domain as Foundation
Codion’s domain-driven approach treats the domain model as the foundation of the entire application architecture. Instead of treating the database as a persistence detail, it embraces relational thinking and makes it type-safe and productive.
This approach:
- Eliminates impedance mismatch - No translation between object and relational models
- Reduces complexity - One model drives everything instead of multiple representations
- Improves maintainability - Business logic lives in one place with clear ownership
- Enables productivity - Rich metadata generates most application infrastructure automatically
- Supports evolution - Domain changes propagate throughout the application automatically
The result is applications where business domain concepts are first-class citizens, not implementation details hidden behind abstractions. Domain experts can read and understand the entity definitions, developers can reason about business relationships directly, and the framework handles the mechanical work of CRUD operations, UI generation, and data access.
When the domain model accurately reflects the business reality, everything else becomes an implementation detail that the framework can handle automatically. This is the power of true domain-driven design in practice.
For developers interested in exploring Codion’s domain modeling capabilities, start with the simple Petclinic entities to understand basic patterns, progress to the World demo for advanced features like custom types and validation, and examine the Chinook demo for complex business domains with functions, reports, and sophisticated relationships.