Ir al contenido
14 ago 2026·8 min de lectura

Monolito o microservicios vistos desde la base de datos

Trata monolito o microservicios como una decisión de propiedad de datos: mapea transacciones, descubre tablas compartidas y separa límites reales.

Monolito o microservicios vistos desde la base de datos

Un límite de servicio solo es creíble cuando los datos de cada lado pueden cambiar sin un commit coordinado en la base. Si dos partes del código deben bloquear las mismas filas, actualizar las mismas tablas o acordar la misma ventana de despliegue, trazar una línea HTTP cambia el transporte, no la arquitectura.

Por eso la comparación útil entre monolito y microservicios empieza por el esquema. El código puede moverse detrás de un endpoint en una tarde. La propiedad, las invariantes, los datos históricos, las consultas de informes, los reintentos y la recuperación siguen unidos a las tablas. Los equipos que empiezan por clases y paquetes suelen descubrirlo después de crear un monolito distribuido: más llamadas de red, más trabajo operativo y el mismo acoplamiento de base de datos debajo.

No me opongo a los microservicios. Me opongo a fingir que el límite de un proceso crea un límite de datos. El esquema indica dónde el sistema ya funciona como una unidad, dónde comparte almacenamiento por comodidad y dónde una separación podría sobrevivir a un mal despliegue a las dos de la mañana.

Un mapa de transacciones vale más que un grafo de dependencias

Asocia cada operación de negocio con las filas que lee y escribe antes de elegir límites de servicio. Un grafo de dependencias muestra qué módulo llama a otro. No muestra que registrar una factura, reservar existencias y escribir una entrada de auditoría deban completarse o fallar juntos. La base conoce ese hecho porque las escrituras comparten una transacción.

Empieza por las operaciones, no por las tablas. Para cada comando que cambia el estado, registra el actor inicial, las tablas leídas y escritas, los bloqueos, las restricciones usadas y el coste de una ejecución parcial. Incluye trabajos, triggers, procedimientos almacenados, importaciones y scripts de operadores. Ahí suele desaparecer el supuesto límite.

Un mapa compacto puede registrar Realizar pedido como una lectura de cliente, producto y existencias que escribe pedido, línea y stock. Su invariante impide que el stock sea negativo; un fallo parcial deja un pedido aceptado que no puede cumplirse. Cobrar pago lee el pedido y los intentos anteriores y escribe el pago, el asiento contable y el estado bajo la regla de un único cobro correcto. Cancelar pedido toca envíos, pagos, stock, reembolsos y pedidos porque un artículo enviado necesita una vía de devolución.

El mapa descubre dos acoplamientos. El transaccional significa que las escrituras deben confirmarse juntas para conservar una invariante. El de lectura significa que una operación consulta datos cuyo dueño está en otro lugar. El primero puede impedir la separación. El segundo suele necesitar una réplica, una proyección alimentada por eventos o una petición explícita, pero no exige propiedad compartida. Los equipos los confunden y mantienen todo unido para siempre o distribuyen una transacción que nunca lo necesitó.

Rastrea las consultas de producción además del código. El SQL dinámico, las sentencias del ORM, los trabajos nocturnos y los procedimientos pueden escapar del análisis estático. Una tabla sin referencias visibles puede alimentar el cierre mensual. Si borrar un módulo falsea el informe de contabilidad tres días después, no está aislado.

Las tablas compartidas aplazan el coste de coordinación

Una tabla compartida permite entregar rápido al convertir la base en una API privada de integración. Facturación inserta una fila, informes la lee directamente y cumplimiento añade una columna de estado. Nada parece caro hasta que un equipo cambia una columna, reinterpreta un valor, rellena filas antiguas o restaura una copia. Entonces cada lector forma parte del cambio.

El coste no consiste en que dos procesos puedan consultar una tabla. Consiste en la autoridad ambigua. ¿Qué servicio puede añadir una restricción? ¿Quién decide si status = 4 significa empaquetado o enviado? ¿Qué despliegue controla el backfill? ¿Quién restaura si un servicio necesita volver a un instante y otro ya escribió estado más reciente? El almacenamiento compartido convierte el trabajo normal de esquema en coordinación entre equipos.

