Nice article. I've used composite types with success for similar problems. Some tips:

- Composite types are useful for data validation. Money types and unit-based types are good for composite types.

- Avoid over-using composite types. Most of the time, regular columns are better. Not many tools interact well with composite types, like reporting tools or database libraries.

- Like the article notes, avoid using composite types for data types that may change.

- A JSONB domain type is a good alternative for data types that change often. Note that if you put the validation into a helper function, Postgres will not revalidate the domain if the function definition changes.

- Using composite types is mildly annoying since you must wrap the composite column in parenthesis to access the field.

    -- Bad 
    SELECT package_semver.major FROM package_version;
    -- Errors with `missing FROM-clause entry for table "package_semver"`
    
    -- Good
    SELECT (package_semver).major FROM package_version;
- When defining a domain type, separate checks into named constraints with ALTER DOMAIN. The Postgres error message for check validation failures is lackluster and provides little beyond "it failed."

    CREATE DOMAIN item_delta_node AS jsonb NOT NULL;
    
    ALTER DOMAIN item_delta_node
      ADD CONSTRAINT item_delta_node_is_object
        CHECK (coalesce(jsonb_typeof(value), '') = 'object');
    
    ALTER DOMAIN item_delta_node
      ADD CONSTRAINT item_delta_node_item_delta_id_null_or_num
        CHECK (coalesce(jsonb_typeof(value['item_delta_id']), 'number') IN ('null', 'number'));
totally agree

""Using composite types is mildly annoying since you must wrap the composite column in parenthesis to access the field"""

^ yeah that is frustrating

--

if you create domains over jsonb frequently, check out pg_jsonschema[1] as a way to express the constraints more concisely

[1] https://github.com/supabase/pg_jsonschema