Límites de Esquema Orientados al Dominio
Los contextos delimitados mapeados a esquemas de PostgreSQL te proporcionan aislamiento de nombres de dominio sin el coste operativo de bases de datos separadas.
Receta
Tarjeta de referencia rápida - lista para copiar y pegar.
CREATE SCHEMA identity;
CREATE SCHEMA billing;
CREATE SCHEMA catalog;
REVOKE ALL ON SCHEMA billing FROM PUBLIC;
GRANT USAGE ON SCHEMA billing TO billing_app_role;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA billing TO billing_app_role;
CREATE TABLE identity.users (
user_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email text NOT NULL UNIQUE
);
CREATE TABLE billing.subscriptions (
subscription_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id uuid NOT NULL, -- referencia lógica; FK opcional entre contextos
plan_code text NOT NULL
);Cuándo usar esto:
- Varios equipos comparten un clúster de PostgreSQL pero poseen dominios diferentes.
- Necesitas una propiedad más clara que un esquema
publicplano. - El acoplamiento entre contextos debe ser explícito (FK, evento o API) en lugar de uniones accidentales.
Ejemplo de Trabajo
BEGIN;
CREATE SCHEMA IF NOT EXISTS identity;
CREATE SCHEMA IF NOT EXISTS catalog;
CREATE SCHEMA IF NOT EXISTS fulfillment;
CREATE ROLE storefront_app LOGIN PASSWORD 'replace-me';
CREATE ROLE fulfillment_app LOGIN PASSWORD 'replace-me';
CREATE TABLE identity.customers (
customer_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email text NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE catalog.products (
product_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku text NOT NULL UNIQUE,
name text NOT NULL,
price numeric(12, 2) NOT NULL CHECK (price >= 0)
);
CREATE TABLE fulfillment.orders (
order_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id uuid NOT NULL,
product_id bigint NOT NULL REFERENCES catalog.products (product_id),
quantity integer NOT NULL CHECK (quantity > 0),
status text NOT NULL DEFAULT 'pending',
created_at timestamptz NOT NULL DEFAULT now()
);
-- FK entre contextos solo donde el acoplamiento es intencional
-- identity.customers se referencia lógicamente por customer_id (sin FK para evitar acoplamiento estrecho)
REVOKE ALL ON SCHEMA identity, catalog, fulfillment FROM PUBLIC;
GRANT USAGE ON SCHEMA identity, catalog TO storefront_app;
GRANT SELECT, INSERT, UPDATE ON identity.customers TO storefront_app;
GRANT SELECT ON catalog.products TO storefront_app;
GRANT INSERT ON fulfillment.orders TO storefront_app;
GRANT USAGE ON SCHEMA catalog, fulfillment TO fulfillment_app;
GRANT SELECT ON catalog.products TO fulfillment_app;
GRANT SELECT, UPDATE ON fulfillment.orders TO fulfillment_app;
ALTER ROLE storefront_app SET search_path = identity, catalog, fulfillment, public;
ALTER ROLE fulfillment_app SET search_path = fulfillment, catalog, public;
COMMIT;Lo que esto demuestra:
- Un esquema por contexto delimitado (
identity,catalog,fulfillment). GRANTbasado en roles limita qué aplicación puede escribir en qué tablas.search_pathpor rol mantiene los nombres no calificados predecibles.- Referencia entre contextos a través de
customer_idsin una FK dura cuando los contextos evolucionan de forma independiente.
Análisis Profundo
Cómo Funciona
- Los esquemas de PostgreSQL son espacios de nombres dentro de una sola base de datos, no servidores separados.
GRANT USAGE ON SCHEMAcontrola la visibilidad; los privilegios de tabla controlan la lectura/escritura.ALTER ROLE ... SET search_pathevita prefijoscatalog.productsen el SQL de la aplicación cuando se desea.- Las FK entre esquemas son compatibles pero crean acoplamiento de despliegue - las migraciones en un contexto pueden bloquear a otro.
Patrones de Límites de Contexto
| Patrón | Acoplamiento | Cuándo |
|---|---|---|
| Esquema por contexto | Bajo-Medio | Clúster compartido, equipos distintos |
| FK entre esquemas | Medio-Alto | Se requiere consistencia fuerte |
| Solo referencia de ID | Bajo | Contextos versionados independientemente |
| Tabla de outbox/evento | Bajo | Se prefiere integración asíncrona |
Notas SQL
-- Listar tablas por esquema
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
ORDER BY 1, 2;
-- Mover una tabla mal colocada al contexto correcto
ALTER TABLE public.invoices SET SCHEMA billing;Trampas
- Todo en
public- los equipos se sobrescriben las convenciones de nombres. Solución: crear esquemas de dominio desde el primer día. - Espagueti de FK entre esquemas - un fallo de migración bloquea todos los contextos. Solución: preferir IDs lógicos más validación de aplicación o consistencia eventual.
GRANTexcesivamente amplio -GRANT ALL ON SCHEMA public TO appexpone tablas de catálogo a facturación. Solución: concesiones de mínimo privilegio por rol.search_pathimplícito -SELECT * FROM productsno calificado accede al esquema incorrecto. Solución: establecersearch_pathpor rol o calificar siempre los nombres.- Secuencias compartidas entre contextos - colisiones accidentales de ID en los registros. Solución: nombres de tabla con prefijo de contexto y columnas de identidad separadas.
Alternativas
| Alternativa | Usar Cuándo | No Usar Cuándo |
|---|---|---|
| Base de datos por servicio | Aislamiento fuerte, escala independiente | Equipo pequeño, altas necesidades de informes de uniones cruzadas |
| Esquema único + prefijo de nombre | Aplicaciones pequeñas, un equipo | Múltiples equipos con nombres de tabla conflictivos |
| Multi-inquilino a nivel de fila | Aislamiento de inquilino SaaS | Propiedad de dominio distinta (no aislamiento de inquilino) |
| Esquema de integración materializado | Modelos de lectura que cruzan contextos | Omitir la definición de reglas de propiedad de escritura |
Preguntas Frecuentes
¿Es un esquema de PostgreSQL lo mismo que un contexto delimitado de DDD?
Es una correspondencia práctica, no perfecta. Un contexto delimitado es un límite lingüístico y de propiedad; un esquema es una herramienta de espacio de nombres. Alinearlos cuando un equipo posee un contexto.
¿Debo usar claves foráneas entre esquemas?
Úsalas cuando ambos contextos se desplieguen juntos y la consistencia fuerte sea importante. Omítelas cuando los equipos envíen migraciones en cadencias independientes - usa referencias UUID y trabajos de reconciliación en su lugar.
¿Cómo evito SELECT entre contextos en SQL ad-hoc?
Revoca SELECT en esquemas extranjeros de los roles de aplicación. Da a los analistas un rol de solo lectura con acceso más amplio en una réplica.
¿Puede un servicio poseer múltiples esquemas?
Sí - un equipo de plataforma podría poseer identity y access_control. Evita un esquema poseído por muchos servicios; eso recrea el caos de public.
¿Qué va en el esquema de integración o informes?
Tablas desnormalizadas, vistas materializadas y consumidores de outbox - escritos por trabajos por lotes, leídos por BI. No permitas que los servicios OLTP escriban allí directamente.
¿Cómo funcionan las migraciones por esquema?
Herramientas como Flyway pueden usar schemas = billing por tabla de historial de migraciones. Mantén carpetas de migración separadas por contexto delimitado en git.
¿Deben los contextos compartir tipos enum?
Duplicar text + CHECK por contexto reduce el acoplamiento. Los tipos ENUM compartidos atan los horarios de migración.
¿Cómo interactúa esto con SaaS multi-inquilino?
El aislamiento de inquilinos (RLS, tenant_id) es ortogonal. Puedes tener un esquema billing con tenant_id en cada tabla - ver Patrones Multi-Inquilino.
¿Cuál es un buen primer paso desde un esquema público plano?
CREATE SCHEMA legacy;
ALTER TABLE public.old_invoices SET SCHEMA legacy;Mueve tablas en grupos por equipo, luego ajusta las concesiones.
¿Cómo documento los límites de contexto?
Usa COMMENT ON SCHEMA más un ADR que liste propietarios, llamadas permitidas entre contextos y uniones prohibidas.
Relacionado
- Conceptos Básicos de Modelado de Datos - ejemplo de agrupación de esquemas
- ADR: Base de Datos Única vs. Base de Datos por Servicio - cuándo los esquemas no son suficientes
- Estrategia de Evolución de Esquemas - contratos en evolución entre contextos
- Esquema Compartido + tenant_id - aislamiento de inquilino dentro de un contexto
Versiones de Stack: Esta página fue escrita para PostgreSQL 18.4 (estable 18, mantenimiento 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x, y PostGIS 3.5+.