Las claves foráneas exigen una distinción precisa. Dentro de un límite de propiedad son documentación ejecutable útil. A través de futuros servicios indican que la base aún impone una invariante compartida. Quitarlas no elimina la regla; traslada la detección al código y permite referencias inválidas. Mantén los sistemas juntos hasta poder decir qué reemplaza esa garantía.

Esta consulta de propiedad sirve como primera pasada en PostgreSQL:

SELECT table_schema, table_name,
       array_agg(DISTINCT application_name ORDER BY application_name) AS writers
FROM audit_statement_usage
WHERE command IN ('INSERT', 'UPDATE', 'DELETE')
GROUP BY table_schema, table_name
HAVING count(DISTINCT application_name) > 1
ORDER BY table_schema, table_name;

PostgreSQL no incluye audit_statement_usage. Créala desde registros de auditoría o telemetría con al menos application_name, comando, esquema y tabla. Importa la forma del resultado: cada fila con varios escritores necesita una decisión de propiedad, no un endpoint. No deduzcas propiedad solo de roles si varias aplicaciones comparten credenciales, porque esa costumbre inutiliza la evidencia.

El objetivo sano tiene un escritor autoritativo por tabla. Otros componentes reciben copias diseñadas para sus consultas. Una copia tiene un contrato de frescura y puede reconstruirse. Una tabla compartida tiene varias partes que dependen en silencio de su forma actual, un contrato mucho más difícil de ver.

El requisito de coherencia elige el límite

Mantén los datos en una transacción cuando el negocio no pueda tolerar un estado intermedio observable. Sepáralos cuando acepte un acuerdo demorado y puedas definir la reparación. Es una decisión de producto expresada en la base, no una preferencia por código síncrono o asíncrono.

Pagos y contabilidad lo muestran bien. Si el sistema registra un cobro sin su asiento, aunque sea brevemente, otro trabajo puede devolverlo, liquidarlo o informarlo mal. Puedes separar las escrituras, pero necesitarás un protocolo explícito para intención atómica, reintentos, deduplicación y conciliación. La red convierte una invariante local en un flujo distribuido. Puede justificarse, pero no es desacoplamiento gratis.

La recomendación popular de poner un broker entre todo es errónea si llega antes que la decisión de coherencia. Un broker transporta mensajes bajo condiciones declaradas. No decide cuánto puede retrasarse una reserva, qué hacer con un duplicado ni quién repara una proyección ausente. Eso es semántica de aplicación. Ocultarla detrás de publish() deja menos evidencia.

Haz cuatro preguntas por límite:

  1. ¿Qué estado pueden observar usuarios o trabajos entre los commits?
  2. ¿Cuánto puede permanecer incoherente?
  3. ¿Qué lado reintenta y cómo reconoce el receptor un duplicado?
  4. ¿Qué proceso detecta y repara un mensaje que no produjo el estado previsto?

Si la segunda respuesta es prácticamente cero, prefiere una transacción salvo que el aislamiento normativo, la escala o la propiedad organizativa justifiquen la coordinación distribuida. Si son segundos o minutos, escribe el límite y vigílalo. La palabra eventual no es un objetivo de servicio.

Una base no implica un único dueño

Puedes imponer propiedad real dentro de una base separando esquemas, roles, migraciones y permisos de escritura. A la inversa, servidores separados siguen acoplados si los servicios coordinan cada versión y hacen llamadas síncronas para reconstruir joins antiguos. La separación física solo prueba un límite cuando trae independencia operativa.

Una transición útil da a cada componente un esquema y una cuenta que solo escribe allí. Las lecturas entre esquemas son excepciones temporales inventariadas. Los permisos convierten escrituras accidentales en fallos visibles:

REVOKE ALL ON SCHEMA billing FROM fulfillment_app;
GRANT USAGE ON SCHEMA billing TO fulfillment_app;
GRANT SELECT ON billing.invoice_summary TO fulfillment_app;
REVOKE INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA billing FROM fulfillment_app;

Concede acceso a una vista dedicada como invoice_summary, no a todas las tablas. La vista ofrece una pequeña superficie de compatibilidad mientras los consumidores migran a una API o proyección local. Registra cada permiso con dueño y condición de retirada. De lo contrario, el puente temporal será la próxima base compartida.

