Tutorial 2: Domain Modeling¶
Duration: 45 minutes
Prerequisites: Tutorial 1 completed
What you'll learn: Advanced domain modeling with relationships, computed fields, and complex invariants
Overview¶
Domain modeling is the foundation of USL applications. In this tutorial, you'll learn:
- Entity relationships (one-to-many, many-to-many)
- Value objects and type safety
- Complex invariants and validations
- Computed fields
- Domain events
The Project: E-Commerce Order System¶
We'll build a complete order management system with:
- Customers and addresses
- Products with inventory tracking
- Orders with line items
- Payment processing
- Shipping logistics
Step 1: Define Value Objects¶
Value objects are immutable types that represent domain concepts:
domain ECommerce {
// Value object for money
value Money {
amount: Decimal
currency: String
invariant valid_amount {
this.amount >= 0.0
}
invariant valid_currency {
this.currency in ["USD", "EUR", "GBP"]
}
}
// Value object for address
value Address {
street: String
city: String
state: String
postalCode: String
country: String
invariant valid_postal {
len(this.postalCode) >= 5
}
}
// Email with validation
value Email {
address: String
invariant valid_format {
matches(this.address, "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
}
}
}
Value Object Benefits¶
- Immutability: Cannot be modified after creation
- Validation: Invariants checked at construction
- Type safety:
MoneyandDecimalare different types - Semantic clarity:
Addressvs. raw strings
When to Use Value Objects
Use value objects for concepts that: - Have no identity (two $5 bills are equivalent) - Should be immutable - Have validation rules - Appear throughout your domain
Step 2: Define Core Entities¶
entity Customer {
id: CustomerId @primary
email: Email @unique
name: String
addresses: List[Address]
createdAt: Timestamp
// Business rule: must have at least one address
invariant has_address {
len(this.addresses) > 0
}
}
entity Product {
id: ProductId @primary
name: String
description: String
price: Money
stockQuantity: Int
category: String
active: Boolean
invariant positive_stock {
this.stockQuantity >= 0
}
invariant positive_price {
this.price.amount > 0.0
}
// Inactive products can't have stock
invariant inactive_no_stock {
!this.active -> this.stockQuantity == 0
}
}
Step 3: Model Relationships¶
One-to-Many: Order and Line Items¶
entity Order {
id: OrderId @primary
customerId: CustomerId
lineItems: List[LineItem]
status: OrderStatus
shippingAddress: Address
billingAddress: Address
placedAt: Timestamp
// Computed field
computed totalAmount: Money {
sum(this.lineItems.map(|item| item.subtotal))
}
// Orders must have at least one item
invariant has_items {
len(this.lineItems) > 0
}
// Total must be positive
invariant positive_total {
this.totalAmount.amount > 0.0
}
}
entity LineItem {
id: LineItemId @primary
orderId: OrderId // Foreign key
productId: ProductId
quantity: Int
unitPrice: Money
// Computed subtotal
computed subtotal: Money {
Money {
amount: this.quantity * this.unitPrice.amount,
currency: this.unitPrice.currency
}
}
invariant positive_quantity {
this.quantity > 0
}
}
enum OrderStatus {
Pending
Confirmed
Shipped
Delivered
Cancelled
}
Many-to-Many: Product Categories¶
entity Category {
id: CategoryId @primary
name: String @unique
parentId: Option[CategoryId] // Self-referential
invariant no_self_reference {
this.parentId != Some(this.id)
}
}
// Join entity for many-to-many
entity ProductCategory {
productId: ProductId
categoryId: CategoryId
@unique_together(productId, categoryId)
}
Step 4: Complex Invariants¶
Cross-Entity Invariants¶
// Global invariant: order line items must reference valid products
global invariant valid_order_products {
forall order in Order {
forall item in order.lineItems {
exists product in Product {
product.id == item.productId && product.active
}
}
}
}
// Stock availability check
global invariant sufficient_stock {
forall order in Order where order.status == OrderStatus.Confirmed {
forall item in order.lineItems {
exists product in Product {
product.id == item.productId &&
product.stockQuantity >= item.quantity
}
}
}
}
Temporal Invariants¶
entity Payment {
id: PaymentId @primary
orderId: OrderId
amount: Money
method: PaymentMethod
status: PaymentStatus
processedAt: Option[Timestamp]
// If processed, must have timestamp
invariant processed_has_timestamp {
this.status == PaymentStatus.Completed -> this.processedAt.is_some()
}
// Can't be processed before order was placed
invariant payment_after_order {
forall order in Order where order.id == this.orderId {
match this.processedAt {
Some(ts) => ts >= order.placedAt
None => true
}
}
}
}
enum PaymentMethod { CreditCard, DebitCard, PayPal, BankTransfer }
enum PaymentStatus { Pending, Completed, Failed, Refunded }
Step 5: Computed Fields and Aggregations¶
entity Customer {
// ... previous fields ...
// Computed: total lifetime value
computed lifetimeValue: Money {
let orders = findAll(Order, |o| o.customerId == this.id)
sum(orders.map(|o| o.totalAmount))
}
// Computed: number of orders
computed orderCount: Int {
count(Order, |o| o.customerId == this.id)
}
// Computed: VIP status
computed isVip: Boolean {
this.lifetimeValue.amount > 1000.0 || this.orderCount > 10
}
}
Computed Field Performance
Computed fields are recalculated on access. For expensive computations, consider: - Caching strategies - Materialized views - Async updates with events
Step 6: Domain Events¶
// Events capture important state changes
event OrderPlaced {
orderId: OrderId
customerId: CustomerId
totalAmount: Money
placedAt: Timestamp
}
event OrderShipped {
orderId: OrderId
trackingNumber: String
shippedAt: Timestamp
}
event PaymentProcessed {
paymentId: PaymentId
orderId: OrderId
amount: Money
processedAt: Timestamp
}
event InventoryReduced {
productId: ProductId
quantity: Int
reason: String
}
Events are emitted automatically when state changes occur.
Step 7: Advanced Type Features¶
Option Types¶
entity Product {
// Optional discount
discount: Option[Money]
// Computed effective price
computed effectivePrice: Money {
match this.discount {
Some(d) => Money {
amount: this.price.amount - d.amount,
currency: this.price.currency
}
None => this.price
}
}
}
Result Types for Errors¶
// Function that can fail
function validateOrder(order: Order) -> Result[Void, ValidationError] {
if len(order.lineItems) == 0 {
return Err(ValidationError.EmptyOrder)
}
if order.totalAmount.amount <= 0.0 {
return Err(ValidationError.InvalidTotal)
}
Ok(())
}
enum ValidationError {
EmptyOrder
InvalidTotal
InsufficientStock
InvalidAddress
}
Step 8: Validate the Model¶
Compile with verification enabled:
USL will:
- ✅ Check all invariants are satisfiable
- ✅ Verify computed fields are well-defined
- ✅ Ensure relationships are consistent
- ✅ Prove no deadlocks in state machines
Example verification output:
Verifying domain model...
✓ All invariants satisfiable
✓ No circular dependencies
✓ All relationships valid
✓ 15 theorems proved
✓ 0 warnings
Compilation successful!
Common Patterns¶
1. Soft Delete¶
entity Product {
deleted: Boolean = false
deletedAt: Option[Timestamp]
invariant deleted_has_timestamp {
this.deleted -> this.deletedAt.is_some()
}
}
2. Audit Trail¶
entity Order {
createdBy: UserId
createdAt: Timestamp
modifiedBy: UserId
modifiedAt: Timestamp
version: Int // Optimistic locking
}
3. Polymorphism with Variants¶
variant ShippingMethod {
Standard { estimatedDays: Int }
Express { guaranteedDate: Date }
Overnight { cutoffTime: Time }
Pickup { locationId: LocationId }
}
Testing Your Domain Model¶
Create spec tests:
spec OrderTests {
test "order total is sum of line items" {
let order = Order {
// ... setup ...
lineItems: [
LineItem { quantity: 2, unitPrice: Money(10.0, "USD") },
LineItem { quantity: 1, unitPrice: Money(5.0, "USD") }
]
}
assert order.totalAmount.amount == 25.0
}
test "empty order violates invariant" {
expect_error[has_items] {
Order {
lineItems: []
// ... other fields ...
}
}
}
}
Best Practices¶
DO ✅¶
- Keep entities focused on a single responsibility
- Use value objects for validated concepts
- Write invariants for critical business rules
- Use computed fields for derived data
- Emit events for important state changes
DON'T ❌¶
- Store computed data in entity fields
- Use primitive types for domain concepts
- Skip validation in value objects
- Create circular dependencies
- Ignore compiler warnings
What You've Learned¶
✅ Value objects for type safety
✅ Entity relationships and foreign keys
✅ Complex invariants with quantifiers
✅ Computed fields and aggregations
✅ Domain events for observability
✅ Advanced type features (Option, Result, variants)
✅ Domain model verification
Next Steps¶
Continue to Tutorial 3: Policy-Driven Design to learn:
- Role-based access control (RBAC)
- Attribute-based access control (ABAC)
- Context-aware policies
- Policy composition and inheritance
- Testing authorization logic