Database Schema Design with Claude Code
The Problem
Designing a database schema requires balancing normalization, query performance, data integrity, and future extensibility. Getting it wrong means painful migrations later, slow queries, and data inconsistencies that are hard to fix once production data exists.
Quick Start
Describe your domain to Claude Code and ask for a schema:
Design a PostgreSQL schema for a SaaS project management tool.
Core entities: Users, Organizations, Projects, Tasks, Comments.
Requirements:
- Users belong to multiple organizations (many-to-many)
- Each organization has projects
- Tasks belong to projects and are assigned to users
- Tasks have status, priority, due date, and can be nested (subtasks)
- Comments belong to tasks and have an author
- Soft delete for all entities
- Audit trail (created_at, updated_at, created_by)
Use Prisma schema format. Include indexes for common queries.
What Claude Code Brings to Schema Design
Claude Code excels at database schema design because it can:
- Apply normalization rules consistently across your entire schema
- Identify missing indexes by analyzing your query patterns
- Spot data integrity gaps like missing foreign key constraints or cascading delete issues
- Generate migration files that safely transform existing schemas
- Review schemas against best practices and catch common mistakes before they reach production
Step-by-Step Guide
Step 1: Define your domain model
Start with a plain-English description of your entities and relationships. Claude Code works best when you describe the business rules:
Design a schema for an e-commerce platform:
- Customers can have multiple addresses (billing and shipping)
- Products belong to categories (products can be in multiple categories)
- Products have variants (size, color) with separate inventory tracking
- Orders contain order items that reference product variants
- Orders have a status lifecycle: pending → paid → shipped → delivered → completed
- Support partial refunds on individual order items
- Track inventory changes with a log (who, when, quantity change, reason)
Step 2: Review the generated schema
Claude Code produces a complete schema. Ask it to explain trade-offs:
model Customer {
id String @id @default(cuid())
email String @unique
name String
addresses Address[]
orders Order[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([email])
@@index([deletedAt])
}
model Product {
id String @id @default(cuid())
name String
description String?
slug String @unique
categories CategoryProduct[]
variants ProductVariant[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([slug])
@@index([deletedAt])
}
model ProductVariant {
id String @id @default(cuid())
productId String
product Product @relation(fields: [productId], references: [id])
sku String @unique
name String // e.g., "Large / Red"
price Decimal @db.Decimal(10, 2)
inventory Int @default(0)
orderItems OrderItem[]
@@index([productId])
@@index([sku])
}
model Order {
id String @id @default(cuid())
customerId String
customer Customer @relation(fields: [customerId], references: [id])
status OrderStatus @default(PENDING)
items OrderItem[]
total Decimal @db.Decimal(10, 2)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([customerId])
@@index([status])
@@index([createdAt])
}
enum OrderStatus {
PENDING
PAID
SHIPPED
DELIVERED
COMPLETED
CANCELLED
}
Step 3: Optimize indexes for your queries
Tell Claude Code about your most common queries:
My most common queries are:
1. Get all orders for a customer, sorted by date (paginated)
2. Search products by name (full-text)
3. Get all pending orders older than 24 hours
4. Get inventory levels for all variants of a product
5. Monthly revenue report grouped by product category
Add or modify indexes to optimize these queries.
Claude Code will add composite indexes:
model Order {
// ... fields ...
// Composite index for customer orders, sorted by date
@@index([customerId, createdAt(sort: Desc)])
// For finding stale pending orders
@@index([status, createdAt])
}
model Product {
// ... fields ...
// For full-text search (PostgreSQL specific)
// Note: Prisma doesn't support GIN indexes directly;
// use a custom migration for full-text search
}
Step 4: Add data integrity constraints
Ask Claude Code to review for integrity gaps:
Review this schema for data integrity issues. Check for:
- Missing cascading deletes or restricts
- Columns that should be non-nullable
- Missing unique constraints
- Enum values that might need expansion
- Decimal precision for monetary values
Claude Code will identify issues like:
model OrderItem {
id String @id @default(cuid())
orderId String
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
variantId String
variant ProductVariant @relation(fields: [variantId], references: [id], onDelete: Restrict)
quantity Int
unitPrice Decimal @db.Decimal(10, 2) // Snapshot price at time of order
// ^^ Important: store the price, don't reference current price
@@unique([orderId, variantId]) // Prevent duplicate items
}
Step 5: Design for soft deletes
Soft deletes are common in SaaS applications. Ask Claude Code to implement them consistently:
Add soft delete support across the schema. Include:
- deletedAt column on every entity
- Default scope that excludes deleted records
- Index on deletedAt for query performance
- Cascade logic (soft-deleting a project should soft-delete its tasks)
Step 6: Add audit trails
For compliance and debugging:
model AuditLog {
id String @id @default(cuid())
entity String // "Order", "Product", etc.
entityId String
action String // "CREATE", "UPDATE", "DELETE"
userId String
changes Json // Before/after snapshot
createdAt DateTime @default(now())
@@index([entity, entityId])
@@index([userId])
@@index([createdAt])
}
Step 7: Generate and review migrations
After finalizing the schema, generate migrations:
npx prisma migrate dev --name initial_schema --create-only
Ask Claude Code to review the generated SQL:
Review the migration SQL in prisma/migrations/. Check for:
- Correct index types (B-tree vs GIN vs GiST)
- Proper foreign key constraints
- Sensible default values
- Any destructive operations
Common Schema Design Mistakes
Ask Claude Code to check for these anti-patterns:
- Storing money as floats: Use
Decimal(10, 2)for currency - Missing timestamps: Every table should have
createdAtandupdatedAt - Over-normalization: Sometimes denormalization improves read performance
- No soft deletes: Hard deletes lose data and break referential integrity
- Generic columns: Columns named
data,value, ortypewithout clear semantics
Prevention
Add schema design rules to your CLAUDE.md:
## Database Schema Rules
- Use cuid() for primary keys (not auto-increment)
- Every table: id, createdAt, updatedAt, deletedAt
- Money: Decimal(10, 2), never Float
- Foreign keys: always specify onDelete behavior
- Add indexes for every foreign key column
- Add composite indexes for common query patterns
- Use enums for status fields with known values