Database-per-service también se entiende mal. Significa que el servicio controla su contrato de persistencia y otros no pueden rodearlo. No exige un clúster por proceso pequeño. Bases lógicas separadas ayudan con restauración y permisos, mientras que esquemas en una instancia PostgreSQL pueden bastar durante la extracción. Elige el aislamiento que impone propiedad sin multiplicar operaciones antes de probar el límite.

Todo join entre límites necesita un sustituto definido antes de extraer. Un join local filtra, ordena y pagina sobre una instantánea coherente. Varias llamadas API pueden cargar miles de registros, crear un patrón N-más-uno y combinar datos observados en momentos distintos. El endpoint puede devolver el JSON correcto en una prueba y fallar con la cardinalidad real.

Elige según la consulta. Una pantalla que necesita un estado actual puede hacer una petición directa con timeout y alternativa definidos. Una búsqueda que filtra pedidos por atributos de cliente suele necesitar una proyección local. Un informe sin conexión pertenece a un almacén analítico. Copiar hechos seleccionados es duplicación deliberada; acumular llamadas hasta formar por accidente un join distribuido es acoplamiento oculto.

La paginación muestra un fallo común. Si se piden los primeros 50 pedidos ordenados por riesgo, pero pedido y riesgo tienen dueños distintos, cargar 50 y consultar luego el riesgo no produce los 50 correctos. Cargar más es una conjetura de rendimiento inestable. Mueve el ranking a un modelo de lectura o cambia el contrato. El fan-out de red no conserva la semántica por optimismo.

Define la frescura según la decisión. Un bloqueo antifraude puede exigir estado actual y cerrar si su dueño no responde. Un panel comercial puede aceptar minutos de retraso. Guarda la versión observada o la hora de actualización para que los llamadores apliquen la regla. Sin procedencia, la caché parece actual aunque el feed lleve horas parado.

No dividas una base solo porque una tabla sea grande. Particionado, índices, archivo y aislamiento de carga resuelven mejor los problemas de almacenamiento y consulta. Un límite compensa su coste cuando separa autoridad de cambio o comportamiento ante fallos. El tamaño dice poco sobre ambos.

Los eventos necesitan una fuente atómica de verdad

Encuentra cada escritor oculto
La plataforma lee juntos todos los lenguajes, incluso árboles mixtos de más de un millón de líneas.

Usa una outbox transaccional cuando un cambio confirmado deba producir un evento de forma fiable. Escribir datos y publicar después deja un hueco: el commit puede funcionar y la publicación fallar. Publicar primero crea el hueco inverso. Una transacción distribuida lo cierra, pero aumenta el acoplamiento operativo y rara vez tiene soporte limpio entre todos los sistemas.

La outbox guarda el cambio y el evento en el mismo commit local:

BEGIN;
UPDATE orders
SET status = 'confirmed', version = version + 1
WHERE id = :order_id AND status = 'pending';

INSERT INTO outbox_event (event_id, aggregate_id, event_type, payload, created_at)
VALUES (:event_id, :order_id, 'order.confirmed', :payload, CURRENT_TIMESTAMP);
COMMIT;

Un relay publica filas pendientes y marca el avance. Puede publicar dos veces si cae tras enviar y antes de registrar el éxito, así que los consumidores necesitan idempotencia. Guarda un ID estable y condiciona el cambio a que no se haya procesado. Las promesas exactly-once suelen ser entrega at-least-once con efectos deduplicados. Di qué ofreces.

El orden también importa. Una secuencia global limita caudal y crea acoplamiento falso; sin regla, una cancelación puede adelantar a la confirmación. Ordena por agregado cuando el negocio lo pida, incluye su versión y rechaza o aparca huecos. La reparación debe ser tan rutinaria que un operador pueda ejecutarla sin inventar estado.

La captura de cambios puede alimentar la outbox, pero capturar filas no sustituye eventos de dominio. Una actualización dice que cambió el almacenamiento, no si un pedido fue confirmado, corregido, importado o reparado. Los consumidores que deducen intención desde columnas quedan unidos al esquema que querías liberar.

