Discover our learnings from scaling some of Europe's top tech orgsDownload White Paper
← All articles

What Are Domain Constraints in Database Design?

August 21, 2026

What Are Domain Constraints in Database Design?

A domain constraint limits the set of values a column will accept, combining a data type with logical rules like NOT NULL, CHECK, or an enumerated list. The core definition in DBMS theory ties the constraint to atomicity: a column should hold one indivisible value, drawn from a defined, restricted set.

A domain constraint can be as simple as a column-level rule or as reusable as a named type:

  • Column-level: age INTEGER NOT NULL CHECK (age > 0)
  • Domain-level (reusable): CREATE DOMAIN age_domain AS INTEGER CHECK (VALUE > 0);

Once age_domain exists, any table can declare a column AS age_domain and inherit the same rule automatically, per the PostgreSQL CREATE DOMAIN documentation.

Key Takeaways

Domain constraints work by pairing a data type with logical rules like NOT NULL and CHECK, and they scale best when kept nullable at the domain level with NOT NULL enforced per column.

Point Details
Definition is type plus logic A domain constraint combines a base data type with rules like CHECK, NOT NULL, or enumerated lists.
Reuse justifies a domain Use CREATE DOMAIN for rules shared across three or more tables; keep one-off rules as column-level CHECK.
NULL handling needs care Keep domains nullable and apply NOT NULL at the column to avoid outer-join and revalidation surprises.
Portability varies by engine PostgreSQL and Oracle support rich domain features; MySQL has no CREATE DOMAIN equivalent at all.
Governance extends past SQL ORM validators, masking rules, and privacy domains extend the same logic into applications and analytics.

Table of Contents

Why Do Domain Constraints Matter for Data Quality?

Domain constraints stop bad data before it ever lands in a table. A column defined as INTEGER CHECK (VALUE > 0) rejects a negative age at the moment of insert, not three reports later when someone notices the average customer age is negative 4. That single rejection point protects every query, join, and report that touches the column afterward.

The bigger payoff shows up at scale. When five tables each need a “valid US ZIP code” rule, hardcoding that regex five times means five places to fix when the rule changes. A shared domain fixes the logic once. Centralized domain definitions let a schema store formatting and validation rules in a single object, so a policy change becomes one ALTER DOMAIN instead of a hunt across dozens of CREATE TABLE statements.

This also reinforces atomic value design, a core tenet of relational theory: each cell holds a single, well-typed value rather than a delimited string or ambiguous blob.

  • Prevents invalid inserts and updates at the source
  • Reduces duplicated CHECK logic across tables
  • Enforces atomicity, which keeps queries and joins predictable

Reusable domains turn validation logic into an abstraction that reduces duplicated code, lowering both maintenance effort and the chance of a rule drifting out of sync between tables.

What Types of Domain Constraints Exist in SQL?

SQL gives you a handful of building blocks, and most real schemas combine several of them on a single column.

  1. NOT NULL — the simplest domain restriction. It removes the null value from the allowed set entirely, forcing every row to supply something.
  2. CHECK predicates — a Boolean expression evaluated against the column value. If it resolves to FALSE, the write is rejected; TRUE or UNKNOWN (the SQL null-logic result) passes. This detail matters more than it looks: a CHECK involving a null comparison often evaluates to UNKNOWN, which SQL treats as a pass, not a failure.
  3. Enumerated lists — a CHECK (status IN ('active', 'paused', 'cancelled')) clause restricts a column to a fixed, named set of values, functioning like a lightweight enum.
  4. DEFAULT clauses — not a restriction by themselves, but they pair naturally with domains to supply a fallback value when none is given.
  5. CREATE DOMAIN — an abstraction layer over a base type. It bundles a data type with NOT NULL, CHECK, and DEFAULT into one named, reusable object.

The distinction between a domain and a column-level constraint comes down to reuse. A one-off rule belongs on the column. A rule three or more tables will share belongs in a domain. Per PostgreSQL’s own documentation, CHECK expressions attached to a domain are evaluated at the moment a value is converted to that domain type, not just at insert, which affects how casts and updates behave.

How Do You Write a Domain Constraint in SQL?

Here’s a domain for US postal codes that accepts both five-digit and ZIP+4 formats:

CREATE DOMAIN us_postal_code AS TEXT
  CHECK (VALUE ~ '^\d{5}$' OR VALUE ~ '^\d{5}-\d{4}$');

CREATE TABLE customers (
  id SERIAL PRIMARY KEY,
  zip us_postal_code
);

A plain column-level version, without the reusable domain, looks like this:

CREATE TABLE employees (
  id SERIAL PRIMARY KEY,
  age INTEGER NOT NULL CHECK (age >= 18)
);

And an enum-style domain for order status:

CREATE DOMAIN order_status AS TEXT
  CHECK (VALUE IN ('pending', 'shipped', 'delivered', 'cancelled'));
Constraint style Reusable across tables Typical use
Column-level CHECK No One-off rule specific to a single table
CREATE DOMAIN Yes Shared format or range rule used in multiple tables
Enum-style CHECK (IN ...) Depends Fixed status/category lists

Portability varies by vendor: MySQL has no CREATE DOMAIN equivalent at all, so teams migrating between systems often need to convert domains into repeated column-level checks or application code.

Pro Tip: Name every constraint explicitly (CONSTRAINT valid_age CHECK (age > 0)) instead of letting the database auto-generate a name. When a constraint fails, the error message references that name, and a descriptive one saves you from guessing which rule just rejected your insert.

What Are the Best Practices When Designing Domain Constraints?

The most common mistake is putting NOT NULL on the domain itself instead of the column. PostgreSQL’s own documentation flags this: a domain marked NOT NULL applies that restriction everywhere the domain is used, which breaks the moment you need one table to allow nulls for that same type. The safer pattern is to leave the domain nullable and apply NOT NULL at the column level where it’s actually required.

Altering an existing domain is not free. Adding a new CHECK to a domain that already has rows built on it forces the database to revalidate every existing value, and a migration script that fixes bad data before flipping the constraint live avoids a failed rollout.

  • Keep domain CHECK expressions immutable. Avoid volatile functions like now() or random() inside them, since a value valid today could become invalid tomorrow with no data change.
  • Prefer nullable domains, then enforce NOT NULL per column.
  • Test constraint changes against a copy of production data before altering the live domain.

Pro Tip: Watch for NULL behavior in outer joins. A PostgreSQL reference notes that outer joins can surface null values in a column that otherwise carries a domain constraint, since the join itself introduces the null, bypassing the domain’s logic entirely.

Advanced Uses: Governance, Masking, and Privacy Domains

Domains do more than reject bad rows once you treat them as shared metadata objects. Enterprise systems like Oracle extend the concept into use-case domains, which attach annotations, display formatting, and masking rules to a data type so every application reading that column renders and protects it the same way, without touching the underlying type.

Some systems distinguish restrictive domain constraints, which block invalid input outright, from productive ones, which silently transform input — canonicalizing capitalization or rounding a decimal, for instance. Productive constraints need their own governance, since a silent correction can confuse a downstream system expecting the original value.

  • Centralized domains share formatting, validation, and masking logic across every table that uses them.
  • Use-case domains let applications standardize display and privacy rules without schema changes.
  • In analytics platforms, explicit privacy domains help differential privacy systems estimate sensitivity correctly. Omitting the domain can degrade both query performance and the privacy guarantee itself.

A Quick Checklist: Domain vs. Column-Level Constraint

Run through five questions before deciding where a rule belongs:

  1. Will this rule repeat across tables? Reuse points toward a domain.
  2. Is the rule stable, or will it change often? Stable rules are safer to centralize; volatile ones are easier to manage per column.
  3. How should NULL behave? If some tables need nulls and others don’t, keep NOT NULL at the column level.
  4. Do you need portability across database engines? MySQL and some other systems lack CREATE DOMAIN, so a domain-heavy design won’t move cleanly.
  5. How will you test it? A domain used in ten tables needs broader regression coverage than a rule touching one.

A ZIP code format used across five tables is a clear domain candidate. A one-off discount cap on a single promotions table is better left as a column-level CHECK.

Point Details
Reuse drives the decision Rules shared across three or more tables belong in a domain, not repeated column checks.
NULL handling stays local Keep domains nullable and apply NOT NULL at the column level to avoid outer-join anomalies.
Portability is not universal Not every database engine supports CREATE DOMAIN, so plan for engine-specific fallbacks.

