Schema design decisions compound over time. A resilient database is built on pragmatic normalization, consistent column standards, and robust constraints.
Start in Third Normal Form (3NF) to eliminate data redundancy and preserve the single source of truth. Denormalize only when performance metrics demonstrate that join costs exceed the overhead of managing duplicate state.
Every mutable table should include the following standard columns:
BIGINT GENERATED ALWAYS AS IDENTITY or UUID v7. Do not use natural keys (emails, usernames) as primary keys; they are subject to change and reveal sensitive data.created_at and updated_at (both TIMESTAMPTZ NOT NULL DEFAULT NOW()).deleted_at TIMESTAMPTZ NULL for user-recoverable data.tenant_id on every table in multi-tenant systems, enabling Row-Level Security (RLS) from day one.Always favor surrogate keys for primary and foreign keys. Natural keys should be enforced via Unique Constraints but never used as the target for a relationship.
| Key Type | Strength | Weakness |
|---|---|---|
| BIGINT | Sequential, small (8 bytes), cache-friendly. | Reveals creation order; centralized sequence. |
| UUID v4 | Distributed, random (16 bytes). | Fragmented indexes; poor locality. |
| UUID v7 | Timestamp-prefixed (2024 standard). | Recommended default: Locality of BIGINT with the distributability of UUID. |
The database is the final arbiter of correctness. Do not rely exclusively on application-layer validation.
status IN ('pending', 'paid')).NUMERIC for currency (never FLOAT), TIMESTAMPTZ for time, and JSONB for semi-structured blobs.Use soft delete selectively for data where "undo" is expected. For derived or ephemeral data, use hard DELETE to maintain index performance and storage efficiency.
For high-integrity domains, use History Tables. A database trigger writes a row snapshot to a parallel table on every change, providing a verifiable audit trail with minimal application logic overhead.
Implement shared-schema multi-tenancy using Postgres Row-Level Security (RLS). By binding the tenant_id to the session context, the database enforces isolation at the storage layer, preventing cross-tenant leakage even if application queries omit a WHERE clause.
tenant_id + status).updated_at to support high-performance polling.Schema changes must be versioned, immutable, and additive-by-default.