Los informes revelan la propiedad que ocultan los comandos

Los límites operativos rara vez coinciden con las preguntas analíticas, así que los informes deben combinar copias propias en lugar de recuperar acceso a cada tabla. Un informe de rentabilidad puede requerir pedidos, devoluciones, soporte y contabilidad. Hacer que llame síncronamente a cuatro servicios recrea un join distribuido con peor latencia y más fallos.

Crea un almacén de informes desde eventos, cambios o extracciones programadas, con reglas explícitas de frescura y conciliación. Puede desnormalizar porque no posee la verdad operativa. Cuando cambie una definición, reconstruye desde hechos retenidos o una instantánea controlada en vez de obligar a las fuentes a conservar cada consulta histórica.

Cuidado con la tabla customer. Identidad, pagador, destinatario, cuenta y contraparte legal suelen empezar en una fila y divergir. Un servicio universal puede depender de casi cada petición. Define qué hechos posee cada dominio y qué identificadores los conectan. Duplicar el nombre visible suele ser más barato que exigir disponibilidad síncrona de un perfil central.

La conciliación hace responsable la coherencia eventual. Compara recuentos y totales por claves estables, sigue el evento pendiente más antiguo y conserva un procedimiento de replay. Un panel verde del broker no prueba que los informes coincidan con el libro mayor. Compara resultados de negocio, no actividad de transporte.

En entornos regulados, las copias afectan retención, acceso y borrado. Una proyección sigue siendo datos. Inventaría adónde fluyen campos sensibles, limita lo que recibe cada consumidor y prueba la eliminación o retención en réplicas. Separar almacenamiento no elimina la gobernanza.

Una extracción segura mueve la propiedad antes que el tráfico

Rompe el tren de versiones
CodeHero convierte monolitos web y de escritorio en servicios Go y clientes TypeScript propios.

Mueve una capacidad estableciendo su contrato de datos, observando el comportamiento en sombra y transfiriendo autoridad de escritura antes de enrutar todo el tráfico. Extraer primero el código deja el servicio dependiendo de tablas antiguas, y la parte peligrosa llega tarde y bajo presión.

Una secuencia fiable es:

  1. Elige una capacidad con un dueño plausible y un límite de coherencia tolerable. Inventaría cada lector y escritor.
  2. Cubre el comportamiento actual con pruebas de caracterización, incluidos fallos, reintentos, redondeo, nulos y cambios de operadores. Captura peticiones representativas solo bajo las normas de privacidad.
  3. Introduce el esquema futuro y rellénalo con trabajos repetibles y checkpoints. Haz doble lectura en sombra y compara sin servir resultados.
  4. Pasa a una escritura autoritativa. Si el doble escrito temporal es inevitable, registra ambos resultados y concílialos; no supongas que se mantienen iguales.
  5. Enruta lecturas y tráfico al nuevo dueño, conserva un rollback probado y retira permisos antiguos solo cuando retraso y paridad permanezcan dentro de los límites.

Lo incómodo es combinar backfill y cambios en vivo. Una instantánea empieza mientras producción continúa. Usa una instantánea coherente y una posición de cambio, luego aplica cambios posteriores en orden. Haz idempotente el relleno. Registra filas rechazadas con contexto; una migración que omite datos antiguos defectuosos crea un esquema limpio y un registro de negocio falso.

Evita sincronización bidireccional. Parece un rollback seguro, pero sus conflictos se convierten en otra aplicación. Nombra una autoridad para cada campo en cada fase. El rollback debe revertir rutas y reproducir un registro conocido, no permitir que ambos sistemas editen el mismo hecho.

Una fachada strangler ayuda a enrutar, pero no resuelve la propiedad. Si envía peticiones a un servicio que aún actualiza tablas del monolito, solo cambió la topología. Mide progreso por escritores retirados, permisos cruzados eliminados y migraciones coordinadas suprimidas.

Las pruebas de paridad deben comparar efectos de negocio

Prueba una reescritura por el comportamiento observable y el estado resultante, no por códigos HTTP parecidos. Los sistemas antiguos guardan reglas en triggers, trabajos, valores predeterminados, truncado, intercalación, zonas horarias y reparaciones manuales. Una reescritura limpia puede parecer lógica y cambiar facturas o existencias.

