.do
Patterns

Entity Modeling Patterns

Best practices for modeling business entities using semantic types

Entity modeling in Business-as-Code is about expressing your business domain using semantic types. This guide covers patterns and best practices.

Core Principles

1. Use Standard Types First

Always start with Schema.org types before creating custom ones:

// @errors: 7006
// @strict: true
import $ from 'sdk.do'

// ✅ Good: Use Schema.org - TypeScript validates structure
const customer = await $.Person.create({
  $type: 'Person',
  givenName: 'Alice',
  familyName: 'Johnson',
  email: '[email protected]',
})
//    ^?

const business = await $.Organization.create({
  $type: 'Organization',
  name: 'Acme Corp',
  legalName: 'Acme Corporation LLC',
})
//    ^?

// ❌ Avoid: Custom types when Schema.org exists
const user = await $.User.create({
  // Use Person instead
  firstName: 'Alice', // Use givenName
  lastName: 'Johnson', // Use familyName
})

2. Model Relationships Explicitly

Express relationships between entities as semantic triples:

// Create entities
const employee = await $.Person.create({ name: 'Bob' })
const company = await $.Organization.create({ name: 'Tech Corp' })

// Model the relationship
await db.relate(employee, $.worksFor, company)
await db.relate(company, $.employee, employee)

// Query relationships
const employer = await db.related(employee, $.worksFor, $.Organization)
const employees = await db.related(company, $.employee, $.Person)

3. Composition Over Inheritance

Combine multiple types rather than deep inheritance:

// @errors: 7006
// @strict: true
import $ from 'sdk.do'

type Person = { $id: string; name: string; email: string }
type Occupation = { code: string; title: string }
type Skill = { name: string; level: number }
type CreativeWork = { title: string; url: string }
type PriceSpecification = {
  $type: 'PriceSpecification'
  price: number
  priceCurrency: string
  unitText: string
}

// ✅ Good: Composition - Each field is independently typed
interface FreelancerProfile {
  person: Person // Identity
  occupation: Occupation // What they do
  skills: Skill[] // Capabilities
  portfolio: CreativeWork[] // Work samples
  rate: PriceSpecification // Pricing
}

const freelancer: FreelancerProfile = {
  person: await $.Person.create({
    name: 'Carol Designer',
    email: '[email protected]',
  }),
  //      ^?
  occupation: $.Occupation['27-1024.00'], // Graphic Designer
  skills: [$.Skill.AdobePhotoshop, $.Skill.Illustrator],
  portfolio: [],
  rate: {
    $type: 'PriceSpecification',
    price: 85,
    priceCurrency: 'USD',
    unitText: 'per hour',
  },
}
//    ^?

// ❌ Avoid: Deep inheritance
interface Freelancer extends Person, Worker, Designer, Contractor {
  // Too many concerns mixed together
}

Common Patterns

Pattern 1: Customer Entity

Model customers with rich semantic information:

// @errors: 7006
// @strict: true
import $, { on, send, db } from 'sdk.do'

type Order = { $id: string }
type Subscription = { $id: string }
type PostalAddress = { streetAddress: string; city: string }
type Person = { $type: 'Person'; givenName: string; familyName: string; email: string }

// Rich customer entity with constrained types
interface CustomerEntity extends Person {
  $type: 'Person'

  // Identity
  $id: string
  givenName: string
  familyName: string
  email: string
  telephone?: string

  // Preferences
  interests?: string[]
  preferredLanguage?: string

  // Business data - segment is type-constrained
  customerSince: Date
  segment?: 'high-value' | 'regular' | 'at-risk' | 'new'
  //        ^?
  lifetimeValue?: number

  // Relationships
  orders?: Order[]
  subscriptions?: Subscription[]
  address?: PostalAddress
}

// Create customer - TypeScript enforces all required fields
const customer = await $.Person.create({
  $type: 'Person',
  givenName: 'David',
  familyName: 'Miller',
  email: '[email protected]',
  customerSince: new Date(),
  segment: 'new', // Type-safe: must be one of the allowed values
})
//    ^?