How Do Domain Constraints Interact With Foreign Keys and Indexes?

Domain constraints and foreign keys enforce different kinds of integrity, and confusing them causes real bugs. A domain constraint governs what a single column’s value can look like in isolation, independent of any other row or table. A foreign key governs whether that value exists as a valid reference in another table. A column can pass its domain check (a properly formatted five-digit ZIP code) while still failing a foreign key check if that ZIP code isn’t in a reference table, and vice versa.

Diagram comparing domain constraints and foreign keys

The two checks run at different stages, and order matters for troubleshooting. When an insert fails, the database typically evaluates domain and column CHECK constraints first, since they don’t require touching another table. A foreign key violation surfaces separately, often with a clearer error pointing to the referenced table. Keeping these concerns separate, rather than trying to encode referential logic inside a CHECK expression, keeps error messages meaningful.

Indexes interact with domains more subtly. An index built on a domain-typed column behaves exactly like an index on the base type. A us_postal_code domain built on TEXT indexes the same way a plain TEXT column would. The domain’s CHECK constraint doesn’t slow down index lookups, since the check runs at write time, not at read time. Where domains do affect index design is uniqueness: if a domain includes a formatting rule that normalizes input (say, stripping hyphens from a phone number), a unique index on that column only catches duplicates that match the exact stored format, not every equivalent representation of the same value.

Do Domain Constraints Slow Down Large Databases?

The performance cost of a domain constraint comes almost entirely from the CHECK expression’s complexity, not from the fact that it’s a domain rather than a column-level rule. A simple range check like VALUE > 0 costs essentially nothing, evaluated once per write in constant time. A regex-based check, like the ZIP code pattern used earlier, costs more per row because pattern matching is inherently more expensive than a numeric comparison, though the difference is negligible for typical write volumes.

Where performance actually degrades is bulk operations. Loading a million rows into a table with a domain-typed column means the database evaluates that domain’s CHECK expression a million times, once per row, during the load. For a lightweight check this barely registers. For a check involving a subquery or a heavy string operation, bulk loads can slow noticeably, and some teams work around this by disabling constraints during a bulk load and revalidating afterward.

Altering a domain that’s already in use across a large table is the more common performance trap. Adding a stricter CHECK to an existing domain forces a revalidation pass over every row currently using that domain, across every table. On a table with tens of millions of rows, that revalidation can run long enough to require scheduling it as a proper migration window rather than a quick schema change.

Read performance is unaffected either way. Domain constraints only fire on write. A SELECT query against a domain-typed column runs exactly as fast as it would against the raw base type, since there’s nothing left to check once the value is already stored.

How Do Domain Constraints Compare Across Database Systems?

Support for domain constraints varies more than most developers expect, and this is where a database-agnostic schema design plan can quietly fall apart. PostgreSQL implements CREATE DOMAIN closely to the SQL standard, supporting NOT NULL, CHECK, and DEFAULT on a named, reusable type, as documented in the PostgreSQL CREATE DOMAIN reference.

Oracle takes the concept further with use-case domains, which bundle not just validation but display formatting, masking, and ordering behavior into a single reusable object, aimed squarely at enterprise applications that need consistent rendering across many client applications.

MySQL, by contrast, has no CREATE DOMAIN statement at all. Developers working in MySQL simulate domain-like behavior with repeated column-level CHECK constraints (supported since MySQL 8.0.16) or by pushing validation into application code or stored procedures. This gap is one of the most common surprises for teams migrating a PostgreSQL schema to MySQL, since every CREATE DOMAIN statement has to be manually unpacked into individual column constraints.

SQL Server sits in between: it supports rule objects and check constraints but deprecated its older CREATE RULE mechanism in favor of column and table-level CHECK constraints, nudging developers toward the same column-level pattern MySQL uses by necessity rather than by standard.

The practical upshot: if portability across engines matters for your project, don’t lean heavily on CREATE DOMAIN. Build validation logic that translates cleanly to column-level CHECK constraints, and treat true domain reuse as a PostgreSQL or Oracle-specific convenience rather than a universal SQL feature.

What Are the Limitations of Domain Constraint Support?