Construye un arnés que envíe la misma petición grabada o sintética a ambos caminos, normalice diferencias permitidas y compare respuestas y efectos en la base. Para comandos, compara filas, dinero, eventos y errores. Para consultas, orden, paginación, nulos y filtros de permisos. Enmascara datos sensibles antes y conserva el resultado como evidencia.

Define tolerancias por campo. Los tiempos pueden diferir dentro de una ventana. Los ID pueden cambiar manteniendo relaciones. El dinero decimal suele exigir coincidencia exacta tras la regla de redondeo. Un diff JSON global genera ruido hasta que los ingenieros lo ignoran.

Ejecuta fallos. Mata el relay después de publicar, reintenta un timeout, entrega eventos desordenados, restaura una instantánea y despliega un consumidor antiguo con un evento nuevo. El límite es creíble cuando los fallos tienen efectos acotados y reparación documentada, no cuando el diagrama se ve limpio.

Aquí importa el enfoque de CodeHero: lee todo el código, moderniza la arquitectura y comprueba comportamiento con un arnés de paridad frente a tráfico grabado. La entrega en menos de 30 días sería temeraria si los efectos de base no fueran parte del comportamiento y solo se tradujeran archivos.

Los cambios compatibles permiten despliegues independientes

Haz creíble la separación
CodeHero moderniza arquitectura y compara los efectos de negocio contra tráfico de producción.

Un límite no se despliega por separado si cada cambio obliga a productores y consumidores a cambiar juntos. Diseña bases y eventos para que versiones nuevas y antiguas coincidan durante un periodo. Ese solapamiento permite desplegar, observar y retroceder sin convocar a todos.

Usa expand and contract. Añade el campo nuevo sin retirar el antiguo. Haz que los escritores llenen la nueva forma, rellena historia y deja que los lectores prefieran lo nuevo aceptando lo viejo. Verifica que no quedan lectores, deja de producir el campo antiguo y bórralo después. Cada fase necesita una salida medible, como cero lecturas durante un ciclo operativo, no una fecha supuesta.

Renombrar una columna directamente parece limpio, pero rompe binarios antiguos, trabajos olvidados y rollbacks. Añade el nombre nuevo, copia bajo una autoridad y contrae luego. La columna extra es complejidad temporal con plan de retirada; una caída coordinada es complejidad sin plan.

Los esquemas de eventos necesitan la misma disciplina. Los consumidores deben ignorar campos desconocidos, los productores no cambiar significados y los campos obligatorios tener una introducción válida. Versiona el contrato semántico, no cada adición inocua. Publicar order.cancelled.v2 por cada atributo opcional llena el sistema de conversiones sin proteger del cambio peligroso: redefinir cancelled.

Las vistas ofrecen una ventana corta para lecturas renombradas. Son malas API permanentes si dependen de joins o planes indocumentados. Pon una condición de caducidad y observa quién consulta. Quitar una vista por una búsqueda del repositorio omite informes ad hoc y binarios externos.

El rollback prueba el plan. Si la aplicación nueva escribe un valor que la antigua no puede leer, volver al binario no restaura el servicio. Prueba código antiguo contra estado nuevo, incluidos enums, nulos, cadenas más largas y variantes de eventos. El DDL compatible es solo la mitad; los datos deben seguir legibles.

El despliegue independiente exige evidencia. Muestra que cada versión funciona con cada fase, que el backfill continúa y que la telemetría identifica consumidores. Si falla esa matriz, ambos servicios comparten tren de versiones aunque repositorios y bases tengan nombres distintos.

Algunos monolitos deben seguir siendo monolitos

Conserva el monolito cuando un equipo lo cambia coherentemente, sus transacciones coinciden con las invariantes, el riesgo está controlado y la escala no requiere ubicación independiente. Un monolito modular con propiedad impuesta ofrece casi toda la claridad buscada sin fallos de red ni una flota operativa.

