Most bad architecture decisions cost you a sprint to fix. A small number of database decisions cost you a migration weekend with the whole team on call, at 2am, on a system that can't go down. Those are the ones worth slowing down for.
Primary key strategy
Auto-incrementing integers are fine — until you need to merge data across environments, shard, or expose IDs in a public API and don't want competitors counting your signups. Switching from integer IDs to UUIDs after launch means touching every foreign key, every index, and every cached reference in the codebase.
My default now: UUIDv7 (or ULID) as the primary key from day one, unless you have a specific, measured reason not to. You get the sortability of an incrementing ID with none of the lock-in.
Multi-tenancy: decide the isolation model before you write the first migration
There are three real options — shared tables with a tenant_id column, schema-per-tenant, or database-per-tenant. Each has a completely different operational story for backups, noisy-neighbor performance, and compliance requirements like data residency.
The mistake I see most often: teams start with shared tables because it's fastest to build, then get an enterprise deal that requires data isolation, and discover the migration touches every single query in the application. Decide this one on purpose, even if the answer is "shared tables, and here's why that's fine for our market."
Soft deletes or hard deletes — pick one and mean it
Soft deletes (deleted_at) feel safer, but they quietly break every unique constraint in your schema (can you re-register with an email that belongs to a "deleted" account?) and every count query needs a WHERE deleted_at IS NULL that someone will eventually forget.
If you need an audit trail, use an actual event log or an archive table — don't overload your primary tables with a state that changes the meaning of every row.
Timestamps, time zones, and the bug you won't find for a year
Store everything in UTC. Always. The number of production incidents I've debugged that trace back to a datetime column stored in local time, on a server that later moved regions, is not small. This is a five-minute decision that prevents a multi-day investigation.
Normalize until it hurts, then denormalize on purpose
Start normalized. When a specific read path becomes measurably slow — not hypothetically slow — denormalize that path deliberately, with a comment explaining why, and a plan for keeping the duplicated data in sync (a job, a trigger, an event listener — pick one and document it).
The actual rule
None of these decisions are hard to make correctly on day one. They're expensive only when you discover them after you have real customers and real data. Fifteen minutes of deliberate architecture at the start of a project buys you out of the categories of incident that take down a weekend.
Building something and want a second opinion?
Thirty minutes, no pitch deck — bring the problem.
Book a discovery call