En el contexto de los módulos de servicios que interactúan, surge la pregunta inevitable: ¿según qué reglas se lleva a cabo la comunicación? En los productos de TI, un “contrato” representa un entendimiento formal de qué datos fluyen entre sistemas y cómo se transmiten. Esto implica el formato de los datos (JSON, Protobuf, etc.), los elementos estructurales (campos, tipos de datos), el protocolo de comunicación (REST, gRPC, colas de mensajes) y otras especificaciones.
Un contrato garantiza apertura (todos saben qué se recibe y se envía), previsibilidad (podemos actualizar el contrato y mantener versiones) y confiabilidad (nuestro sistema no fallará si realizamos cambios bien gestionados).
En la práctica, aunque todo el mundo habla de microservicios, “contratos” y API, a menudo vemos que la gente adopta el enfoque: “¿Por qué no crear una tabla compartida en la base de datos en lugar de crear API?”
Por lo tanto, si bien el uso de una tabla compartida para el intercambio de datos puede parecer eficiente y optimizado para obtener resultados rápidos, genera diversos desafíos técnicos y organizativos a largo plazo. Sin embargo, cuando los equipos eligen tablas compartidas para el intercambio de datos, pueden enfrentar numerosos problemas durante la implementación.
Cuando los servicios se comunican a través de REST/gRPC/GraphQL, tienen una definición formal: OpenAPI (Swagger), esquemas protobuf o esquemas GraphQL. Estos definen en detalle qué recursos (puntos finales) están disponibles, qué campos se esperan, sus tipos y los formatos de solicitud/respuesta. Cuando "una tabla compartida" actúa como un contrato, no hay una descripción formal: No hay una descripción formal del contrato; solo está disponible el esquema de la tabla (DDL) e incluso eso no está bien documentado. Cualquier modificación menor de la estructura de la tabla (por ejemplo, agregar o eliminar una columna, cambiar los tipos de datos) puede afectar a otros equipos que leen o escriben en esta tabla.
El control de versiones de API es una práctica habitual: podemos tener v1, v2, etc., y podemos mantener la compatibilidad con versiones anteriores y luego, gradualmente, trasladar los clientes a las versiones más nuevas. Para las tablas de bases de datos, solo tenemos operaciones DDL (por ejemplo, ALTER TABLE
), que están estrechamente acopladas a un motor de base de datos específico y requieren un manejo cuidadoso de las migraciones.
No existe un sistema centralizado que pueda enviar alertas a los consumidores sobre cambios de esquema que requieran que actualicen sus consultas. Como resultado, pueden ocurrir acuerdos “bajo la mesa” : alguien puede publicar en un chat: “Mañana, cambiaremos la columna X a Y”, pero no hay garantía de que todos estén listos a tiempo.
Cuando hay una API claramente definida, es evidente quién es su propietario: el servicio que actúa como editor de la API. Cuando varios equipos utilizan la misma tabla de base de datos, existe confusión sobre quién determina la estructura y qué campos almacenar y cómo interpretarlos. Como resultado, la tabla puede convertirse en “propiedad de nadie” y cada cambio se convierte en una búsqueda: “¡Tenemos que consultar con ese otro equipo en caso de que estén utilizando la columna anterior!”.
Es difícil hacer un seguimiento de quién puede leer y escribir en una tabla si muchos equipos tienen acceso a la base de datos. Existe la posibilidad de que servicios no autorizados puedan acceder a los datos aunque no estén destinados a ellos. Es más fácil gestionar estos problemas con una API: puedes controlar los derechos de acceso (quién puede llamar a qué métodos), usar la autenticación y la autorización, y supervisar quién llamó a qué. Con una tabla, es mucho más complicado.
Cualquier modificación interna de los datos (reorganización de índices, particionamiento de la tabla, cambio de la base de datos) se convierte en un problema global. Si la tabla funciona como una interfaz pública, el propietario no puede realizar cambios internos sin poner en peligro a todos los lectores y escritores externos.
Éste es el aspecto más doloroso: ¿cómo se informa a otro equipo que el esquema cambiará al día siguiente?
Cuando varios equipos utilizan una tabla compartida para seleccionar y actualizar datos críticos, puede convertirse fácilmente en un “campo de batalla”. El resultado es que la lógica empresarial termina dispersa en diferentes servicios y no hay un control centralizado de la integridad de los datos. Se vuelve muy difícil saber por qué un campo en particular se almacena de una manera particular, quién puede actualizarlo y qué sucede si se deja en blanco.
Por ejemplo, supongamos que la tabla se rompe: digamos que hay datos incorrectos o alguien ha bloqueado algunas filas cruciales. Identificar la fuente del problema a menudo puede requerir preguntar a cada equipo con acceso a la base de datos que determine qué consulta causó el problema. A menudo no es obvio: esto significa que la consulta de un equipo puede haber bloqueado la base de datos, mientras que la consulta de otro equipo produce el error observable.
Una base de datos compartida es un único punto de falla. Si falla, muchos servicios también lo harán. Cuando la base de datos tiene problemas de rendimiento debido a las consultas pesadas de un servicio, todos los servicios experimentan problemas. En un modelo con API y propiedad de datos bien definidas, cada equipo es dueño de la disponibilidad y el rendimiento de su servicio, por lo que una falla en un componente no se propaga a los demás.
Un compromiso común es: “Le daremos una réplica de solo lectura para que pueda realizar consultas sin afectar nuestra base de datos principal”. Al principio, eso podría solucionar algunos problemas de carga, pero:
Las prácticas de diseño modernas (por ejemplo, “API First” o “Contract First”) comienzan con una definición formal de la interfaz. Se utilizan esquemas OpenAPI/Swagger, protobuf o GraphQL. De esta manera, tanto las personas como las máquinas saben qué puntos finales están disponibles, qué campos son necesarios y qué tipos de datos se utilizan.
En una arquitectura de microservicios (o incluso modular), se supone que cada servicio es dueño de sus datos en su totalidad. Define la estructura, el almacenamiento y la lógica empresarial y proporciona una API para todo acceso externo a esa API. Nadie puede tocar la base de datos de "alguien más": solo los puntos finales o eventos oficiales. Esto facilita la vida cuando hay cambios en cuestión y siempre está claro quién es el culpable.
GET /items
, POST /items
, etc., y los clientes realizan solicitudes con un esquema de datos bien definido (DTO).
Independientemente del modelo, es posible y esencial implementar el control de versiones en la interfaz. Por ejemplo:
Un principio fundamental es que el equipo que posee los datos puede decidir cómo almacenarlos y administrarlos, pero no debe otorgar acceso de escritura directo a otros servicios. Los demás deben hacerlo a través de la API en lugar de editar datos externos. Esto genera una distribución de responsabilidades más clara: si el servicio A falla, entonces es responsabilidad del servicio A repararlo y no de sus vecinos.
A primera vista, si todo está en un solo equipo, ¿por qué complicar las cosas con una API? En realidad, incluso si tienes un solo producto dividido en módulos, una tabla compartida puede generar los mismos problemas.
Por ejemplo, el servicio Pedidos es el propietario de la tabla de pedidos y el servicio de Facturación no accede directamente a esa tabla: realiza llamadas a los puntos finales del servicio Pedidos para obtener detalles del pedido o para marcar un pedido como pagado.
En un nivel superior, cuando dos o más equipos son responsables de distintas áreas, los principios siguen siendo los mismos. Por ejemplo:
Si el Equipo B consulta directamente la tabla “Catálogo” que pertenece al Equipo A, cualquier cambio de esquema interno en A (por ejemplo, agregar campos, alterar la estructura) puede afectar al Equipo B.
El enfoque adecuado es utilizar una API: el equipo A proporciona puntos finales como GET /catalog/items
, GET /catalog/items/{id}
, etc., y el equipo B utiliza esos métodos. Si A puede admitir versiones anteriores y nuevas, puede lanzar /v2, lo que le da tiempo a B para migrar.
Con un contrato formal, todos los cambios son visibles: en Swagger/OpenAPI, en archivos .proto o en la documentación de eventos. Cualquier actualización se puede discutir de antemano, probar adecuadamente y programar, con estrategias de compatibilidad con versiones anteriores según sea necesario.
Los cambios en un servicio tienen menos impacto en los demás. El equipo no tiene que preocuparse por “romper” a alguien más si gestiona adecuadamente los campos o puntos finales nuevos y antiguos, lo que garantiza una transición sin problemas.
Las puertas de enlace de API, la autenticación y la autorización (JWT, OAuth) son estándares para los servicios, pero son casi imposibles con una tabla compartida. Es más fácil ajustar el acceso (quién puede llamar a qué métodos), mantener registros, realizar un seguimiento de las estadísticas de uso e imponer cuotas. Esto hace que el sistema sea más seguro y predecible.
Una tabla compartida en la base de datos es un detalle de implementación más que un acuerdo entre servicios, por lo que no se considera un contrato. Los numerosos problemas (versiones complejas, cambios caóticos, propiedad poco clara, seguridad y riesgos de rendimiento) hacen que este enfoque sea insostenible a largo plazo.
El enfoque correcto es el de "contrato primero" , que implica definir la interacción a través de un diseño formal y seguir el principio de que cada servicio sigue siendo el propietario de sus datos. Esto no solo ayuda a reducir la deuda técnica, sino que también aumenta la transparencia, acelera el desarrollo de productos y permite realizar cambios seguros sin tener que luchar contra los esquemas de bases de datos.
Se trata tanto de una cuestión técnica (cómo diseñar e integrar) como de una cuestión organizativa (cómo se comunican los equipos y gestionan los cambios). Si desea que su producto crezca sin tener que lidiar con interminables emergencias relacionadas con los esquemas de bases de datos, entonces debería empezar a pensar en términos de contratos en lugar de en el acceso directo a las bases de datos.