Engineering Note
Data

Database Schema Design Lessons

Designing Data Around Product Behavior

9 min read
IntermediateData

Introduction

Database design is not only about storing data. It is about shaping data around how the product is used, queried, updated, and scaled over time.

A strong database schema makes backend logic simpler, improves query performance, reduces duplicated data, and makes future features safer to build.

This note focuses on practical database schema design lessons from real product engineering, especially the decisions that affect scalability, reliability, maintainability, and user experience.

The Problem

Schema problems usually do not appear on day one. They appear later when features grow, queries become heavier, production data increases, and the backend starts depending on unclear relationships.

Common Failures

  • Poor relationships create complex queries later
  • Missing indexes slow down important screens
  • Unclear ownership causes duplicated data
  • Schema changes become risky after production data exists

Engineering Impact

  • Backend logic becomes harder to maintain
  • Queries become slower as data grows
  • Feature changes require risky migrations
  • Data consistency becomes harder to guarantee

The challenge is to design a schema that stays understandable, efficient, and safe to evolve as the product becomes more complex.

System Design / Approach

The approach is to design the database around real product behavior, not just around entity names. A good schema supports the way the application actually reads, writes, filters, and updates data.

1. Start from Product Queries

Before finalizing tables or collections, identify the most important screens, filters, dashboards, and user workflows the product will need.

2. Define Relationships Clearly

Relationships should make ownership, access, and data flow obvious. Clear relations reduce confusion in both backend logic and frontend integration.

3. Plan Indexes and Migrations Early

Indexes should support frequent lookups, and migrations should be small, reversible, and safe for production data.

Implementation

Step 1: Model Product Relationships

Tables or collections should be designed around how users interact with the product. A schema should make common product flows easy to query.

relationships.txt
User
  └── Project
        └── Task
              └── Comment

Clear relationships make backend queries easier to write, understand, and optimize.

Step 2: Add Indexes Carefully

Frequently searched, filtered, or joined fields should be indexed to improve read performance on important product screens.

index.sql
CREATE INDEX idx_tasks_user_id
ON tasks(user_id);

Indexes make reads faster, but they also add cost to writes. That is why indexing should be intentional, not automatic.

Step 3: Plan Safe Migrations

Schema changes should be small, reviewed, and easy to recover from. Once production data exists, careless migrations can cause serious data issues.

migration.sh
npx prisma migrate dev --name add_task_status

Good migration discipline protects production data and makes schema evolution safer over time.

Trade-offs

Approach Benefit Cost
Normalized Schema Reduces duplication and improves data consistency Requires more joins and careful query planning
Denormalized Schema Improves read speed for specific use cases Increases the risk of duplicated or stale data
Database Indexes Faster filters, lookups, and query performance Slower writes and additional storage usage

Real-World Impact

Faster Queries

Important screens become faster because the schema supports common filters and lookups.

Cleaner Backend

Clear relationships make service logic easier to write, debug, and maintain.

Safer Features

Future features become easier to add because the data model is stable and predictable.

Key Takeaways

Database design should follow product behavior

Indexes are useful only when they match real query patterns

Relationships affect frontend and backend complexity

Schema decisions become harder to change after launch

Good data modeling reduces hidden technical debt

Future Improvements

Document all major data relationships

Add query performance checks for important pages

Use migration reviews before production changes

Add seed scripts for development data

Track slow queries during testing