Entity Modeling Best Practices
Entities are the foundation of your application. This guide covers how to design effective entity models that translate into maintainable, scalable applications.
What Are Entities?
Entities represent the core business objects in your domain:
Entity Structure
Basic Entity Definition
interface Entity {
name: string // PascalCase singular name
fields: Record<string, Field>
relationships?: Relationship[]
validations?: ValidationRule[]
indexes?: Index[]
operations?: Operation[] // CRUD operations
confidence?: number // AI's confidence level
}
Field Types
type FieldType =
| 'string' // Text data
| 'number' // Integers
| 'decimal' // Float/money
| 'boolean' // True/false
| 'date' // Date only
| 'datetime' // Date + time
| 'json' // Complex data
| 'enum' // Fixed options
| 'uuid' // Unique ID
| 'relation' // Foreign key
Core Modeling Principles
1. Single Responsibility
Each entity should represent ONE concept:✅ Good: Separate entities
User { email, password }
Profile { bio, avatar, location }
❌ Bad: Mixed concerns
User { email, password, bio, avatar, orderHistory, preferences }
2. Clear Naming
Use descriptive, domain-specific names:✅ Good: Domain-specific
Customer, Invoice, LineItem, Payment
❌ Bad: Generic
Data, Info, Thing, Object
3. Proper Granularity
Balance between too many and too few entities:✅ Good: Appropriate separation
Order { total, status }
OrderItem { quantity, price }
Product { name, sku }
❌ Bad: Over-normalized
OrderItemQuantity { value }
OrderItemPrice { amount, currency }
PriceCurrency { code, symbol }
Common Entity Patterns
User/Account Pattern
// Authentication entity
User {
id: uuid (primary key)
email: string (unique)
password: string (hashed)
emailVerified: boolean
createdAt: datetime
}
// Profile information
Profile {
id: uuid
userId: uuid (relation)
name: string
bio: text
avatar: string
updatedAt: datetime
}
Hierarchical Pattern
// Parent entity
Project {
id: uuid
name: string
description: text
}
// Child entity
Task {
id: uuid
projectId: uuid (relation)
title: string
parentTaskId?: uuid (self-relation)
}
Many-to-Many Pattern
// Main entities
Student { id, name, email }
Course { id, title, credits }
// Junction entity (explicit)
Enrollment {
id: uuid
studentId: uuid
courseId: uuid
enrolledAt: datetime
grade?: string
}
Status/State Pattern
Order {
id: uuid
status: enum ['pending', 'processing', 'shipped', 'delivered']
statusHistory: json // Track changes
}
Field Design Guidelines
Required vs Optional
// Be explicit about requirements
Task {
title: string (required) // Must have
description?: text (optional) // Can be empty
dueDate?: date (optional) // Not always set
assigneeId?: uuid (optional) // Can be unassigned
}
Default Values
Post {
status: enum (default: 'draft')
views: number (default: 0)
published: boolean (default: false)
createdAt: datetime (default: now)
}
Unique Constraints
User {
email: string (unique) // Global unique
username: string (unique) // Global unique
}
Product {
sku: string (unique) // Global unique
barcode?: string (unique) // Unique if provided
}
Computed vs Stored
// Stored fields
OrderItem {
quantity: number
unitPrice: decimal
totalPrice: decimal // Store for performance
}
// Virtual/Computed (not stored)
Order {
items: OrderItem[]
// total computed from sum of items
get total() { return items.sum(i => i.totalPrice) }
}
Relationship Modeling
One-to-Many
// Parent side (one)
Author {
id: uuid
name: string
posts: Post[] // Virtual array
}
// Child side (many)
Post {
id: uuid
authorId: uuid (required) // Foreign key
author: Author // Virtual reference
}
Many-to-Many
// Option 1: Implicit (Prisma style)
Post {
tags: Tag[] // Prisma creates junction table
}
Tag {
posts: Post[]
}
// Option 2: Explicit junction entity
PostTag {
postId: uuid
tagId: uuid
addedAt: datetime
addedBy: uuid
}
Self-Relations
// Hierarchical
Category {
id: uuid
name: string
parentId?: uuid // Self reference
parent?: Category
children: Category[]
}
// Network
User {
id: uuid
following: User[] // Many-to-many self
followers: User[]
}
Validation Rules
Field-Level Validation
Product {
name: string {
minLength: 3,
maxLength: 100,
pattern: /^[a-zA-Z0-9 -]+$/
}
price: decimal {
min: 0,
max: 1000000
}
quantity: number {
min: 0,
integer: true
}
}
Entity-Level Validation
Event {
startDate: datetime
endDate: datetime
@validate: endDate > startDate
@validate: startDate > now()
}
Business Rules
Task {
status: enum
completedAt?: datetime
completedBy?: uuid
@rule: "if status = 'completed', completedAt and completedBy required"
@rule: "only assignee or admin can change status"
}
Common Patterns by Domain
E-Commerce
Product { name, sku, price, inventory }
Category { name, slug, parentId }
Cart { userId, sessionId }
CartItem { cartId, productId, quantity }
Order { userId, total, status }
OrderItem { orderId, productId, quantity, price }
Payment { orderId, amount, method, status }
SaaS/B2B
Organization { name, plan, billingEmail }
Team { orgId, name }
Member { userId, teamId, role }
Invite { email, teamId, token, expiresAt }
Subscription { orgId, plan, status, renewsAt }
Content Management
Page { slug, title, content, template }
Block { pageId, type, content, order }
Media { url, type, size, metadata }
Revision { pageId, content, authorId, createdAt }
Project Management
Project { name, description, ownerId }
Milestone { projectId, name, dueDate }
Task { projectId, title, status, assigneeId }
Comment { taskId, authorId, content }
Attachment { taskId, url, uploadedBy }
Anti-Patterns to Avoid
1. God Objects
❌ Bad: Everything in one entityUser {
// Authentication
email, password,
// Profile
name, bio, avatar,
// Settings
theme, language, notifications,
// Activity
lastLogin, loginCount,
// Billing
creditCard, subscription
}
✅ Good: Separated concerns
User { email, password }
Profile { userId, name, bio }
Settings { userId, theme, language }
Billing { userId, subscription }
2. Primitive Obsession
❌ Bad: Everything as stringsProduct {
price: string // "19.99"
date: string // "2024-01-15"
active: string // "true"
}
✅ Good: Proper types
Product {
price: decimal
date: date
active: boolean
}
3. Missing Relationships
❌ Bad: IDs without relationshipsTask {
projectId: string // Just stores ID
userId: string // No relation defined
}
✅ Good: Explicit relationships
Task {
projectId: uuid
project: Project // Relation
assigneeId: uuid
assignee: User // Relation
}
Entity Lifecycle
Audit Fields
// Common audit fields
interface Auditable {
createdAt: datetime
createdBy?: uuid
updatedAt: datetime
updatedBy?: uuid
deletedAt?: datetime // Soft delete
deletedBy?: uuid
}
Versioning
Document {
id: uuid
version: number
content: text
publishedVersion?: number
}
DocumentHistory {
documentId: uuid
version: number
content: text
changedBy: uuid
changedAt: datetime
changeNote?: string
}
Performance Considerations
Indexing Strategy
User {
email: string @index(unique)
username: string @index(unique)
createdAt: datetime @index
}
Post {
authorId: uuid @index
status: enum @index
publishedAt: datetime @index
@index([status, publishedAt]) // Composite
}
Denormalization When Needed
// Normalized (multiple queries)
Comment { userId, postId }
// Denormalized (single query)
Comment {
userId,
userName, // Copied from User
userAvatar, // Copied from User
postId
}
Testing Your Model
Questions to Ask
Red Flags
Migration and Evolution
Adding Fields
// Safe: Optional field
User {
existingField: string
newField?: string // Safe to add
}
// Requires migration: Required field
User {
newRequired: string (default: 'value') // Need default
}
Changing Relationships
// From one-to-many to many-to-many
// Before:
Post { authorId }
// After (requires migration):
Post { authors: User[] }
PostAuthor { postId, userId } // Junction table