Skip to content

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: Money and Decimal are different types
  • Semantic clarity: Address vs. 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 compile ecommerce.usl --verify

USL will:

  1. ✅ Check all invariants are satisfiable
  2. ✅ Verify computed fields are well-defined
  3. ✅ Ensure relationships are consistent
  4. ✅ 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

Further Reading