La razón más fuerte para dividir es una autoridad de cambio independiente respaldada por un límite real de datos. También sirven aislar una carga propensa a fallos, un perímetro de seguridad o cómputo que escala distinto. Ninguna excusa un modelo de coherencia indefinido, pero pueden justificar el coste.

No uses fórmulas de tamaño de equipo. Dos equipos pueden colaborar en un repositorio y uno crear una colección terrible de servicios pequeños. La organización influye, pero la base sigue imponiendo los hechos. Si cada versión exige migraciones coordinadas, no hay entrega independiente.

Antes de aprobar, exige un registro que nombre tablas propias, escritor autoritativo, lecturas cruzadas, ventana de coherencia, contrato de eventos, unidad de restauración, secuencia y autoridad de rollback. Rechaza respuestas como ambos servicios o eventually sin límite y reparación. Ese estándar evita más daño que discutir el número ideal de servicios.

Trata restauración y recuperación como pruebas de límite. Si un servicio no puede restaurarse sin rebobinar otro, comparten unidad operativa. Si el replay exige cambios indocumentados en tablas ajenas, la propiedad está incompleta. Practica con eventos en cola y proyecciones. Confirma que los consumidores rechacen repeticiones antiguas y reconstruyan copias sin escribir en el esquema restaurado; registra quién decide el punto y cómo vuelven las escrituras posteriores.

El primer cambio útil suele ser menor que extraer: elimina credenciales compartidas, registra escritores por aplicación, asigna dueño a cada tabla y muestra accesos cruzados. Cuando el esquema enseñe límites honestos, el código puede seguirlo. Donde el esquema se niegue a separarse, escúchalo.

Preguntas frecuentes

¿Cada microservicio debe tener su propia base de datos?

Cada microservicio debe poseer su persistencia e impedir que otros escriban alrededor de su contrato. Puede significar bases separadas, pero esquemas y roles en una base pueden aislar suficiente durante una migración.

¿Una base compartida siempre es mala para microservicios?

Un servidor compartido no es malo por sí mismo; la autoridad de escritura compartida sí. Si cada servicio posee su esquema y usa contratos de lectura explícitos, un servidor puede ser una solución transitoria o permanente.

¿Cómo encuentro límites de transacción en un monolito?

Sigue cada comando hasta todas las filas leídas y escritas, incluidos triggers, trabajos, procedimientos y scripts. Agrupa los cambios que deben confirmarse juntos para conservar una invariante de negocio.

¿Puede una API eliminar el acoplamiento de base de datos?

Una API oculta el esquema, pero no elimina acoplamiento si los llamadores aún necesitan commits, versiones o recuperación coordinados. El límite mejora cuando el proveedor posee los datos y los llamadores aceptan su contrato.

¿Cuándo debo usar coherencia eventual?

Cuando el negocio acepta un periodo nombrado de desacuerdo y el equipo tiene reintentos, deduplicación, vigilancia y reparación. Si el retraso aceptable es cero, una transacción local suele ser más clara.

¿Cuál es la forma más segura de dividir una tabla compartida?

Elige un escritor autoritativo, crea un esquema propio, rellena desde una posición coherente y aplica los cambios posteriores. Las lecturas en sombra y la conciliación deben probar paridad antes del cambio.

¿Un broker resuelve las transacciones distribuidas?

No. Transporta mensajes, pero la aplicación sigue definiendo estados parciales, duplicados, orden y reparación. Una outbox cierra el hueco entre commit y evento; los consumidores aún necesitan idempotencia.

¿Cómo deben funcionar los informes tras dividir un monolito?

Alimenta un almacén separado con eventos, cambios o extracciones controladas. Dale reglas de frescura y conciliación en lugar de consultar cada base operativa o unir servicios de forma síncrona.

¿Qué debe incluir el rollback de un microservicio?

Debe cubrir rutas, compatibilidad del esquema, eventos en cola y escrituras aceptadas durante la versión fallida. Mantén una autoridad por campo y reproduce un registro conocido; la escritura bidireccional complica conflictos.

¿Es mejor un monolito modular que los microservicios?

Es mejor cuando transacciones, propiedad del equipo y despliegue siguen moviéndose juntos. Módulos y permisos impuestos dan propiedad clara sin llamadas de red, recuperación distribuida y operación separada.