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:

  • User in an authentication system
  • Product in e-commerce
  • Task in project management
  • Post in a blog
  • 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 entity
    User {
      // 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 strings
    Product {
      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 relationships
    Task {
      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

  • Can I represent all my use cases?
  • Are relationships clear and correct?
  • Do field types match the data?
  • Are validations comprehensive?
  • Will it scale with more data?
  • Is it easy to understand?
  • Red Flags

  • • Entities with 20+ fields
  • • Circular dependencies
  • • Many nullable fields
  • • Duplicate data across entities
  • • Complex many-to-many chains
  • 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

    Related Documentation

  • DSL Extraction - How entities are detected
  • Code Generation - How entities become code
  • Database Schema - Storage layer
  • First Conversation - See modeling in action