En proyectos donde los microservicios comparten una misma base de datos, hay momentos en los que un cambio que comienza como una tarea rutinaria dentro de un equipo puede terminar en una reunión con tres equipos distintos. Clientes, el servicio responsable de las cuentas de usuario, despliega una limpieza de cuentas inactivas, cambia el tipo de su identificador o renombra una columna que hasta entonces consideraba interna. Sus pruebas pasan y el despliegue parece correcto. Poco después, Pedidos empieza a fallar.
Normalmente, la situación comienza de una forma mucho más sencilla. Pedidos necesita consultar determinada información de Clientes y, como ambos servicios comparten la misma base de datos, acceder directamente a sus tablas parece una solución rápida y práctica. Con el tiempo, esa consulta puede dejar de ser algo puntual y convertirse en parte del funcionamiento habitual de Pedidos. El servicio comienza entonces a asumir que las tablas, columnas y estructuras que consulta estarán disponibles y conservarán el mismo significado.
Lo que inicialmente parecía una integración sencilla puede terminar haciendo que decisiones internas de Clientes tengan consecuencias sobre Pedidos. Clientes puede ser responsable de la identidad, el estado y las reglas de ciclo de vida de una persona, mientras que Pedidos solo necesita conservar una referencia estable al cliente y obtener determinados datos para cumplir con sus propias responsabilidades. Sin embargo, cuando Pedidos resuelve esa necesidad consultando directamente las tablas de Clientes, termina dependiendo no solo de los datos que necesita, sino también de la forma en que Clientes los almacena y organiza.
Este escenario plantea una cuestión que va más allá de una consulta concreta o de una columna que haya cambiado. Si dos microservicios comparten la misma base de datos, ¿cómo pueden relacionarse sin convertir las estructuras internas de un dominio en dependencias del otro? Y, sobre todo, ¿cómo se puede establecer una frontera clara cuando la infraestructura sigue siendo compartida?
El problema real: acoplamiento al esquema ajeno
Supongamos que Clientes es responsable de la identidad, el estado y las reglas de ciclo de vida de una persona. Pedidos, en cambio, es responsable de los pedidos y necesita conservar una referencia estable al cliente asociado.
Pedidos necesita saber quién es el cliente, pero no necesita conocer cómo Clientes organiza internamente esa información. No debería depender de si el nombre se almacena en una columna, en dos columnas, en una tabla normalizada o mediante una relación con otra entidad. Tampoco debería decidir qué significa anonimizar una cuenta ni asumir que todas las columnas existentes en clientes son datos que puede interpretar.
Sin embargo, cuando ambos servicios comparten una base de datos, es fácil terminar con algo como esto:
CREATE TABLE pedidos (
id UUID PRIMARY KEY,
cliente_id UUID NOT NULL,
total NUMERIC(12, 2) NOT NULL,
creado_en TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT fk_pedidos_cliente
FOREIGN KEY (cliente_id) REFERENCES clientes(id)
);
Al estudiar pedidos, la FOREIGN KEY deja visible una relación con clientes. Esa relación es importante, pero conviene distinguir dos conceptos que suelen mezclarse.
Una clave foránea expresa una garantía de integridad referencial. Le dice al motor que un valor de pedidos.cliente_id debe corresponder a un registro existente en clientes.id, de acuerdo con las reglas de la restricción.
Eso no significa que clientes se haya convertido en una API para Pedidos.
Tampoco significa que Pedidos tenga derecho a consultar cualquier columna de la tabla, ejecutar JOIN arbitrarios o depender de la forma en que Clientes almacena sus datos. La clave foránea hace visible una relación entre estructuras; no define una interfaz de integración entre dominios.
El verdadero acoplamiento aparece cuando Pedidos empieza a hacer algo como esto:
SELECT
p.id,
p.total,
c.nombre,
c.estado,
c.tipo_documento
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id;
La consulta parece inocente. El problema es que convierte clientes en una interfaz accidental.
A partir de ese momento, Clientes no puede modificar libremente nombre, estado o tipo_documento porque Pedidos ha asumido que esas columnas forman parte de su contrato. Si mañana Clientes normaliza el nombre, cambia el modelo de estados o separa la información personal de la comercial, el cambio deja de ser exclusivamente suyo.
flowchart LR
Clientes[(Clientes)]
Pedidos[(Pedidos)]
Pedidos -->|JOIN y acceso a tablas privadas| Clientes
La frontera se ha roto porque el consumidor depende de la estructura interna del propietario para funcionar.
Por eso, al analizar una base de datos compartida, la pregunta importante no es únicamente «¿existe una FOREIGN KEY?». La pregunta relevante es «¿qué parte de esta estructura está siendo utilizada como interfaz por otro dominio?».
Relación local o contrato entre dominios
Antes de modificar el DDL conviene clasificar la relación.
Cuando la relación protege una invariante dentro del mismo dominio, la FOREIGN KEY sigue siendo una herramienta apropiada. Si dos tablas pertenecen al mismo modelo y la existencia de una fila depende de la existencia de otra, eliminar la restricción únicamente para conseguir una apariencia de autonomía significa renunciar a una garantía que el motor puede proporcionar de forma fiable.
La situación cambia cuando la relación cruza límites de propiedad.
Pedidos puede conservar cliente_id como referencia estable al cliente. Ese identificador expresa una relación entre conceptos, pero no concede a Pedidos permiso para interpretar el modelo interno de Clientes.
Esta distinción es importante porque eliminar una FOREIGN KEY no crea automáticamente autonomía. Solo elimina una garantía de integridad referencial.
La autonomía aparece cuando cada dominio puede modificar su estructura interna sin romper a sus consumidores.
De aquí surge una estrategia especialmente útil para escenarios de transición: Database as an API. La base de datos continúa siendo compartida desde el punto de vista físico, pero sus esquemas dejan de funcionar como una superficie de acceso indiscriminado. Cada dominio mantiene sus estructuras privadas y decide explícitamente qué información publica y qué operaciones permite.
La frontera lógica aparece antes que la frontera física.
Vistas como contratos de lectura
Para las lecturas entre dominios, una de las herramientas más sencillas disponibles en una base relacional es una vista.
Clientes puede mantener sus tablas privadas y publicar únicamente la información que Pedidos necesita:
CREATE VIEW clientes_publicos_para_pedidos AS
SELECT
id AS cliente_id,
nombre_comercial,
estado_comercial
FROM clientes;
Pedidos deja entonces de depender directamente de clientes:
SELECT
cliente_id,
nombre_comercial,
estado_comercial
FROM clientes_publicos_para_pedidos
WHERE cliente_id = :cliente_id;
La diferencia parece pequeña desde el punto de vista de SQL, pero es significativa desde el punto de vista arquitectónico.
La tabla clientes representa el modelo interno del dominio. La vista clientes_publicos_para_pedidos representa una superficie publicada.
La vista puede funcionar conceptualmente como un DTO o como un endpoint GET: expone un conjunto definido de datos y oculta cómo se obtiene internamente.
Clientes podría, por ejemplo, pasar de una tabla monolítica a varias tablas normalizadas:
clientes
├── identidades
├── perfiles
└── estados_comerciales
Mientras la vista conserve su contrato:
cliente_id
nombre_comercial
estado_comercial
Pedidos no necesita conocer ese cambio.
La vista, por supuesto, no hace que el contrato desaparezca. Al contrario: lo hace explícito. Cambiar el nombre de una columna publicada, eliminarla o alterar significativamente su semántica debe tratarse como un cambio contractual, aunque no exista HTTP de por medio.
Esta estrategia resulta particularmente útil cuando varios servicios ya utilizan el mismo motor de base de datos. Permite reducir el acoplamiento sin exigir inmediatamente una migración física completa.
También puede tener ventajas operativas. Una lectura que permanece dentro del mismo motor evita llamadas adicionales entre microservicios y puede evitar tráfico de red o costos de egress que aparecerían al sacar la consulta fuera de la infraestructura. Pero conviene no confundir esto con «latencia cero»: la consulta sigue consumiendo CPU, memoria, I/O y capacidad de concurrencia de la base de datos.
Además, una vista no crea independencia física. Los servicios siguen compartiendo disponibilidad, capacidad, credenciales, copias de seguridad, mantenimiento y, potencialmente, una misma condición de fallo.
La vista reduce el acoplamiento al esquema. No elimina el acoplamiento operativo a la infraestructura compartida.
Las lecturas y las escrituras necesitan fronteras diferentes
Aquí aparece una distinción fundamental. No todas las interacciones con una base de datos pueden modelarse de la misma manera.
Una lectura puede exponerse mediante una vista porque, conceptualmente, está proporcionando una representación de información. Una escritura, en cambio, puede modificar el estado del dominio y activar decisiones de negocio.
Por eso conviene distinguir entre una operación técnica y una operación que expresa una decisión de negocio.
Cuando la operación pertenece al negocio
Si una escritura necesita validar reglas, permisos, transiciones de estado, límites, invariantes o efectos secundarios, la autoridad debería permanecer en el servicio propietario.
En ese caso, REST o gRPC son canales adecuados para expresar la operación:
Pedidos
|
| POST /clientes/{id}/suspension
v
Clientes
|
+--> valida autorización
+--> comprueba estado actual
+--> aplica reglas de negocio
+--> registra auditoría
+--> persiste el nuevo estado
Pedidos no debería ejecutar directamente una función SQL equivalente a «suspender cliente» simplemente porque ambos servicios comparten una base de datos.
La suspensión no es solamente un UPDATE. Es una decisión.
Clientes debe determinar si la transición es válida, quién puede ejecutarla, qué auditoría requiere y qué otros efectos deben producirse. Si Pedidos pudiera modificar directamente la fila, el servicio propietario perdería el control de su propio dominio.
Cuando la operación es puramente técnica
Existe otro tipo de operación que sí puede tener sentido encapsular en SQL: una operación técnica, acotada, atómica y sin decisiones de negocio.
Por ejemplo, una función podría eliminar un registro temporal identificado explícitamente:
CREATE FUNCTION purgar_registro_temporal(p_registro_id UUID)
RETURNS VOID
LANGUAGE SQL
AS $$
DELETE FROM registros_temporales
WHERE id = p_registro_id;
$$;
La semántica es deliberadamente limitada. Si el registro existe, se elimina; si no existe, la operación no necesita tomar una decisión adicional.
La función no determina quién tiene derecho a suspender una cuenta, qué significa que una cuenta esté inactiva ni qué transición de negocio corresponde.
Este tipo de función puede ser útil como mecanismo de infraestructura, pero existe un riesgo importante: convertir gradualmente la base de datos en una segunda capa de aplicación.
Cuando cada nueva regla termina implementada como un stored procedure, la lógica queda repartida entre el backend y la base de datos. Las pruebas, el versionado, la observabilidad y el razonamiento sobre las reglas se vuelven progresivamente más difíciles.
La regla práctica es sencilla:
La base puede encapsular acceso y operaciones técnicas; el servicio debe conservar las decisiones del dominio.
Una matriz para decidir dónde vive cada operación
Esta separación puede resumirse en una regla operativa:
| Operación |
Tipo de lógica |
Canal |
Responsabilidad |
| Lectura de datos publicados |
Consulta |
VIEW |
El dominio propietario define el contrato |
| Escritura técnica acotada |
Infraestructura |
FUNCTION SQL |
La función ejecuta una operación atómica y limitada |
| Escritura con decisiones |
Negocio |
REST/gRPC |
El servicio propietario valida y persiste |
| Lectura entre dominios |
Consulta |
VIEW |
Se evita el JOIN sobre tablas privadas |
La matriz no pretende convertir la base de datos en un reemplazo universal de los servicios. Su propósito es asignar cada responsabilidad al canal que puede sostenerla sin volver a abrir el acceso indiscriminado al esquema interno.
El contrato también necesita gobierno
Una vista por sí sola no resuelve el problema. Si cualquier desarrollador puede modificarla sin considerar a sus consumidores, simplemente se habrá sustituido una dependencia implícita por otra.
Cada esquema y cada tabla privada deben tener un propietario claro. Las credenciales de Pedidos no deberían disponer de escritura sobre las tablas de Clientes y, cuando sea posible, tampoco deberían tener lectura directa sobre ellas.
El acceso público debe concederse sobre objetos concretos:
clientes
├── tablas privadas
├── funciones internas
└── vistas publicadas
└── acceso para Pedidos
El principio de mínimo privilegio ayuda a convertir la arquitectura deseada en una restricción técnica. Si Pedidos no tiene permiso para leer clientes, un nuevo JOIN directo deja de ser una tentación que depende exclusivamente de la disciplina del equipo.
Los contratos publicados también necesitan versionado.
Si la vista expone:
cliente_id
nombre_comercial
estado_comercial
y una nueva versión requiere eliminar estado_comercial, no debería tratarse como una modificación trivial. Es un cambio de contrato.
Una estrategia puede ser crear una nueva versión:
clientes_publicos_para_pedidos_v1
clientes_publicos_para_pedidos_v2
y mantener ambas durante un período de transición.
La nomenclatura concreta puede variar. Lo importante es que exista una forma de distinguir entre cambios compatibles y cambios incompatibles y que los consumidores puedan migrar deliberadamente.
El mismo principio aplica a las funciones SQL. Su firma, parámetros, comportamiento y permisos forman parte de una interfaz. No porque exista HTTP, sino porque otro componente depende de ella.
Medir antes de retirar
Una de las dificultades prácticas de estas migraciones es descubrir quién utiliza realmente una tabla.
En sistemas maduros, la documentación rara vez contiene todas las dependencias. Puede haber consultas en servicios antiguos, procesos batch, scripts operativos, herramientas de análisis, trabajos programados o accesos manuales que nadie recuerda.
Por eso la migración debería empezar con un inventario.
No basta con buscar referencias en el código fuente. También conviene revisar permisos, consultas observables en el motor, jobs programados, procesos de integración y consumidores conocidos.
El objetivo es construir un mapa aproximado:
┌── Pedidos
clientes ────────┼── Facturación
├── Reportes
└── Batch histórico
A partir de ahí, cada dependencia puede clasificarse.
Algunas serán invariantes legítimas. Otras serán lecturas que deberían convertirse en vistas. Otras serán escrituras que necesitan regresar al servicio propietario. Y algunas serán dependencias históricas que ya pueden eliminarse.
La observabilidad también permite medir el éxito de la transición: número de consumidores, frecuencia de consultas, latencia, errores, volumen de datos y tráfico entre dominios.
El objetivo no es solamente cambiar SQL. Es poder demostrar que la frontera está funcionando.
Una migración gradual sobre la misma infraestructura
Una de las ventajas de este enfoque es que no exige separar físicamente las bases desde el primer día.
La transición puede comenzar con la infraestructura actual.
Primero se construye un inventario de JOIN, consultas directas, procesos batch y permisos entre esquemas. Después se clasifican las relaciones para distinguir invariantes locales de dependencias entre dominios.
A continuación se declara quién es propietario de cada tabla, identificador y regla de ciclo de vida. Esta definición es importante porque una arquitectura no puede establecer fronteras si no está claro quién tiene autoridad sobre aquello que queda dentro de ellas.
Las lecturas necesarias para otros dominios se trasladan a vistas públicas. Las escrituras que expresan reglas de negocio se llevan a REST o gRPC. Las funciones SQL que permanezcan se mantienen deliberadamente pequeñas y técnicas.
Después se versionan los contratos y se empieza a medir su utilización.
Solo cuando los consumidores han dejado de depender de las tablas privadas tiene sentido retirar gradualmente esos permisos y revisar las FOREIGN KEY que atraviesan límites de propiedad.
Este orden importa.
Eliminar primero las restricciones o mover físicamente las bases no resuelve las dependencias semánticas. Es posible tener dos bases de datos completamente separadas y seguir manteniendo un acoplamiento fuerte si un servicio depende de la estructura interna del otro mediante consultas, replicaciones o procesos frágiles.
La frontera lógica debe preceder a la frontera física.
Preparar una futura separación física
Este modelo también puede funcionar como una etapa intermedia hacia una arquitectura con bases independientes.
Mientras ambos dominios comparten el mismo motor, Pedidos puede consumir:
VIEW clientes_publicos_para_pedidos
Más adelante, si Clientes pasa a tener su propia base de datos, esa misma semántica puede representarse mediante:
GET /clientes/{id}
o mediante un contrato equivalente en gRPC.
El cambio de infraestructura no necesita redefinir desde cero qué información necesita Pedidos. La interfaz conceptual ya existía.
Esto permite entender Database as an API no como una arquitectura final obligatoria, sino como una técnica de transición: primero se estabiliza el contrato y después, si es necesario, se separa la infraestructura que lo implementa.
La separación física deja de ser el mecanismo que crea la frontera y pasa a ser una consecuencia posible de una frontera que ya estaba definida.
Conclusión: la frontera que realmente hay que proteger
El desafío de trabajar con microservicios sobre una base de datos compartida no está en la existencia de una relación entre tablas, sino en determinar qué parte del modelo pertenece a cada dominio y qué información puede ser utilizada por los demás. Una relación entre pedidos y clientes puede ser necesaria desde el punto de vista de los datos, pero eso no significa que Pedidos deba conocer o depender de la estructura interna con la que Clientes gestiona sus propias entidades.
La autonomía comienza cuando cada dominio puede evolucionar su modelo interno sin obligar a los demás servicios a conocer esos cambios. Para conseguirlo, la base de datos compartida necesita límites explícitos. Las vistas permiten publicar únicamente los datos que un consumidor necesita; las funciones SQL pueden encapsular operaciones técnicas acotadas; y las decisiones que contienen reglas de negocio deben permanecer bajo la responsabilidad del servicio propietario, mediante REST, gRPC u otro mecanismo de integración apropiado.
Esto convierte la base de datos en algo más que un repositorio común. Puede actuar como una infraestructura compartida que ofrece contratos de acceso definidos y gobernados, en lugar de convertirse en un espacio donde cualquier servicio puede consultar y modificar libremente las estructuras de los demás.
Para que este modelo sea sostenible, los contratos necesitan las mismas garantías que cualquier otra interfaz entre componentes: propietarios claros, permisos restringidos, versionado, compatibilidad entre cambios, observabilidad y un proceso controlado para retirar consumidores. De esta manera, una vista o una función no son simplemente objetos de base de datos, sino parte de una superficie que un dominio decide publicar y mantener.
Este enfoque también permite avanzar de forma gradual. No es necesario separar físicamente las bases de datos para comenzar a establecer límites entre los servicios. Primero pueden definirse los contratos y eliminarse las dependencias directas sobre las tablas privadas. Más adelante, si las necesidades operativas lo requieren, esos mismos contratos pueden trasladarse a una API o a otra forma de comunicación entre servicios.
La separación física, por tanto, no tiene que ser el punto de partida para conseguir autonomía. Puede ser una evolución posterior de una frontera que ya existe a nivel lógico.
La idea central es sencilla:
Compartir una base de datos no obliga a compartir el modelo interno de cada dominio.
La cuestión importante no es cuándo eliminar una relación entre tablas ni cuándo separar físicamente las bases de datos. La cuestión es qué información y qué operaciones está dispuesto a publicar cada dominio, bajo qué condiciones y con qué garantías de estabilidad.
Cuando esa frontera está claramente definida, la base de datos compartida deja de ser una fuente de dependencias accidentales y puede convertirse en una etapa controlada hacia una arquitectura con mayor independencia. El siguiente paso natural consiste en estudiar cómo versionar estos contratos, detectar automáticamente a sus consumidores y establecer mecanismos que permitan evolucionar desde una base unificada hacia servicios con almacenamiento independiente cuando la arquitectura y las necesidades operativas lo justifiquen.