skip to content

If the application already validates every field before saving, why still declare constraints such as NOT NULL, unique keys and foreign keys in the database schema?

level: juniorimportance: must knowfreq 58%

answer

  1. App checks are for UX, constraints are for truth
  2. Scripts, migrations, other services skip your code
  3. Check-then-write has a gap; constraints do not
  4. Data outlives the app; schema documents intent
  5. Optimizer uses constraints too

basics

~20 s

Because the application is not the only writer, and its checks are not atomic with the write. Scripts, migrations, other services and future bugs all reach the same tables. Database constraints are the last line of defense that no code path can bypass; app validation exists for fast, friendly feedback.

solid answer

~60 s

Application validation and database constraints answer different questions. The application validates **for the user**: fast feedback, field-level messages, several errors at once, business rules that change with policy. It is a UX layer. The database constrains **for the data**: it is the only place a rule holds for every writer and every moment. Concretely: - **Multiple writers.** Batch jobs, data migrations, an ops engineer in a console, a second service, a future rewrite — none of them run your validation code. - **Concurrency.** A check in application code and the subsequent write are two separate operations with a gap between them; a constraint is evaluated atomically as part of the write. - **Bugs and deploy skew.** One missed code path, or an old version still running during a rollout, and the invariant is gone. - **Lifetime.** Data outlives the application that wrote it. The schema is what the next system inherits. - **Bonus.** Constraints document intent that cannot go stale, and the optimizer can exploit them. The right posture is both, not either: validate in the app for UX, constrain in the database for truth, and handle the constraint error when it fires.

go deeper

for a junior

Give the core reason — the app is not the only writer and app checks can be skipped or raced — and name one or two constraints you would always declare.

for a middle

Separate the two jobs explicitly (UX versus guarantee), mention atomicity, and acknowledge the write cost honestly.

for a senior

Add the optimizer benefit, the deploy-skew and multi-writer realities, and describe how the write path translates violations into user-facing errors.

for a principal

Frame it as where invariants live in the architecture: stable structural rules in the schema, volatile policy in code, with a stated rule for the boundary so teams stop re-litigating it per feature.

## Two different jobs The question sounds like a choice; it is not. Application validation and database constraints are two controls with different guarantees, and mature systems run both. **Application validation is a user-experience feature.** It runs before the round trip, can report every problem on a form at once, produces messages in the user's language, and can express rules that depend on things the database cannot see — the caller's permissions, a feature flag, a pricing policy, a third-party service. It is also where rules that change monthly belong, because changing code is cheaper than migrating a schema. **Database constraints are a correctness guarantee.** They are enforced by the engine on every write regardless of origin, evaluated atomically with the write, and they cannot be forgotten by a code path that did not know about them. ## Why the application cannot carry the guarantee alone **It is not the only writer.** Even in a strictly single-service architecture, the tables are also written by data migrations, backfill scripts, an admin console, a support engineer running a manual UPDATE at 2 a.m., an ETL import, a restore, and eventually a second service someone adds. None of those execute your validation layer. The set of writers only grows over the system's lifetime. **Validation and write are not atomic.** Any rule of the form 'check the state, then write' has a window between the two steps in which another transaction can change that state. Uniqueness is the canonical example, but the shape recurs for balance checks, capacity limits and status transitions. A constraint has no window: the engine evaluates it as part of applying the write. **Code paths multiply and drift.** A new endpoint, a bulk operation, a retry handler, a test fixture that writes directly, an ORM that patches a subset of columns — each is an opportunity to skip a validator. Deploys make this worse: during a rolling release two versions run at once, and only one of them may know the new rule. **Data outlives applications.** Schemas commonly survive multiple rewrites of the code above them. Whatever invariants exist only in the application are lost with it, and the next team inherits a table whose real shape they must reverse-engineer from the data. ## The secondary benefits, which are not small **Documentation that cannot rot.** A NOT NULL, a unique key or a CHECK in the schema is a statement about the domain that is guaranteed to be true, unlike a comment or a wiki page. New engineers read the schema first. **Optimizer information.** Planners use constraints. Knowing a column is unique changes join cardinality estimates and enables plan shapes; knowing a column is NOT NULL removes NULL-handling branches; a validated CHECK can eliminate impossible predicates or prune partitions. Declared constraints often make queries faster, not just safer. **Cheaper incident response.** When a class of corruption is impossible, it never appears in an incident. The cost of finding and repairing bad data after the fact usually dwarfs the write-time cost of the constraint by orders of magnitude. ## The honest costs Constraints are not free and a good answer says so. They add work to every write, most visibly foreign keys and unique indexes. They make some schema changes and bulk loads slower or more involved. They produce errors the application must catch and translate, and an uncaught constraint violation surfacing as a 500 with a raw database message is a real, common defect. Rules that change frequently are painful to keep in the schema, because changing them is a migration rather than a deploy. Those costs argue for choosing *which* rules to encode, not for skipping the layer. The usual dividing line: stable structural invariants — identity, referential integrity, mandatory fields, value domains — go in the schema; volatile policy goes in code. ## What 'defense in depth' means here A useful mental model: the application check is the guard rail that makes the common path pleasant, and the constraint is the wall that makes the bad outcome impossible. You do not remove the wall because the guard rail is working, and you do not remove the guard rail because the wall exists — hitting the wall produces an error the user should rarely see. The corollary is that the write path must handle constraint violations gracefully rather than treat them as impossible. If your validation is correct they are rare, but rare is not never: a race, a stale read, a retry, or a rule the application does not know about will all surface as a violation. Map the constraint name to a user-facing message and, where the operation is naturally idempotent, treat the violation as a normal outcome rather than an error. ## The one-sentence answer Application validation makes the good path fast and friendly; database constraints make the bad path impossible for every writer, including the ones you have not written yet.

  • Which rules should stay in the application rather than being pushed into the schema?
    Rules that are volatile, contextual or need data the row does not contain: pricing policy that changes quarterly, permission checks that depend on the caller, validation requiring a call to another service, and anything where the correct behaviour differs by tenant or feature flag. Encoding those in the schema turns every policy change into a migration and makes the constraint effectively non-deterministic. Stable structural invariants — identity, references, mandatory fields, value domains — belong in the schema.
  • If constraints slow down writes, when is it right to drop one?
    Almost never for correctness-critical invariants; the cost of repairing corrupted data usually exceeds the accumulated write cost by a wide margin. The legitimate cases are narrow and measured: a foreign key on an extremely hot append-only path where the reference is guaranteed by construction, or constraints temporarily disabled during a controlled bulk load and re-validated afterwards. The decision should be backed by a measurement and by a compensating control, such as a reconciliation job, not by a hunch.

Client-side form validation and server-side validation stand in exactly this relation. Nobody argues that a JavaScript check makes the server check redundant, because anyone can bypass the browser. The database is the server of your data layer, and every script, migration and future service is somebody bypassing the browser.

saying these in an interview costs you the question

  • 'The application is the only thing that writes to this database' — true today, false within a year
  • Treating constraints as redundant duplication of validation logic
  • Assuming an ORM's model-level validators are enforced for every write path
  • Not handling constraint-violation errors because 'validation already prevents them'
  • Claiming constraints only cost performance and provide no query benefit

context