When building a schema in YugabyteDB, you might catch yourself repeating the same constraints, like making sure emails are valid, integers are positive, or strings aren’t empty.
With domains, you can encapsulate those rules into reusable custom types. YugabyteDB supports PostgreSQL-compatible domains, and they work seamlessly across your distributed cluster.
What is a Domain?
A domain is a wrapper around a base type (like TEXT
, INT
, CHAR
) that includes validation logic. You can reuse it across tables and invalid data will be rejected before it touches your application logic.
Example:
Email validation domain:
CREATE DOMAIN email AS TEXT
CHECK (VALUE ~* '^[^@]+@[^@]+\.[^@]+$');
CREATE TABLE users (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
email_address email NOT NULL
);
-- A valid email address
INSERT INTO users (email_address) VALUES ('alice@example.com');
-- An invalid email address
INSERT INTO users (email_address) VALUES ('not-a-valid-email');
Here’s the results of running the INSERT statements above:
yugabyte=# INSERT INTO users (email_address) VALUES ('alice@example.com');
INSERT 0 1
yugabyte=# INSERT INTO users (email_address) VALUES ('not-a-valid-email');
ERROR: value for domain email violates check constraint "email_check"
Any insert or update with an invalid email will be rejected automatically. No triggers, no extra code!
Why use Domains?
In traditional Postgres, triggers can be handy, but in a distributed environment like YugabyteDB, they can introduce problems:
-
❌ Performance hits due to coordination across nodes
-
❌ Harder debugging and replication issues
-
❌ Non-deterministic behavior under retry or failure conditions
By using domains, you’re pushing validation into the type system itselfwhich is safe, lightweight, and repeatable across nodes. That’s a cleaner, more resilient approach that scales naturally with your cluster.
More handy domain examples:
-- Positive Integers
CREATE DOMAIN positive_int AS INT
CHECK (VALUE > 0);
-- Non-Empty Strings
CREATE DOMAIN non_empty_text AS TEXT
CHECK (length(trim(VALUE)) > 0);
-- US ZIP Code
CREATE DOMAIN zip_code AS TEXT
CHECK (VALUE ~ '^\d{5}(-\d{4})?$');
-- Percentage (0–100 only)
CREATE DOMAIN percent_100 AS NUMERIC
CHECK (VALUE >= 0 AND VALUE <= 100);
-- ISO Country Code (2-letter)
CREATE DOMAIN iso_country_code AS CHAR(2)
CHECK (VALUE ~ '^[A-Z]{2}$');
-- Phone Number (basic North American format)
CREATE DOMAIN phone_number AS TEXT
CHECK (VALUE ~ '^\(\d{3}\) \d{3}-\d{4}$');
-- UUID (safeguard against bad UUID text)
CREATE DOMAIN safe_uuid AS TEXT
CHECK (VALUE ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$');
-- Currency Code (ISO 4217)
CREATE DOMAIN currency_code AS CHAR(3)
CHECK (VALUE IN ('USD', 'EUR', 'GBP', 'JPY', 'CAD'));
-- Future Timestamp Only
CREATE DOMAIN future_ts AS TIMESTAMPTZ
CHECK (VALUE > now());
NOTE: Domains support CHECK
, NOT NULL
, and DEFAULT
. They do not support UNIQUE
or FOREIGN KEY
… keep those constraints in your table schema.
In short, YugabyteDB’s support for domains brings PostgreSQL’s reusability and built-in constraint enforcement into the distributed database world, offering a low-effort, high-impact way to strengthen your data model, especially in large-scale or multi-tenant environments.
Give domains a try in your next YugabyteDB schema!
Have Fun!