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