data-modeling

Designs domain models, entity relationships, and data shapes before writing code or schemas. Covers entity identification, relationship mapping, aggregate design, and access pattern validation. Use when designing data structures for new features or systems.

Data Modeling

Model the domain, not the UI, not the database, not the API. The domain model is upstream of everything - get it wrong and every layer inherits the mistake.

Reference: reference/modeling-patterns.md - Normalization levels, relationship patterns, soft delete, audit trails, ID types, polymorphic associations, temporal data.

Step 1: Identify Entities

An entity is a thing with identity that changes over time.

Ask These Questions

  • What are the "nouns" in the domain? (User, Order, Product, Payment)
  • Which things have a lifecycle? (Created → Active → Closed)
  • Which things need to be referenced by other things? (Foreign keys)
  • Which things are always accessed together? (Same aggregate)

Entity vs Value Object

EntityValue Object
IdentityHas a unique IDNo ID, defined by its values
LifecycleChanges over timeImmutable
ExampleUser, Order, InvoiceAddress, Money, DateRange
StorageOwn table/documentEmbedded in parent entity
Entity: User (id: 123, name: "Alice", email: "[email protected]")
  - Identity matters: User 123 is User 123 even if name changes

Value Object: Money (amount: 5000, currency: "USD")
  - Identity doesn't matter: any Money(5000, "USD") is interchangeable

Step 2: Map Relationships

RelationshipPatternExample
One-to-OneFK on either side, or embedUser ↔ Profile
One-to-ManyFK on the "many" sideUser → Orders
Many-to-ManyJunction tableStudent ↔ Course (via Enrollment)
Self-referentialFK to same tableEmployee → Manager (Employee)
PolymorphicType column + ID, or separate tablesComment on Post or Photo

Relationship Questions

  • Ownership: If A is deleted, should B be deleted too? (cascade)
  • Cardinality: One-to-one, one-to-many, many-to-many?
  • Optionality: Is the relationship required or optional?
  • Direction: Who needs to find whom? (This determines where indexes go)

Step 3: Define Aggregates

An aggregate is a cluster of entities that are always consistent together. The aggregate root is the entry point.

Aggregate: Order
  Root: Order
  Children: OrderItem, OrderDiscount
  Rule: Total = sum of (item prices - discounts)
  Invariant: Total must never be negative

→ You load/save the entire Order aggregate atomically
→ You NEVER modify OrderItems without going through Order
→ Other aggregates reference Order by ID only

Aggregate Design Rules

  1. Small aggregates. An aggregate that contains 1000 items is too big. Split.
  2. Consistency boundary. Everything inside must be consistent. Across aggregates = eventual consistency.
  3. Reference by ID. Don't embed other aggregates. Reference them by ID.
  4. One transaction per aggregate. Modifying two aggregates in one transaction = design smell.

Step 4: Choose IDs

ID TypeProsConsBest For
Auto-increment (INT)Simple, small, sortableNot distributed-safe, predictableSingle-database systems
UUID v4Globally unique, no coordinationLarge (16 bytes), random (bad for B-tree)Distributed systems
UUID v7Globally unique + time-sortableRelatively newModern distributed systems
ULIDTime-sortable, URL-safeLess standardAPIs where URL appearance matters
Prefixed ID (usr_abc123)Human-readable, type-safeCustom generationUser-facing APIs
Composite keyNatural meaningComplex JOINsJunction tables, time-series

Recommendation: UUID v7 or prefixed IDs for new systems. Auto-increment if single database and no API exposure.

Step 5: Validate Against Access Patterns

Before finalizing the model, list every way the data will be read:

ACCESS PATTERNS:
1. Get user by ID → users table, PK lookup
2. List orders for a user → orders WHERE user_id = X
3. Search products by name → products WHERE name LIKE '%X%' (full-text search?)
4. Get order with all items → orders JOIN order_items
5. Dashboard: orders per day → orders GROUP BY date (materialized view?)
6. Get user's recent activity → needs cross-entity query (denormalize?)

For each pattern:

  • Can the model serve this efficiently?
  • Does it require a JOIN? How many?
  • Does it need an index? What kind?
  • Should anything be denormalized for read performance?

If an access pattern is awkward, the model is wrong. Adjust the model, don't work around it.

Step 6: Handle State Machines

Most entities have a lifecycle. Model it explicitly.

Order States:
  DRAFT → PLACED → PAID → SHIPPED → DELIVERED
                ↘ CANCELLED

Allowed Transitions:
  DRAFT → PLACED (when submitted)
  PLACED → PAID (when payment confirmed)
  PLACED → CANCELLED (when cancelled before payment)
  PAID → SHIPPED (when shipped)
  SHIPPED → DELIVERED (when delivery confirmed)

Invalid Transitions (reject):
  DELIVERED → DRAFT (can't go back)
  CANCELLED → PAID (can't pay cancelled order)

Rules:

  • Enumerate all states explicitly
  • Define allowed transitions
  • Validate transitions in code (reject invalid state changes)
  • Store timestamps for each transition (placed_at, paid_at, shipped_at)

Model Documentation Template

## Entity: [Name]

### Fields
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| id | UUID v7 | Yes | Primary key |
| ... | ... | ... | ... |

### Relationships
- Has many [Entity] (via [field])
- Belongs to [Entity] (via [field])

### States
[state diagram if applicable]

### Access Patterns
1. [Pattern] - [how it's served]

### Invariants
- [Business rule that must always be true]

Common Mistakes

MistakeProblemFix
Model mirrors the UIUI changes → model changesModel the domain, not the screens
God entityOne entity with 40 fields, handles everythingSplit into aggregates
Anemic modelEntities are just data bags, logic lives elsewherePut behavior with the data it operates on
Premature denormalizationDuplicated data, inconsistency riskNormalize first, denormalize for measured performance needs
Missing state machineState transitions unconstrainedExplicit states + allowed transitions
Stringly typedStatus as free-text stringUse enums with exhaustive handling