Domain constraints have real limits, and vendor gaps compound them. The most persistent limitation, even in systems with full CREATE DOMAIN support, involves NULL logic. A CHECK expression that evaluates to UNKNOWN (which happens whenever a NULL is involved in a comparison) is treated as a pass, not a failure. This means a CHECK (VALUE > 0) domain will happily accept a NULL value unless NOT NULL is added separately, a nuance the PostgreSQL documentation calls out explicitly.

Outer joins introduce a related trap. A translated PostgreSQL reference notes that a LEFT JOIN or RIGHT JOIN can produce a NULL in a domain-typed column even though that domain’s constraint never explicitly allows nulls, because the join itself manufactures the null value after the constraint has already been satisfied on the base table.

Cross-vendor support gaps are the second major limitation. MySQL’s total lack of CREATE DOMAIN means any schema built around domains needs a translation layer or full rewrite to run there. Even among systems that do support domains, the feature set diverges: Oracle’s use-case domains with masking and display rules have no direct equivalent in PostgreSQL, and PostgreSQL’s straightforward CHECK-based domains don’t carry over one-to-one into Oracle’s model.

Altering domains also carries a structural limitation: most systems require revalidating every dependent row when a domain’s rule changes, which rules out casual, frequent domain edits on large, actively used tables.

Can Domain Constraints Apply Outside SQL Databases?

The logic of a domain constraint doesn’t stop at the database layer. Object-relational mapping frameworks routinely reimplement the same validation concept in application code, sometimes redundantly with the database, sometimes as the only enforcement layer at all.

Developer coding with hands on keyboard

In Python’s Django ORM, model field validators (MinValueValidator, RegexValidator) mirror exactly what a SQL CHECK constraint does, just enforced before a write ever reaches the database. Ruby on Rails’ Active Record validations work the same way, letting a developer declare validates :age, numericality: { greater_than: 0 } on a model rather than in the schema. Java’s Bean Validation framework (@NotNull, @Min, @Pattern annotations) applies domain-style rules directly to object fields.

The tradeoff between database-level and application-level enforcement comes down to trust boundaries. A CHECK constraint in the database protects data integrity no matter which application, script, or direct SQL connection writes to the table. An application-level validator only protects data that passes through that specific application, leaving the door open for a bulk import script or a second application to insert values the ORM would have rejected.

Most production systems that get this right use both layers rather than choosing one. The application-level validator catches errors early and gives users a friendly message before a request ever reaches the database. The domain constraint or CHECK clause acts as the last line of defense, guaranteeing that no code path, present or future, can quietly corrupt the column’s data.

What Do Engineers Actually Learn From Rolling Out Domain Constraints?

Domain constraints behave differently in a spec than they do in a live migration. Adding them to development and CI environments first, then revalidating data after every migration, catches the edge cases a design review misses. Documenting each domain in a schema registry, tied to the team’s broader data governance process, saves the next engineer from reverse-engineering a CHECK expression’s intent months later. Where a domain touches sensitive data, tying its definition to existing privacy and masking rules keeps validation and protection from drifting apart as the schema grows.

Frequently Asked Questions

What is the difference between a domain constraint and a column constraint? A domain constraint is defined once with CREATE DOMAIN and reused across multiple tables and columns. A column constraint is written directly on a single column in one table and applies only there.

Does NOT NULL belong on the domain or the column? PostgreSQL’s documentation recommends applying NOT NULL at the column level rather than the domain, since a domain marked NOT NULL forces that restriction everywhere it’s used, even in tables that need to allow nulls.

Can you change a domain constraint after data already exists? Yes, but altering a domain’s CHECK expression forces the database to revalidate every row already using that domain, which can require a scheduled migration on large tables.

Do all databases support CREATE DOMAIN? No. PostgreSQL and Oracle support domain types with varying feature depth, but MySQL has no CREATE DOMAIN statement and relies on repeated column-level CHECK constraints instead.

Are domain constraints only relevant to SQL databases? No. ORM frameworks like Django, Rails, and Java’s Bean Validation implement the same concept as application-level field validators, often layered on top of database-level domain constraints for defense in depth.

Sources

Want to put this into practice?

Book a session with a Tekkr operator who's run the playbook in the field.

What Are Domain Constraints in Database Design? · Tekkr