Domain Types
Define your domain model once. Use it everywhere.
Domain types are the core feature of Typr Bridge. Each domain type is grounded in a primary source (usually a database table) and can be aligned to other source entities. Typr generates the type plus all conversion methods automatically.
The Problemβ
You have multiple representations of the same concept:
// From PostgreSQL (generated by Typr DB)
public record CustomerRow(CustomerId id, FirstName firstName, LastName lastName,
Email email, Instant createdAt, Instant updatedAt) {}
// From MariaDB (generated by Typr DB)
public record CustomersRow(CustomerId customerId, FirstName firstName, LastName lastName,
Email email) {}
// From your API (generated by Typr API)
public record CustomerDto(CustomerId id, FirstName firstName, LastName lastName, Email email) {}
Same concept, three types. You write the mapping code:
// You write this. For every entity. Forever.
Customer fromPostgres(CustomerRow row) {
return new Customer(row.id(), row.firstName(), row.lastName(), row.email());
}
Customer fromMariadb(CustomersRow row) {
return new Customer(row.customerId(), row.firstName(), row.lastName(), row.email());
}
CustomerDto toApi(Customer c) {
return new CustomerDto(c.id(), c.firstName(), c.lastName(), c.email());
}
The Solutionβ
Define a domain type grounded in a primary source, with alignments to other sources:
domainTypes:
Customer:
primary: postgres:sales.customer # The anchor source
fields:
id: CustomerId
firstName: FirstName
lastName: LastName
email: Email? # ? means optional/nullable
alignedSources:
mariadb:customers: superset # Aligned to another DB
api:Customer: exact # Aligned to API contract
Typr generates your domain type with all converters:
public record Customer(
CustomerId id,
FirstName firstName,
LastName lastName,
Optional<Email> email
) {
// From sources
public static Customer fromPostgres(CustomerRow row) {
return new Customer(row.id(), row.firstName(), row.lastName(), row.email());
}
public static Customer fromMariadb(CustomersRow row) {
return new Customer(row.customerId(), row.firstName(), row.lastName(), row.email());
}
// To sources
public CustomerRowUnsaved toPostgresUnsaved(Instant createdAt, Instant updatedAt) {
return new CustomerRowUnsaved(id, firstName, lastName, email, createdAt, updatedAt);
}
public CustomerDto toApi() {
return new CustomerDto(id, firstName, lastName, email);
}
}
Your service becomes trivial:
public CustomerDto getCustomer(CustomerId id) {
CustomerRow row = customerRepo.selectById(id, conn);
return Customer.fromPostgres(row).toApi();
}
Defining Domain Typesβ
Basic Structureβ
domainTypes:
TypeName:
primary: source:entity # The anchor source (optional)
fields:
fieldName: FieldType
optionalField: FieldType? # ? = optional
alignedSources: # Other sources aligned to this type
source:entity: mode
Primary Sourceβ
The primary source is the anchor entity that your domain type is based on. It determines the canonical field names and types:
domainTypes:
Customer:
primary: postgres:sales.customer
fields: { ... }
When you specify a primary source:
- Fields are inferred from that source if not explicitly defined
- Type compatibility is validated against the primary
- The primary source always has implicit
supersetalignment
Fieldsβ
Fields define the canonical shape of your domain type:
fields:
id: CustomerId # Your field type (see Field Types)
firstName: FirstName # Your field type
count: Int # Built-in type
active: Boolean # Built-in type
score: BigDecimal # Built-in type
email: Email? # ? makes it optional (nullable)
Built-in types: String, Int, Long, Double, Boolean, BigDecimal, UUID, Instant, LocalDate, LocalDateTime
Optional fields: Add ? suffix to make a field nullable. This generates Optional<T> in Java, Option[T] in Scala, and T? in Kotlin.
Aligned Sourcesβ
Aligned sources map your domain type to entities in other sources beyond the primary.
Aligned Source Syntaxβ
alignedSources:
source:entity: mode
Where:
- source - The name of your data source (from
sources:section) - entity - The table, view, or model name in that source
- mode - How the fields align (
exact,superset, orsubset)
Alignment Modesβ
| Mode | Meaning | Use Case |
|---|---|---|
exact | Fields must match exactly | API contracts, strict validation |
superset | Source may have additional fields | Database tables (audit columns, timestamps) |
subset | Source may have fewer fields | Read-only views, summary endpoints |
superset is the most common mode for databases, since tables typically have extra columns like created_at, updated_at, or audit fields that aren't part of your domain model.
exact is typical for API contracts where you want strict alignment.
subset is useful for views or summary responses that omit some fields.
Examplesβ
domainTypes:
Customer:
primary: postgres:sales.customer # Primary is always implicitly superset
fields: { ... }
alignedSources:
# Legacy database with different naming
mariadb:legacy.customers:
mode: superset
mappings:
id: cust_id
email: electronic_mail
# API must match exactly
api:Customer: exact
# Summary endpoint omits some fields
api:CustomerSummary: subset
Field Alignmentβ
Typr automatically aligns fields between your domain type and each aligned source using normalized names.
Automatic Alignmentβ
Fields align automatically when names match after normalization:
Domain Field PostgreSQL MariaDB API
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
id β customer_id β customer_id β id
firstName β first_name β first_name β firstName
lastName β last_name β last_name β lastName
email β email β email β email
Typr normalizes names by removing underscores and comparing case-insensitively, so firstName, first_name, and FirstName all match.
Explicit Field Mappingsβ
When names don't align automatically, specify explicit mappings:
alignedSources:
mariadb:legacy.customers:
mode: superset
mappings:
id: cust_identifier # Domain 'id' maps to 'cust_identifier'
email: electronic_mail # Domain 'email' maps to 'electronic_mail'
Excluding Fieldsβ
To exclude certain source fields from a superset alignment:
alignedSources:
postgres:sales.customer:
mode: superset
exclude: [internal_notes, admin_flags]
Including Extra Fieldsβ
For toSource converters when the source requires additional fields:
alignedSources:
postgres:sales.customer:
mode: superset
includeExtra: [createdAt, updatedAt]
This generates converter methods that require these extra fields as parameters:
public CustomerRowUnsaved toPostgresUnsaved(Instant createdAt, Instant updatedAt) {
return new CustomerRowUnsaved(id, firstName, lastName, email, createdAt, updatedAt);
}
Compatibility Validationβ
Typr validates that your domain type is compatible with all aligned sources at generation time:
Domain type 'Customer' has incompatible aligned sources:
Field 'id':
mariadb:legacy_customers.cust_id β String (expected Long from primary)
Fix: Ensure all sources use compatible underlying types, or use
a custom field type to bridge the difference
Schema mismatches become compile-time errors, not production bugs.
Generated Codeβ
The Domain Typeβ
Typr generates a domain type record/data class/case class:
/**
* Domain type 'Customer'
* Primary: postgres:sales.customer
* Aligned: mariadb:customers (superset), api:Customer (exact)
*/
public record Customer(
CustomerId id,
FirstName firstName,
LastName lastName,
Optional<Email> email
) {
// Converters...
}
From-Source Convertersβ
For the primary and each aligned source, Typr generates a fromSource method:
public static Customer fromPostgres(CustomerRow row) { ... }
public static Customer fromMariadb(CustomersRow row) { ... }
To-Source Convertersβ
For non-readonly aligned sources, Typr generates toSource methods:
// exact mode - direct conversion
public CustomerDto toApi() { ... }
// superset mode - requires extra fields
public CustomerRowUnsaved toPostgresUnsaved(Instant createdAt, Instant updatedAt) { ... }
Readonly Sourcesβ
Mark an aligned source as readonly to skip generating toSource converters:
alignedSources:
postgres:sales.customer_view:
mode: subset
readonly: true
Complete Exampleβ
domainTypes:
Customer:
primary: postgres:sales.customer # Primary source
fields:
id: CustomerId
firstName: FirstName
lastName: LastName
email: Email?
active: Boolean
alignedSources:
postgres:sales.customer: # Can also specify primary explicitly with options
includeExtra: [createdAt, updatedAt]
mariadb:legacy.customers:
mode: superset
mappings:
id: cust_id
active: is_active
api:Customer: exact
api:CustomerSummary:
mode: subset
readonly: true
Order:
primary: postgres:sales.order
fields:
id: OrderId
customerId: CustomerId
total: BigDecimal
status: OrderStatus
alignedSources:
api:Order: exact
Using with Field Typesβ
Domain types work best when combined with Field Types. Field types give you type safety at the individual field level (CustomerId vs OrderId), while domain types give you type safety at the record level (Customer vs CustomerRow).
# Define field types for type safety
fieldTypes:
CustomerId:
db: { column: [customer_id], primary_key: true }
model: { name: [customerId] }
FirstName:
db: { column: [first_name, firstname] }
model: { name: [firstName] }
# Use them in domain types
domainTypes:
Customer:
fields:
id: CustomerId # Can't accidentally pass an OrderId
firstName: FirstName # Can't accidentally pass a LastName
...
Next Stepsβ
- Field Types - Add field-level type safety
- Configuration - Complete YAML reference