// Track customer lifecycle with type safety
on($.Person.registered, async (person) => {
  await db.update($.Person, person.$id, {
    customerSince: new Date(),
    segment: 'new', // TypeScript validates segment value
  })

  await send($.Email.send, {
    to: person.email,
    template: 'welcome',
  })
})

Pattern 2: Product Entity

Model products with complete metadata:

interface ProductEntity extends Product {
  $type: 'Product'

  // Identity
  $id: string // SKU or unique identifier
  name: string
  description: string

  // Classification
  category?: string
  brand?: Brand
  manufacturer?: Organization

  // Pricing
  offers?: Offer[] // Can have multiple offers

  // Inventory
  inventory?: number
  inventoryStatus?: 'InStock' | 'OutOfStock' | 'PreOrder'

  // Media
  image?: string | ImageObject[]
  video?: VideoObject[]

  // Attributes
  weight?: QuantitativeValue
  dimensions?: QuantitativeValue
  color?: string
  material?: string
}

// Create product with offer
const product = await $.Product.create({
  $type: 'Product',
  $id: 'SKU-001',
  name: 'Wireless Headphones',
  description: 'Premium noise-cancelling headphones',
  category: 'Electronics',
  brand: await $.Brand.create({ name: 'AudioTech' }),
  offers: [
    {
      $type: 'Offer',
      price: 299.99,
      priceCurrency: 'USD',
      availability: $.InStock,
      priceValidUntil: '2025-12-31',
    },
  ],
  inventory: 150,
  image: 'https://cdn.example.com/headphones.jpg',
})

Pattern 3: Order Entity

Model orders as complete transactions:

