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:
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
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 meaningSummary
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 →