1. What Is It?
The relational model organizes data into tables (relations), where each row represents one entity instance and each column represents an attribute of that entity. Relationships between entities are expressed through shared keys rather than physical pointers or nested structures.
Constraints are rules enforced by the database engine that prevent invalid data from being written. Without constraints, your application code becomes the only line of defense against bad data — and application code has bugs, gets bypassed by scripts, and doesn't run inside transactions initiated by other services. Constraints encode data rules once, at the storage layer, and enforce them unconditionally.
A backend team decides to skip adding a NOT NULL constraint on a critical user_email column, reasoning that their API layer already validates that emails are always provided before inserting a row. A few months later, a data migration script run directly against the database inserts several rows with NULL emails, corrupting downstream features. Which principle does this scenario best illustrate?
2. How It Works
Consider an e-commerce schema: users, orders, and order_items.
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
status TEXT NOT NULL CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled')),
total_cents BIGINT NOT NULL CHECK (total_cents >= 0),
placed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE order_items (
id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
sku TEXT NOT NULL,
quantity INT NOT NULL CHECK (quantity > 0),
unit_price_cents BIGINT NOT NULL CHECK (unit_price_cents >= 0)
);What the database enforces here:
- PRIMARY KEY — every row has a unique, non-null identifier; the engine creates an index automatically
- NOT NULL — the column must have a value; prevents "empty" rows that break application logic
- UNIQUE — no two rows can share this value;
emailbeing unique means no duplicate accounts - FOREIGN KEY —
orders.user_idmust match an existingusers.id; prevents orphaned orders - CHECK — arbitrary boolean expression;
status IN (...)prevents undocumented status strings - ON DELETE CASCADE — when an order is deleted, its items are automatically deleted too
When an INSERT or UPDATE violates a constraint, the database raises an error, rolls back the statement, and your application receives an exception. The data is never written.
Reject an order with no user
A foreign key requires orders.user_id to match users.id. A failed write is rolled back and the application receives an exception.
Suppose an application tries to insert an order for a user who does not exist.
Text version
- Suppose an application tries to insert an order for a user who does not exist.
- The database checks the foreign key by looking for a matching user.
- With no matching user, the database rejects the write and rolls back the statement.
- The application receives an exception, and the invalid order is never written.
An e-commerce database has an orders table with a foreign key user_id referencing users(id). A developer tries to insert a new order with a user_id value that does not exist in the users table. What happens?
3. What SDEs Actually Need to Know
Store money as integers, not floats. total_cents BIGINT instead of total FLOAT. Floating-point arithmetic accumulates rounding errors; integers are exact. Divide by 100 only at display time.
Foreign key constraints have a performance cost at write time. Each insert to orders triggers a lookup in users to verify user_id exists. At high insert volume this becomes measurable. Some teams disable FK enforcement and enforce referential integrity at the application layer — a tradeoff that trades write speed for the risk of orphaned data.
ON DELETE behavior choices:
CASCADE— delete child rows when parent is deleted (use for owned entities likeorder_items)RESTRICT/NO ACTION— block deletion if children exist (default; use when the parent shouldn't be deleted while children reference it)SET NULL— null out the FK column (use when the child can exist without a parent, e.g., a post whose author was deleted)
CHECK constraints pass when the expression evaluates to NULL. CHECK (quantity > 0) passes when quantity IS NULL because NULL > 0 evaluates to NULL (not false). Add NOT NULL separately if the column must have a value.
Common error messages and what they mean:
violates not-null constraint→ your code sent NULL for a required fieldviolates unique constraint→ duplicate value on a unique column; handle in code with upsert or by catching the errorviolates foreign key constraint→ referencing a row that doesn't exist, or trying to delete a parent that still has children
CHECK does not replace NOT NULL
CHECK (quantity > 0) rejects 0 but lets NULL through, because NULL > 0 is NULL, not false. A separate NOT NULL constraint rejects the missing value.
The check quantity greater than zero gives false for zero, but null for a null quantity.
Text version
- The check quantity greater than zero gives false for zero, but null for a null quantity.
- The check rejects only false, so it rejects zero but stores the null row.
- When the column also has not null, the null insert is rejected too.
- Declare not null separately whenever a column must have a value, as order items does.
A backend service stores product prices as FLOAT columns and runs millions of transactions per day. After months in production, the finance team notices cumulative discrepancies in totals. A developer proposes switching to storing prices as integers in cents (e.g., $19.99 stored as 1999) and dividing by 100 only when displaying to users. What is the core reason this change fixes the discrepancy?
4. Tradeoffs & Decisions
Enforce in the database vs. enforce in application code
| Database constraints | Application validation | |
|---|---|---|
| Enforced when? | Every write, from any client | Only when your code runs |
| Location of truth | Single place | Scattered across services |
| Error handling | Generic DB exception | Custom error with context |
| Migration cost | Schema change required | Code deploy required |
The practical answer: use both. Application validation gives users fast, specific error messages. Database constraints are the backstop that prevents corrupt data when the application has bugs.
Nullable vs. non-nullable columns
Defaulting everything to nullable feels safe but creates problems downstream: every query that reads the column must handle NULL, JOIN conditions silently drop rows, and aggregate functions skip NULLs in ways that surprise developers. Make columns NOT NULL unless you have a specific reason a value can be absent.
Surrogate vs. natural keys as primary keys
- Surrogate key (e.g.,
BIGSERIAL id) — meaningless integer, stable across renames, simple FK references - Natural key (e.g.,
email,order_number) — carries meaning but can change, is longer (worse index performance), and leaks internal IDs in URLs
Most applications use surrogate keys for PKs and add UNIQUE constraints on natural identifiers.
Use application validation and database constraints
Application validation gives specific errors. Database constraints enforce shared rules on every write, including writes that bypass application code.
Database constraints cover every write, while application validation runs only when your code runs.
Text version
- Database constraints cover every write, while application validation runs only when your code runs.
- The database keeps rules in one place instead of scattering them across services.
- Application validation can explain an error with context instead of exposing raw database constraint details.
- Database rules need a schema change, while application rules need a code deploy.
- Use both for specific feedback and protection against corrupt data when application code has bugs.
A startup's backend has multiple services — a web app, a mobile API, and a data import script — all writing to the same PostgreSQL database. The team decides to enforce a 'user email must be unique' rule only in the web app's validation layer, skipping a database-level UNIQUE constraint. What is the most significant risk of this approach?
5. Interview Cheat Sheet
Key sentences:
- "The relational model enforces data integrity through constraints at the storage layer, not just in application code."
- "A foreign key constraint guarantees referential integrity: you can't have an order for a user that doesn't exist."
- "NOT NULL, UNIQUE, CHECK, and FOREIGN KEY constraints each catch a different class of invalid data before it's persisted."
- "I store monetary values as integer cents to avoid floating-point rounding errors."
Common follow-ups:
Q: What happens if you remove a foreign key constraint for performance?
A: Writes get faster (no FK lookup), but you trade correctness guarantees for speed. Orphaned rows can accumulate, breaking JOIN queries and causing null values in your application. You need compensating application logic and periodic consistency checks.
Q: Why would a UNIQUE constraint fail on a column you know has distinct values?
A: NULL handling. A UNIQUE constraint allows multiple NULLs in most databases (PostgreSQL, MySQL) because NULL != NULL. If you're seeing unexpected uniqueness violations, check for an existing race condition or a bug that sent the same value twice in a concurrent path.
Q: What's the difference between ON DELETE CASCADE and ON DELETE RESTRICT? A: CASCADE removes children automatically when the parent is deleted — appropriate when children are owned by the parent and have no meaning without it. RESTRICT (like the default, NO ACTION) blocks deletion of the parent if any children still reference it — appropriate when the child relationship should prevent accidental deletion of the parent.