Domain Modeling with Kastor Gen
This tutorial covers best practices for creating domain interfaces that work well with Kastor Gen’s side-channel architecture.
Principles of Domain Modeling
1. Keep Interfaces Pure
Domain interfaces should contain only business logic and use pure Kotlin types:
// ✅ Good - Pure domain interface
@Rdf(iri = "http://xmlns.com/foaf/0.1/Person")
interface Person {
@Rdf(iri = "http://xmlns.com/foaf/0.1/name")
val name: List<String>
@Rdf(iri = "http://xmlns.com/foaf/0.1/age")
val age: List<Int>
@Rdf(iri = "http://xmlns.com/foaf/0.1/knows")
val friends: List<Person>
}
// ❌ Bad - RDF types in domain interface
@Rdf(iri = "http://xmlns.com/foaf/0.1/Person")
interface Person {
val name: List<String>
val rdfNode: RdfTerm // Don't do this!
val RdfGraph: RdfGraph // Don't do this!
}
2. Use Appropriate Collection Types
Choose the right collection type for your use case:
interface Product {
// Single value (use List and take firstOrNull())
@Rdf(iri = "http://example.org/name")
val name: List<String>
// Multiple values
@Rdf(iri = "http://example.org/tags")
val tags: List<String>
// Related objects
@Rdf(iri = "http://example.org/category")
val categories: List<Category>
// Optional single object (use List and take firstOrNull())
@Rdf(iri = "http://example.org/manufacturer")
val manufacturer: List<Manufacturer>
}
3. Design for Extensibility
Design interfaces that can grow over time:
// ✅ Good - Extensible design
@Rdf(iri = "http://example.org/Product")
interface Product {
// Core properties
@Rdf(iri = "http://example.org/name")
val name: List<String>
@Rdf(iri = "http://example.org/price")
val price: List<Double>
// Related objects
@Rdf(iri = "http://example.org/category")
val categories: List<Category>
// New properties can be added without breaking existing code
// Access via side-channel: product.asRdf().extras
}
Property Types and Mapping
Primitive Types
Map RDF literals to appropriate Kotlin types:
interface Example {
// String literals
@Rdf(iri = "http://example.org/name")
val name: List<String>
// Numeric literals
@Rdf(iri = "http://example.org/age")
val age: List<Int>
@Rdf(iri = "http://example.org/height")
val height: List<Double>
// Boolean literals
@Rdf(iri = "http://example.org/isActive")
val isActive: List<Boolean>
// Date literals (as strings, parse as needed)
@Rdf(iri = "http://example.org/birthDate")
val birthDate: List<String>
}
Object Properties
Map RDF objects to domain interfaces:
@Rdf(iri = "http://example.org/Person")
interface Person {
@Rdf(iri = "http://xmlns.com/foaf/0.1/name")
val name: List<String>
// Single related object
@Rdf(iri = "http://example.org/employer")
val employer: List<Organization>
// Multiple related objects
@Rdf(iri = "http://xmlns.com/foaf/0.1/knows")
val friends: List<Person>
// Self-referential properties
@Rdf(iri = "http://example.org/parent")
val parents: List<Person>
@Rdf(iri = "http://example.org/child")
val children: List<Person>
}
Complex Relationships
Model complex relationships between entities:
@Rdf(iri = "http://example.org/Order")
interface Order {
@Rdf(iri = "http://example.org/orderNumber")
val orderNumber: List<String>
@Rdf(iri = "http://example.org/customer")
val customer: List<Customer>
@Rdf(iri = "http://example.org/orderItem")
val items: List<OrderItem>
@Rdf(iri = "http://example.org/shippingAddress")
val shippingAddress: List<Address>
}
@Rdf(iri = "http://example.org/OrderItem")
interface OrderItem {
@Rdf(iri = "http://example.org/product")
val product: List<Product>
@Rdf(iri = "http://example.org/quantity")
val quantity: List<Int>
@Rdf(iri = "http://example.org/unitPrice")
val unitPrice: List<Double>
}
Handling Optional Values
Single Optional Values
Use List<T> and access with firstOrNull():
interface Person {
@Rdf(iri = "http://xmlns.com/foaf/0.1/name")
val name: List<String>
@Rdf(iri = "http://xmlns.com/foaf/0.1/age")
val age: List<Int>
@Rdf(iri = "http://xmlns.com/foaf/0.1/mbox")
val email: List<String>
}
// Usage
val person: Person = graph.materialize(node)
val name = person.name.firstOrNull() ?: "Unknown"
val age = person.age.firstOrNull() ?: 0
val email = person.email.firstOrNull()
Multiple Optional Values
Use List<T> and filter as needed:
interface Product {
@Rdf(iri = "http://example.org/name")
val name: List<String>
@Rdf(iri = "http://example.org/description")
val descriptions: List<String>
@Rdf(iri = "http://example.org/tags")
val tags: List<String>
}
// Usage
val product: Product = graph.materialize(node)
val primaryName = product.name.firstOrNull() ?: "Unnamed Product"
val allDescriptions = product.descriptions.filter { it.isNotBlank() }
val activeTags = product.tags.filter { it.isNotEmpty() }
Inheritance and Polymorphism
Interface Inheritance
Use interface inheritance to model type hierarchies:
@Rdf(iri = "http://example.org/Person")
interface Person {
@Rdf(iri = "http://xmlns.com/foaf/0.1/name")
val name: List<String>
@Rdf(iri = "http://xmlns.com/foaf/0.1/age")
val age: List<Int>
}
@Rdf(iri = "http://example.org/Employee")
interface Employee : Person {
@Rdf(iri = "http://example.org/employeeId")
val employeeId: List<String>
@Rdf(iri = "http://example.org/salary")
val salary: List<Double>
}
@Rdf(iri = "http://example.org/Customer")
interface Customer : Person {
@Rdf(iri = "http://example.org/customerId")
val customerId: List<String>
@Rdf(iri = "http://example.org/loyaltyPoints")
val loyaltyPoints: List<Int>
}
Polymorphic Collections
Handle collections of different types:
@Rdf(iri = "http://example.org/Organization")
interface Organization {
@Rdf(iri = "http://example.org/name")
val name: List<String>
// Collection of different person types
@Rdf(iri = "http://example.org/member")
val members: List<Person> // Can be Employee, Customer, or Person
}
// Usage
val org: Organization = graph.materialize(node)
org.members.forEach { member ->
when (member) {
is Employee -> println("Employee: ${member.employeeId.firstOrNull()}")
is Customer -> println("Customer: ${member.customerId.firstOrNull()}")
else -> println("Person: ${member.name.firstOrNull()}")
}
}
Validation and Constraints
Domain-Level Validation
Add validation logic to your domain interfaces:
@Rdf(iri = "http://example.org/Product")
interface Product {
@Rdf(iri = "http://example.org/name")
val name: List<String>
@Rdf(iri = "http://example.org/price")
val price: List<Double>
@Rdf(iri = "http://example.org/stockQuantity")
val stockQuantity: List<Int>
// Validation helper methods (optional)
fun isValid(): Boolean {
val name = name.firstOrNull()
val price = price.firstOrNull()
val stock = stockQuantity.firstOrNull()
return name != null &&
price != null && price > 0 &&
stock != null && stock >= 0
}
fun getValidationErrors(): List<String> {
val errors = mutableListOf<String>()
if (name.firstOrNull() == null) {
errors.add("Product name is required")
}
val price = price.firstOrNull()
if (price == null || price <= 0) {
errors.add("Product price must be positive")
}
val stock = stockQuantity.firstOrNull()
if (stock == null || stock < 0) {
errors.add("Stock quantity cannot be negative")
}
return errors
}
}
SHACL Integration
Use SHACL for structural validation:
// Materialize with validation
val validation = JenaValidation.fromTurtle(shapesTtl) // shapesTtl: your SHACL shapes as Turtle text
val product: Product = rdfRef.asValidatedType(validation)
// Or validate manually
val rdfHandle = product.asRdf()
rdfHandle.validateOrThrow()
Performance Considerations
Lazy Evaluation
Properties are evaluated lazily, so expensive operations are deferred:
interface LargeDataset {
@Rdf(iri = "http://example.org/item")
val items: List<Item> // Only evaluated when accessed
@Rdf(iri = "http://example.org/statistics")
val statistics: List<String> // Only evaluated when accessed
}
// Usage
val dataset: LargeDataset = graph.materialize(node)
// No expensive operations yet
val firstItem = dataset.items.firstOrNull() // Now items are loaded
val stats = dataset.statistics.firstOrNull() // Now statistics are loaded
Caching
Results are cached after first access:
val person: Person = graph.materialize(node)
// First access - queries RDF graph
val name1 = person.name.firstOrNull()
// Second access - uses cached result
val name2 = person.name.firstOrNull()
// name1 === name2 (same reference)
Testing Domain Interfaces
Unit Testing
Test domain interfaces independently of RDF:
class PersonTest {
@Test
fun `person validation works correctly`() {
// Create a mock person (no RDF involved)
val person = object : Person {
override val name = listOf("John Doe")
override val age = listOf(30)
override val friends = emptyList()
}
// Test domain logic
assertTrue(person.name.firstOrNull() == "John Doe")
assertTrue(person.age.firstOrNull() == 30)
}
}
Integration Testing
Test the full materialization process:
class PersonIntegrationTest {
@Test
fun `person materialization works end-to-end`() {
val repo = Rdf.memory()
repo.add {
val person = iri("http://example.org/person")
person - FOAF.name - "John Doe"
person - FOAF.age - 30
}
val personRef = RdfRef(iri("http://example.org/person"), repo.defaultGraph)
val person: Person = personRef.asType()
assertEquals("John Doe", person.name.firstOrNull())
assertEquals(30, person.age.firstOrNull())
}
}
Best Practices Summary
✅ Do
- Keep domain interfaces pure (no RDF types)
- Use
List<T>for all properties (single or multiple values) - Access single values with
firstOrNull() - Design for extensibility
- Use interface inheritance for type hierarchies
- Add domain-level validation methods
- Test domain interfaces independently
❌ Don’t
- Include RDF types in domain interfaces
- Use
Optional<T>or nullable types directly - Assume properties always have values
- Mix RDF operations with domain logic
- Create deep inheritance hierarchies
- Ignore validation requirements
Next Steps
- Explore RDF Integration - Advanced side-channel usage
- Learn about Validation - SHACL validation patterns
- Check out Advanced Usage - Complex scenarios
- See Practical Examples - Real-world use cases