← BACK TO BLOGENTRY 08 / 08
DATABASES/9 MIN

Database Schemas That Survive the Client Changing Their Mind

A practical field guide to designing MySQL schemas for clients who say "just one more field" every sprint. Status columns, soft deletes, JSON escape hatches, and when to say no.

KS
Kusal Salpura
Full-Stack Developer & Software Engineer

Database Schemas That Survive Client Changes

Every client says "just one more field". After eight client projects, I have a schema playbook that absorbs those changes without migrations, downtime, or tears. It is not clever. It is just disciplined.

The five rules

1. Status columns are enums, not booleans

If you have is_active, is_pending, is_archived on the same row, you have a bug waiting to happen. Replace with a single status enum:

status ENUM('draft', 'pending', 'active', 'archived', 'deleted')

Yes, deleted is in there. Soft deletes are non-negotiable for client work. The day someone "accidentally" deletes a paid order, you will thank me.

2. JSON columns for "just one more field"

MySQL 8 JSON columns are your escape hatch. Every entity gets a meta JSON column. When the client asks for a new field that does not need indexing — a favorite color, an internal note, a secondary phone number — it goes into meta without a migration.

ALTER TABLE orders ADD COLUMN meta JSON NOT NULL DEFAULT (JSON_OBJECT());

The rule: anything in meta is allowed to disappear. If it becomes load-bearing, promote it to a real column with a migration.

3. Timestamps: four of them

Every table gets:

Laravel's migrations give you the first two by default. Add the other two. You will need them.

4. UUIDs for public IDs, ints for primary keys

Internal primary key is a BIGINT auto-increment. Public-facing identifier (in URLs, API responses) is a UUID stored in a separate uuid CHAR(36) column with a unique index.

The int PK keeps joins fast and storage small. The UUID keeps URLs unguessable and decoupled from row count — clients cannot tell how many orders you have processed by looking at /orders/1042.

5. Money is always integer cents

DECIMAL(10,2) works until you start doing currency conversions. Store money as BIGINT cents. $19.99 is 1999. Format on the way out. This has saved me from at least three rounding bugs that would have been embarrassing in production.

The "say no" list

These requests always come. Always say no.

The migration discipline

The takeaway

The schema you ship on day one is not the schema you will have on day 365. Design for change. The five rules above buy you 90% of the flexibility you need, and the discipline of saying "no" to the rest keeps the schema legible three years in.