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:

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:

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:

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:

Type Safety Throughout

Compile-time safety eliminates entire classes of runtime errors:

Performance by Design

Domain definitions enable framework optimizations:

Framework Integration

Domain models integrate seamlessly with all framework layers:

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:

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.