Backend and API Development·7 min

How to Design a Database Schema That Won't Need a Rewrite in a Year

By Bahaj Abderrazak·Published April 28, 2026
How to Design a Database Schema That Won't Need a Rewrite in a Year

Most early-stage rewrites aren't caused by bad code — they're caused by a database schema that couldn't handle the product's actual growth. Here's how to avoid being one of those rewrites.

A surprising share of "we need to rebuild the whole platform" conversations trace back to one root cause: a database schema that made sense for the first version of the product but can't cleanly support what the product grew into. Good schema design upfront prevents most of this.

Normalization basics, in plain terms

Normalization is the practice of structuring your data so each piece of information lives in exactly one place, referenced elsewhere rather than duplicated. A simple example:

Poor structure: a bookings table with columns for client_name, client_email, and client_phone directly on every booking row.

Better structure: a separate clients table, with each booking referencing a client_id. The client's name, email, and phone live in one place.

Why this matters practically: if a client updates their phone number, the poor structure requires updating every single booking row that mentions them (and risks some being missed, leaving inconsistent data). The better structure updates one row, and every booking referencing that client automatically reflects the change.

This isn't academic — it's the difference between a data bug that's easy to explain ("the client's old number is showing on this one old booking") and a five-minute fix, versus a genuinely confusing, hard-to-track inconsistency spread across hundreds of rows.

The most common early-stage mistakes

1. Storing derived data instead of calculating it. Storing a total_price column that's just quantity × unit_price seems convenient, until the unit price changes and every stored total is now silently wrong. Calculate derived values at query time, or recalculate deliberately when the inputs change — don't store them as if they're independent facts.

2. Using a single generic table for fundamentally different things. A table called items used for both products and services, distinguished only by a type column, tends to accumulate columns that only make sense for one type or the other, leaving many nulls and unclear rules. If two things have meaningfully different data and behavior, they usually deserve separate tables.

3. No foreign key constraints. Skipping database-level foreign key constraints (relying on application code alone to keep references valid) is faster to build initially, but allows orphaned or inconsistent data to accumulate silently — a booking referencing a client that no longer exists, for example — until someone hits a confusing bug months later.

4. Designing only for the current feature, not the current pattern. If a user can have one address today, but the business will obviously need multiple addresses eventually (billing vs. shipping, multiple locations), designing a single address field directly on the user table instead of a separate related table locks in a limitation that's expensive to undo later.

A scalable design approach

  • Identify the core entities in the business (clients, orders, staff, services) before writing any table
  • Give each entity its own table, with relationships expressed through foreign keys, not duplicated columns
  • Anticipate one-to-many relationships early — almost everything that looks like "one" today (one address, one payment method, one location) eventually becomes "many"
  • Add database-level constraints (foreign keys, unique constraints, not-null where appropriate) rather than relying solely on application code to enforce data integrity
  • Index columns used in frequent lookups and joins, but don't over-index prematurely — this is a genuine performance/write-speed trade-off, not a "more is always better" situation

Migration strategy — because the schema will still need to change

No schema, however well designed, survives a growing product completely unchanged. The goal isn't to predict every future need — it's to make future changes safe:

  • Use a proper migration tool (built into most frameworks — Laravel migrations, Django migrations, Prisma migrate, etc.) rather than making manual changes directly in a production database
  • Write migrations that are reversible where possible, so a bad change can be rolled back cleanly
  • Test schema changes against a copy of production-like data before running them on the real database, especially for changes touching large existing tables

Why this matters more than it seems early on

Early in a project, almost any schema "works" because the data volume is small and nobody has hit the edge cases yet. The cost of a poor schema doesn't show up on day one — it shows up eight months later, as a rewrite disguised as "just fixing a few bugs." Getting the entity relationships right early, even if it takes an extra day of planning, is consistently cheaper than that.

If you're scoping a new project and want this handled from the start, see the backend and API development services page, or check the MVP development cost breakdown for how database complexity factors into project pricing.

Backend DevelopmentDatabase DesignSoftware ArchitectureSchema Design

Bahaj Abderrazak

Full-Stack Developer · Morocco · Maroc (Casablanca, Rabat & Remote)

About the author →

Related articles

Let’s begin

Building something with these tools?

I help teams apply these patterns to real products. Share your project and I'll respond with next steps.