Database schema design is the discipline of preserving the meaning of facts while products, teams, traffic, and regulations change. A table is not merely a storage shape: it says what one row represents, which values are authoritative, which relationships must exist, and how a correction is recorded. Start with that contract before choosing an ORM, partition key, or cloud database. A useful design lets a support engineer explain a record, lets a report count it once, and lets a release change it without silently invalidating old data. The PostgreSQL constraints guide is a useful reminder that integrity belongs close to shared data.
Define the grain before naming tables
Write one sentence for every proposed entity: one row is one customer, one order, one payment attempt, one line item, or one immutable event. Do not let a table mix a current snapshot with a history of changes. If a customer can have several addresses, use a child relationship and state which address is effective; do not serialize an arbitrary list into a text field that no query can validate. Record identifiers, lifecycle states, effective time, correction rules, retention, and the team allowed to change each fact. When two teams disagree about a row’s meaning, the schema review is not ready to pass. The PostgreSQL data-definition guide helps separate structural choices from application workflow.

| Question | Failure prevented | Design response |
|---|---|---|
| What is one row? | Double counting and contradictory updates | Name the grain and keep repeating facts in child records |
| What is authoritative? | Two services overwriting each other | Assign a writer and expose a read contract |
| Can it change? | Lost history and weak auditability | Store revisions, effective periods, or events |
| What must be unique? | Duplicate imports and retry-created records | Use stable identifiers and database uniqueness |
Encode invariants where every writer can see them
Application validation improves messages, but it cannot protect a shared database from a second service, a migration script, a replay, or a race. Use NOT NULL for required facts, CHECK for bounded values, UNIQUE for real uniqueness, and foreign keys for relationships that must not dangle. Decide deletion behavior explicitly: prohibit it, cascade it, retain a tombstone, or detach the relationship. A constraint is not a substitute for a business workflow; it is the last durable guard against an impossible state. Test concurrent writes as well as happy paths, especially where two requests might claim the same inventory, reservation, or external reference.
Choose identifiers and time semantics deliberately
A primary key should be stable, opaque where exposure is risky, and usable across imports and retries. UUIDs are standardized by the IETF UUID specification, but a UUID does not make a record idempotent by itself: pair it with a unique business key or idempotency token when a request can be replayed. Separate created time, effective business time, and observed time. A late event may be valid even when it arrives after a newer snapshot. Name the time zone and precision policy, and avoid a single updatedat column pretending to answer every historical question.
Plan migrations as compatible releases
Treat a schema change as a sequence of deployable states. First add the new nullable column or table, then teach the application to write both representations if needed, backfill in bounded batches, validate the result, switch reads, and remove the old shape only after rollback is no longer required. PostgreSQL’s ALTER TABLE reference notes that some operations can scan or lock large tables; estimate duration, lock behavior, replication impact, and cancellation before production. A migration is successful only when old and new application versions can coexist during the release window.
| Stage | Evidence to collect | Stop condition |
|---|---|---|
| Add | DDL succeeds on production-like size | Lock or replication impact exceeds budget |
| Dual write | New values reconcile with old values | Mismatch rate is unexplained |
| Backfill | Counts, checksums, and error queue are visible | Lag threatens customer work |
| Switch reads | Latency and business results remain normal | Fallback path is untested |
| Remove | No dependent readers or rollback need remains | Unknown consumer still exists |
Index observed access patterns, not imagined ones
An index is a write cost and a memory claim, so begin with real queries: filters, joins, ordering, pagination, and the rows a user expects to see. Composite column order should follow the common predicate and selectivity rather than a generic preference. Add an index when it improves a measured path, inspect the query plan, and watch write latency and storage growth. For large time-oriented tables, partitioning can simplify retention and maintenance, but it introduces routing and uniqueness considerations. Partition only when the operating problem justifies the added rules.
Operate the schema as a product
Ownership continues after the migration merges. Publish data definitions, sensitive-field handling, retention, backup and restore expectations, and the escalation route for incorrect records. Monitor constraint failures, deadlocks, migration duration, replica lag, bloat, slow queries, and growth against the capacity plan. Test restores with representative dependencies; a successful backup job is not proof that the business can recover. Give consumers a deprecation window and a contact, because undocumented columns become permanent contracts through accidental use.
- Write the row-grain and authoritative-writer sentence before the DDL.
- Prefer durable constraints to cleanup jobs for states that must never exist.
- Make migrations reversible or prove why a forward-only step is safe.
- Use query evidence and production-like cardinality before adding indexes or partitions.
- Treat definitions, restore drills, and deprecation notices as part of the schema surface.
Key takeaways
Good database schema design makes meaning explicit, protects invariants, and gives change a controlled path. Keep a decision record covering row grain, keys, writers, time, constraints, migration stages, access patterns, and ownership so teams can challenge ambiguity before it becomes expensive.
Database schema design FAQ
Should every business rule be a database constraint?
No. Put universal integrity rules such as required values, uniqueness, and referential existence in the database. Keep workflow sequencing, permissions, and human review in application services, while using transactions and tests to connect the two.
How do I change a large table safely?
Measure the operation on production-like data, separate DDL from backfill, batch work, monitor locks and replication, and keep compatible readers and writers during the transition. Define a pause and rollback decision before starting.
When should a table be partitioned?
Partition when retention, pruning, maintenance, or access patterns create a measurable need. Do not use partitioning as a default answer to a vague performance concern; it adds routing, uniqueness, and operational complexity.
Consider a support subscription that can be paused, resumed, transferred, or cancelled. A weak design stores the current status and a free-text note on one customer row. It cannot answer who changed the status, whether a retry repeated the transition, or which plan was active when an invoice was issued. A stronger design separates customer, subscription, plan, subscription event, and invoice facts. The subscription has a stable identifier and an effective state; events record actor, time, previous state, next state, reason, and request id; invoices point to the plan version used at issue. A unique constraint on the external provider.
Ask whether an import can create a duplicate, whether two concurrent requests can claim the same resource, whether a late event can be represented, whether a deleted record remains explainable, and whether a restore can reproduce the customer-visible state. Run the questions against real examples, not only a diagram. Estimate the largest table, write rate, retention window, backfill duration, lock behavior, and index growth. Record the answer beside the migration plan so a future maintainer understands why a constraint, partition, or denormalized field exists. This is the practical bridge between a good model and a database that remains changeable under production pressure.
Before approval, ask the reporting owner to count the same business event from the proposed schema and from a trusted source. Ask the operator to correct a record and explain what remains auditable. Ask the release owner to pause a backfill and resume it without duplicating work. These scenarios turn abstract design quality into evidence. Record expected row counts, constraint failures, index plans, migration locks, restore duration, and the contact for each signal. A schema review should leave behind decisions that another engineer can test, not just a diagram that describes the current tables.
A useful review session ends with three artifacts: a workflow map, a state inventory, and an outcome baseline. The map names actors and handoffs; the inventory lists loading, empty, blocked, stale, permission, success, and failure states; the baseline captures completion time, correction, escalation, and workaround. Use them during design, build, acceptance, and the first production review. If the interface changes a policy decision, include the policy owner. If it changes keyboard or focus behavior, include an accessibility check. This keeps internal tool UX connected to the work rather than to a one-time visual approval.
Every quarter, compare cache benefit with the cost of operating it. Review which data classes are still reused, whether their freshness windows changed, whether key cardinality grew, and whether incidents required a bypass. Check that invalidation consumers are still subscribed, that eviction has not moved into a critical path, and that a new tenant or permission dimension did not make an old key unsafe. If the origin query was optimized, rerun the baseline and consider removing the cache. A cache is healthy when it can be explained, tested, disabled, and justified with current evidence.
Publish a weekly view of review age by risk, not a leaderboard of individual reviewers. When a change waits, distinguish missing ownership from missing evidence, and fix the system that caused the wait. When a reviewer requests changes, make the reason precise enough that the author can act without a meeting. When emergency work bypasses the normal path, require a short follow-up review and record the accepted risk. These controls create accountability without turning review into surveillance. The system should help a new engineer understand how decisions are made and help an experienced engineer spend attention where a mistake would matter.
Before the first schema migration, record the DDL shape, lock expectation, backfill batch size, validation query, rollback trigger, replica budget, alert thresholds, and accountable owner. During an incident, identify whether the defect is wrong data, missing data, a blocked migration, replica lag, or unsafe sharing; each has a different response. After recovery, compare application reads with authoritative records and write down whether the schema contract held. At renewal or architecture review, include storage, backup, indexing, migration maintenance, and support contacts in the cost. This checklist prevents a data model from becoming invisible infrastructure that only appears when it fails.
Suppose a custom API adds bulk case reassignment. The author should explain the maximum batch size, authorization scope, idempotency behavior, partial success response, audit event, rate limit, and operator recovery. Unit tests cover selection rules; integration tests cover the queue and identity provider; contract tests protect clients; an end-to-end scenario verifies a batch with valid, unauthorized, duplicate, and missing records. The reviewer checks whether a retry can create a second assignment, whether an error response identifies which records need attention, and whether support can find the request by correlation id. The release owner tests a small cohort, watches queue age and assignment correctness, and keeps a feature switch. This is more useful than requiring two approvals by default because it ties review effort to the actual consequence. After release, compare manual correction, support contacts, and processing time with the baseline. If the API is later extended for another tenant or workflow, revisit the key and authorization assumptions instead of treating the original approval as permanent permission.
| Question | Evidence | Decision owner |
|---|---|---|
| Can a retry duplicate work? | Idempotency and replay test | Service owner |
| Can a caller cross a tenant boundary? | Negative authorization matrix | Security owner |
| Can an operator recover partial success? | Runbook and reconciliation queue | Operations owner |
| Can a client understand failure? | Problem details contract test | API owner |
Imagine a catalog cache begins returning old availability after a supplier update. The first response is not to flush everything blindly. Confirm whether the issue is a missed event, a wrong key, a delayed origin, or an allowed stale policy. Compare one cached response with the authoritative record, inspect age and representation version, and identify which tenants and actions are affected. If checkout rechecks authority, the immediate customer risk may be limited to display; if the cached value controls a purchase decision, traffic must bypass the cache. The incident owner can pause writes to the cache, repair the event consumer, purge the affected key family, and compare counts after recovery. The post-incident review asks why the stale window was not visible, why the purge scope was unknown, and whether the runbook named the correct owner. Add a test for the failure that actually occurred, not a generic cache test. Production readiness is demonstrated by the ability to classify and contain an incident without guessing.
| Observation | Likely action | Evidence before restore |
|---|---|---|
| Wrong audience | Disable sharing and rotate key version | Isolation test passes |
| Missed purge | Replay event or targeted purge | Origin and cache agree |
| Origin overload | Coalesce misses and rate-limit fallback | Tail latency recovers |
| Stale but allowed | Show degraded state and monitor age | Age remains within contract |
Conclusion
A durable schema is a shared agreement about facts, not a pile of columns. Define the grain, constrain what must be true, plan compatible migrations, observe real access patterns, and keep ownership visible. Those choices make a database easier to evolve without sacrificing the meaning that customers and operators depend on. Schema design remains a shared agreement about facts, integrity, safe change, and accountable operation.