erDiagram Customer ||--o{ Order : places Order ||--|{ OrderItem : contains OrderItem }o--|| Product : references Order ||--o| Payment : "paid by" Order ||--o| ParcelDelivery : "fulfilled by" Customer { string id string name string email string segment } Order { string id string orderNumber string orderStatus date orderDate decimal totalPrice } OrderItem { int orderQuantity decimal price } Product { string id string name decimal price } Payment { string id string status string method } ParcelDelivery { string trackingNumber date expectedDelivery }
interface OrderEntity extends Order {
  $type: 'Order'

  // Identity
  $id: string // Order number
  orderNumber: string

  // Parties
  customer: Person
  seller?: Organization

  // Items
  orderedItem: OrderItem[]

  // Pricing
  totalPrice: number
  priceCurrency: string
  tax?: number
  discount?: number

  // Status
  orderStatus: OrderStatus
  orderDate: Date

  // Payment
  paymentMethod?: PaymentMethod
  paymentStatus?: 'Paid' | 'Unpaid' | 'Pending'

  // Fulfillment
  orderDelivery?: ParcelDelivery
}

// Create order
const order = await $.Order.create({
  $type: 'Order',
  orderNumber: 'ORD-2025-001',
  customer: customer.$id,
  orderedItem: [
    {
      $type: 'OrderItem',
      orderItem: product.$id,
      orderQuantity: 2,
      price: product.offers[0].price,
    },
  ],
  totalPrice: 599.98,
  priceCurrency: 'USD',
  orderStatus: $.OrderProcessing,
  orderDate: new Date(),
})

// Relate entities
await db.relate(customer, $.places, order)
await db.relate(order, $.contains, product)

Pattern 4: Subscription Entity

Model recurring subscriptions:

interface SubscriptionEntity {
  $type: 'Service'

  // Identity
  $id: string
  subscriptionId: string

  // Customer
  customer: Person | Organization

  // Plan
  plan: Offer // The pricing plan
  features: string[]

  // Lifecycle
  status: 'active' | 'cancelled' | 'past_due' | 'trialing'
  startDate: Date
  currentPeriodStart: Date
  currentPeriodEnd: Date
  cancelAt?: Date

  // Billing
  billingCycle: 'monthly' | 'annual'
  nextBillingDate: Date
  paymentMethod: PaymentMethod
}

// Create subscription
const subscription = await $.Service.create({
  $type: 'Service',
  subscriptionId: 'SUB-2025-001',
  customer: customer.$id,
  plan: {
    $type: 'Offer',
    name: 'Pro Plan',
    price: 29,
    priceCurrency: 'USD',
    billingIncrement: 'monthly',
  },
  features: ['feature-a', 'feature-b', 'feature-c'],
  status: 'active',
  startDate: new Date(),
  billingCycle: 'monthly',
})

// Subscription lifecycle
on($.Subscription.created, async (sub) => {
  await db.relate(sub.customer, $.subscribes, sub)
  await send($.Account.provision, { subscription: sub })
})

on($.Subscription.cancelled, async (sub) => {
  await db.update($.Service, sub.$id, {
    status: 'cancelled',
    cancelAt: new Date(),
  })
  await send($.Account.deprovision, { subscription: sub })
})

Pattern 5: Multi-Tenant Entity

Model entities across multiple tenants/organizations:

interface MultiTenantEntity {
  // Every entity has a tenant
  tenant: Organization

  // Tenant-specific visibility
  visibility: 'private' | 'shared' | 'public'

  // Access control
  owner: Person
  collaborators?: Person[]
  permissions: Permission[]
}

// Apply to any entity
interface TenantedCustomer extends Person, MultiTenantEntity {
  $type: 'Person'
  tenant: Organization
  visibility: 'private'
  owner: Person
}

// Create with tenant context
const customer = await $.Person.create({
  $type: 'Person',
  name: 'Eve',
  email: '[email protected]',
  tenant: user.organization,
  owner: user.$id,
  visibility: 'private',
})

// Query scoped to tenant
const customers = await db.list($.Person, {
  where: {
    tenant: user.organization.$id,
  },
})

Advanced Patterns

Versioned Entities

Track entity changes over time:

interface VersionedEntity {
  $id: string
  $version: number
  $createdAt: Date
  $updatedAt: Date
  $updatedBy: Person

  // History
  $previousVersion?: string
  $nextVersion?: string
}

// Create versioned entity
const product = await $.Product.create({
  $type: 'Product',
  $version: 1,
  $createdAt: new Date(),
  name: 'Widget v1',
})

// Update creates new version
const updatedProduct = await $.Product.update(product.$id, {
  name: 'Widget v2',
  $version: 2,
  $previousVersion: product.$id,
})

// Query version history
const versions = await db.versions(product.$id)

Soft Delete Pattern

Mark entities as deleted without removing them:

interface SoftDeletableEntity {
  deletedAt?: Date
  deletedBy?: Person
  isDeleted: boolean
}

// Soft delete
await db.update($.Product, productId, {
  isDeleted: true,
  deletedAt: new Date(),
  deletedBy: user.$id,
})

// Query only active entities
const activeProducts = await db.list($.Product, {
  where: { isDeleted: { $ne: true } },
})

// Restore
await db.update($.Product, productId, {
  isDeleted: false,
  deletedAt: undefined,
  deletedBy: undefined,
})

Aggregate Entities

Group related entities:

// Shopping Cart as aggregate
interface ShoppingCart {
  $type: 'ItemList'
  $id: string

  // Root entity
  customer: Person

  // Child entities
  items: OrderItem[]

  // Computed properties
  totalItems: number
  subtotal: number
  tax: number
  total: number

  // Lifecycle
  createdAt: Date
  expiresAt: Date
}

// Cart operations maintain aggregate consistency
async function addToCart(cart: ShoppingCart, product: Product, quantity: number) {
  const item: OrderItem = {
    $type: 'OrderItem',
    orderItem: product,
    orderQuantity: quantity,
    price: product.offers[0].price,
  }

  cart.items.push(item)
  cart.totalItems = cart.items.reduce((sum, i) => sum + i.orderQuantity, 0)
  cart.subtotal = cart.items.reduce((sum, i) => sum + i.price * i.orderQuantity, 0)
  cart.tax = cart.subtotal * 0.08
  cart.total = cart.subtotal + cart.tax

  await db.update($.ItemList, cart.$id, cart)
}

Entity Lifecycle

stateDiagram-v2 [*] --> Creating: Create request Creating --> Validating: Validate data Validating --> Creating: Validation failed Validating --> Persisting: Valid Persisting --> Active: Entity created Active --> Updating: Update request Updating --> Validating: Validate changes Updating --> Active: Updated Active --> Deleting: Delete request Deleting --> Deleted: Soft delete Deleting --> [*]: Hard delete Deleted --> Active: Restore Deleted --> [*]: Purge note right of Creating Initialize entity Generate ID Set defaults end note note right of Active Entity in use Can be queried Can be modified end note note right of Deleted Soft deleted Hidden from queries Can be restored end note

Creation Pattern

// Standard creation with validation
async function createCustomer(data: Partial<Person>): Promise<Person> {
  // Validate
  if (!data.email) throw new Error('Email required')

  // Create entity
  const customer = await $.Person.create({
    $type: 'Person',
    $id: generateId(),
    ...data,
    createdAt: new Date(),
  })

  // Emit event
  await send($.Person.created, customer)

  return customer
}

// Listen for creation event
on($.Person.created, async (person) => {
  // Initialize related entities
  await $.ShoppingCart.create({
    customer: person.$id,
  })

  // Send welcome
  await send($.Email.send, {
    to: person.email,
    template: 'welcome',
  })
})

Update Pattern

// Update with change tracking
async function updateProduct(id: string, changes: Partial<Product>) {
  // Get current
  const current = await db.get($.Product, id)

  // Apply changes
  const updated = { ...current, ...changes, updatedAt: new Date() }

  // Save
  await db.update($.Product, id, updated)

  // Emit event
  await send($.Product.updated, {
    previous: current,
    current: updated,
    changes: Object.keys(changes),
  })

  return updated
}

// Track changes
on($.Product.updated, async (event) => {
  if (event.changes.includes('price')) {
    // Price changed, notify subscribers
    await send($.Notification.priceChange, {
      product: event.current,
      oldPrice: event.previous.offers[0].price,
      newPrice: event.current.offers[0].price,
    })
  }
})

Best Practices

1. Rich Entity Models

Include all relevant semantic information:

// ✅ Rich, semantic model
const product = await $.Product.create({
  $type: 'Product',
  name: 'Premium Headphones',
  description: 'Noise-cancelling wireless headphones',
  brand: $.Brand.AudioTech,
  manufacturer: $.Organization.AudioTechCorp,
  category: $.ProductCategory.Electronics,
  gtin: '1234567890123',
  offers: [
    {
      price: 299.99,
      priceCurrency: 'USD',
      availability: $.InStock,
    },
  ],
  aggregateRating: {
    ratingValue: 4.5,
    reviewCount: 234,
  },
})

// ❌ Minimal model
const product = {
  name: 'Headphones',
  price: 299.99,
}

2. Explicit IDs

Use meaningful, semantic IDs:

// ✅ Good: Semantic IDs
$id: 'person:[email protected]'
$id: 'order:2025-001'
$id: 'product:sku-12345'
$id: 'subscription:sub_abc123'

// ❌ Avoid: Opaque IDs (when possible)
$id: '7f3e9a2b-4c1d-8e5f-6g7h-8i9j0k1l2m3n'

3. Validate Early

Validate at creation, not at use:

// ✅ Validate at creation
async function createOrder(data: Partial<Order>) {
  if (!data.customer) throw new Error('Customer required')
  if (!data.orderedItem?.length) throw new Error('Items required')

  return await $.Order.create(data)
}

// ❌ Validate at use
function calculateTotal(order: Order) {
  if (!order.orderedItem) throw new Error('No items') // Too late!
}

4. Use Relationships

Model connections explicitly:

// ✅ Explicit relationships
await db.relate(order, $.placedBy, customer)
await db.relate(product, $.manufacturedBy, manufacturer)

// ❌ Embedded IDs only
order.customerId = customer.$id // Lose semantic meaning

Summary

Entity modeling in Business-as-Code:

  • Start with Schema.org types
  • Model relationships explicitly
  • Use composition over inheritance
  • Include rich semantic metadata
  • Implement proper lifecycle management
  • Validate early and often

Next: Workflow Patterns →