Be Software Architecture Wise
Be Wise Apps

Read in English · Lire en français · Auf Deutsch lesen

Sección 5.3 de 5.8 · Intermedio · 26 min de lectura

El monolito modular

Videolección

5 min · narrado en inglés, con subtítulos
Solo escuchar (audio)
Leer la transcripción

Un monolito modular te da muchas de las ventajas que se buscan en los microservicios, sin tener que operar un sistema distribuido. En esta lección verás cómo se construye, cómo se mantienen sus límites y dónde falla con más frecuencia.

Piensa en una gran tienda por departamentos. Es un solo edificio, con una sola puerta de entrada, un solo horario de apertura y un solo equipo de seguridad. Por dentro, está dividida en departamentos: alimentación, ropa, electrónica. Cada uno tiene su propio personal, su propio almacén y su propia caja. Si ropa necesita algo de alimentación, lo pide en el mostrador de alimentación en lugar de meterse en el almacén de alimentación. Un monolito modular es software construido como esa tienda.

Un monolito modular es una única unidad de despliegue cuyo nivel superior está dividido por dominio de negocio en módulos: pedidos, pagos, reparto, cartas. Combina el particionamiento por dominio de los microservicios con la simplicidad de un monolito: una compilación, un despliegue, llamadas en memoria y transacciones locales.

Así es uno por dentro. El límite exterior es una unidad de despliegue, la aplicación de reparto de comida, y dentro hay módulos como Pedidos y Pagos. Cada módulo tiene una API pública pequeña: las funciones, los datos y los eventos que sus responsables se comprometen a mantener estables. Pedidos usa Pagos solo llamando a esa API. Todo lo demás es interno: el modelo de dominio, las reglas y los repositorios. Meterse en las partes internas de otro módulo está prohibido. Y cada módulo es dueño de su propio esquema de base de datos, sin joins entre esquemas.

En un monolito, técnicamente cualquier clase puede llamar a cualquier otra, así que los límites solo se mantienen si algo los hace cumplir. Si no, degenera en una gran bola de barro en uno o dos años. Empieza por la ocultación más fuerte que ofrezca tu lenguaje. Añade pruebas de arquitectura en la compilación, como ArchUnit para Java o import-linter para Python, que fallan cuando un módulo importa las partes internas de otro. Los frameworks pueden ayudar. Spring Modulith verifica las reglas de los módulos en aplicaciones Spring, y Packwerk, de Shopify, comprueba las dependencias entre paquetes en Ruby. Y traza límites de datos: un esquema por módulo e, idealmente, un usuario de base de datos que solo pueda llegar a ese esquema. La revisión de código no impedirá un join cómodo; los permisos de la base de datos sí.

La propiedad de los datos es donde los monolitos modulares fallan con más frecuencia. Compartir un servidor de base de datos está bien; compartir tablas, no. Cada tabla tiene exactamente un módulo propietario, y solo el propietario escribe en ella. Los demás módulos obtienen los datos a través de la API del propietario o de los eventos que publica. Un informe que unía las tablas de tres módulos ahora necesita llamadas a la API o su propio modelo de lectura. Es un costo real, pero es el costo que traería un paso posterior a servicios, pagado antes y de forma barata. Los módulos se comunican de dos maneras, ambas en memoria. Haz una llamada cuando necesites una respuesta, como saber si se autorizó un pago. Publica un evento cuando no la necesites, y cuando varios módulos reaccionen al mismo hecho.

Veamos un ejemplo práctico. Una startup de reparto de comida con cuatro equipos, unos veinticinco ingenieros, quiere que cada equipo sea dueño de su parte de forma independiente, sin operar microservicios. El arquitecto propone ocho módulos en una única aplicación Python desplegable, con un esquema por módulo. Pedidos está en el centro. Llama a tres módulos a través de sus API: a Pagos para pagar, a Cartas para poner precio a los artículos y a Clientes para obtener la dirección. Restaurantes actualiza las cartas a través de la API de Cartas. Cuando se hace un pedido, Pedidos publica un evento OrderPlaced, y Reparto lo escucha. También lo escuchan Informes y Notificaciones. Pedidos no necesita saber quién está escuchando. Las dependencias apuntan en un solo sentido, así que los ciclos son imposibles, e import-linter lo comprueba en cada compilación. Todo se entrega como una sola imagen de contenedor, ejecutada como seis réplicas, más un worker para los eventos y los trabajos en segundo plano.

Un caso real muy conocido es Shopify. En lugar de dividir su enorme monolito de Ruby on Rails en microservicios, a partir de 2017 reorganizó el código en componentes con nombres de dominios de negocio, como pedidos, envíos y facturación. No fue un éxito sencillo: años después, casi ningún componente tenía un límite completo y sólido. Un monolito modular necesita una inversión sostenida, e incorporar límites a posteriori es mucho más difícil que mantenerlos desde el principio. La recompensa son las opciones. Si más adelante un módulo necesita escalar o lanzarse por su cuenta, la extracción es mucho más fácil cuando ya tiene una API explícita, es dueño de sus datos y se comunica con los demás solo a través de API y eventos.

La sección completa trata con más detalle las buenas API de módulo, las sutilezas de los eventos y la historia de Shopify. A continuación, en la Sección cinco punto cuatro, pasamos a una forma muy distinta: tuberías y filtros. Nos vemos allí.

Lo que necesitas saber

Definición: un despliegue, muchos dominios

Un monolito modular es una aplicación que se construye y se despliega como una sola unidad, pero cuyo código está dividido, en el nivel superior, en módulos que siguen límites de negocio: pedidos, pagos, reparto, cartas. Cada módulo tiene una API pública pequeña y deliberada que otros módulos pueden usar, una parte interna más grande que nadie de fuera puede tocar, y sus propios datos. Combina el particionamiento por dominio de los microservicios con la unidad de despliegue única de un monolito clásico.

La idea no es nueva. Los módulos con partes internas ocultas se remontan a la ocultación de información de David Parnas (1972), y los monolitos grandes y bien estructurados siempre han existido. Lo más reciente es el nombre y la atención que recibe. Tras una década en la que muchas organizaciones dividieron sus sistemas en microservicios y descubrieron el costo de la distribución, profesionales como Simon Brown y Sam Newman sostuvieron que el monolito modular es una opción infravalorada y que a muchos equipos les iría mejor con uno que con un conjunto de servicios. Richards y Ford le dedican un capítulo propio en la segunda edición de Fundamentals of Software Architecture (2025).

La diferencia con el estilo en capas está en el corte del nivel superior. Una aplicación en capas se divide en presentación, negocio y persistencia, con cada dominio repartido entre las tres. Un monolito modular se divide en dominios, y cada módulo puede organizarse en capas por dentro. La diferencia con los microservicios es el despliegue: los módulos comparten un proceso, un lanzamiento y, normalmente, un servidor de base de datos, y se llaman entre sí en memoria.

Anatomía de un monolito modular

Dos módulos en una unidad de despliegue: cada uno tiene una API pública pequeña y una parte interna mayor, y es dueño de su esquema de base de datos; Pedidos puede llamar a la API de Pagos, pero no tocar las partes internas de Pagos ni sus tablasUna unidad de despliegue: la app de reparto de comidaMódulo PedidosAPI públicafunciones, DTO, eventosInternomodelo de dominio,reglas, repos.esquema pedidosMódulo PagosAPI públicafunciones, DTO, eventosInternomodelo de dominio,reglas, repos.esquema pagosllamar APIprohibidosin joins entre esquemas
Cada módulo expone una API pública pequeña y es dueño de su propio esquema; meterse en las partes internas o en las tablas de otro módulo está prohibido.

API de los módulos y código interno

La regla más importante de todas es que un módulo solo se usa a través de su API. La API es el conjunto de funciones, objetos de transferencia de datos (DTO) y eventos que los responsables del módulo se comprometen a mantener estables; todo lo demás, incluidos el modelo de dominio, los repositorios y las tablas de la base de datos, es interno y puede cambiar en cualquier momento. En la práctica, la API es pequeña: un módulo de pagos podría exponer authorise_payment, refund, un tipo PaymentResult y un evento PaymentCaptured, mientras oculta decenas de clases para proveedores, reintentos, libros contables y conciliación.

Las buenas API de módulo comparten varios rasgos. Hablan el lenguaje del dominio (authorise_payment, no insert_payment_row). Pasan datos simples, como identificadores y pequeños registros inmutables, en lugar de objetos internos, para que quien llama no pueda atravesar un objeto devuelto y llegar a las partes internas del módulo. Son de grano lo bastante grueso como para que un caso de uso típico necesite una o dos llamadas, no veinte. Y se diseñan como si algún día pudiera haber una red entre quien llama y quien es llamado: sin objetos mutables compartidos, sin depender de la transacción de base de datos de quien llama, con errores explícitos. Este último rasgo es el que conserva la opción de extraer el módulo más adelante.

Dentro del límite vale todo lo que al equipo del módulo le resulte útil: una estructura en capas, una hexagonal, un modelo de dominio rico o simples scripts de transacción. Distintos módulos pueden tomar decisiones distintas, porque nada de fuera depende de ellas.

# delivery_platform/payments/__init__.py  (the module's public API)
from .api import authorise_payment, refund, PaymentResult
from .events import PaymentCaptured

__all__ = ["authorise_payment", "refund", "PaymentResult", "PaymentCaptured"]

# delivery_platform/ordering/internal/checkout.py
from delivery_platform import payments             # allowed: public API
# from delivery_platform.payments.internal import ledger   # forbidden

def place_order(order, card_token):
    result = payments.authorise_payment(order.id, order.total, card_token)
    if not result.ok:
        raise PaymentDeclined(result.reason)
    order.confirm(result.payment_id)
    events.publish(OrderPlaced(order.id, order.restaurant_id))
Un paquete de Python como módulo: el paquete payments exporta una API pequeña, y ordering importa solo esa API.

Hacer cumplir los límites

En un monolito, técnicamente cualquier clase puede llamar a cualquier otra, así que los límites solo se mantienen si algo los hace cumplir. Sin ese control, un atajo hacia las partes internas de otro módulo, tomado por la presión de un plazo, pasa inadvertido en la revisión, y un monolito modular degenera en una gran bola de barro (big ball of mud) en uno o dos años. Hay varias capas de defensa, y lo mejor es usarlas juntas.

Visibilidad del lenguaje. Usa la ocultación más fuerte que ofrezca el lenguaje: clases privadas de paquete en Java, internal en Kotlin y C#, los exports del Java Platform Module System, módulos de compilación separados (subproyectos de Maven o Gradle, proyectos .NET) cuyas dependencias se declaran explícitamente. Python y Ruby solo ofrecen convenciones (un guion bajo inicial, un subpaquete internal), así que las herramientas tienen que hacer el trabajo.

Pruebas de arquitectura. Las pruebas de arquitectura comprueban las dependencias en la compilación. ArchUnit (Java) puede afirmar que ninguna clase de fuera de un módulo toca sus paquetes internos y que las dependencias entre módulos no tienen ciclos. import-linter (Python) ofrece un contrato forbidden, que puede impedir que otros módulos importen el paquete interno de un módulo sin dejar de permitirles usar su API pública; un contrato layers cuyas capas pueden contener varios hermanos mutuamente independientes separados por |, lo que permite comprobar un orden de dependencia en un solo sentido entre módulos; y un contrato independence para paquetes que no deben depender unos de otros en absoluto, ni siquiera de forma indirecta.

Soporte del framework. Spring Modulith, para aplicaciones Spring Boot, trata cada subpaquete directo del paquete principal de la aplicación como un módulo, trata el paquete base del módulo como su API y sus subpaquetes como internos, y ofrece una verificación de una sola línea (ApplicationModules.of(Application.class).verify()) que falla cuando un módulo hace referencia a las partes internas de otro, cuando las dependencias entre módulos forman un ciclo o, si un módulo enumera sus dependencias permitidas, cuando usa una que no está en la lista. También puede generar diagramas de componentes de los módulos al estilo C4. En el mundo Ruby, Packwerk, la herramienta de código abierto de Shopify, analiza estáticamente las referencias entre paquetes, cada uno declarado mediante un archivo package.yml, e informa de las dependencias no declaradas.

Límites de datos. Dale a cada módulo su propio esquema de base de datos (o al menos sus propias tablas con un prefijo claro) e, idealmente, su propio usuario de base de datos que solo pueda acceder a ese esquema. La revisión de código por sí sola no impedirá un JOIN cómodo entre módulos; los permisos de la base de datos sí.

[importlinter:contract:payments-internals]
name = Only payments may use payments internals
type = forbidden
source_modules =
    delivery_platform.ordering
    delivery_platform.delivery
    delivery_platform.reporting
forbidden_modules =
    delivery_platform.payments.internal
# going through the public API (which itself uses internals) is fine
allow_indirect_imports = True

[importlinter:contract:module-order]
name = Module dependencies point one way
type = layers
layers =
    delivery_platform.reporting | delivery_platform.notifications
    delivery_platform.delivery
    delivery_platform.ordering | delivery_platform.restaurants
    delivery_platform.payments | delivery_platform.menus | delivery_platform.customers
Contratos de import-linter (en la práctica, uno por módulo): otros módulos no pueden importar las partes internas de payments, y las dependencias entre módulos deben seguir una sola dirección, con los módulos de una misma línea independientes entre sí.

Propiedad de los datos dentro de un monolito

La propiedad de los datos es donde los monolitos modulares fallan con más frecuencia. Compartir un servidor de base de datos está bien; compartir tablas, no. Si el módulo de reparto lee directamente la tabla orders, el equipo de pedidos ya no puede cambiar esa tabla sin coordinarse con reparto, y los dos módulos quedan tan acoplados como si fueran uno. La regla es fácil de enunciar: cada tabla tiene exactamente un módulo propietario, solo el propietario escribe en ella, y los demás módulos obtienen los datos a través de la API del propietario o de los eventos que este publica.

La regla tiene consecuencias que los equipos deben aceptar de forma deliberada. Un informe que antes era una sola consulta SQL que unía pedidos, pagos y repartidores ahora necesita o bien llamadas a las API de tres módulos, o bien un modelo de lectura: un módulo de informes que se suscribe a los eventos de los demás y mantiene su propia copia desnormalizada, optimizada para consultas. Las claves foráneas entre esquemas de módulos suelen eliminarse, porque harían que el esquema de un módulo dependiera del de otro; en su lugar, la integridad entre módulos la mantiene la aplicación. Estos costos son reales, pero son los mismos que impondría un paso posterior a servicios, pagados antes y en un entorno más barato.

Queda una ventaja que los sistemas distribuidos pierden: los módulos de un mismo proceso pueden, cuando de verdad es necesario, compartir una única transacción local de base de datos. Úsala con moderación. Cada uso acopla operativamente los datos de los módulos y habrá que deshacerlo si algún día se extrae un módulo.

Comunicación entre módulos: llamadas o eventos

Los módulos se comunican de dos maneras, ambas en memoria. Una llamada en proceso a la API de otro módulo es simple, rápida e inmediata: pedidos llama a payments.authorise_payment y obtiene el resultado al instante, y una excepción se propaga con normalidad. Crea una dependencia directa, de quien llama a quien es llamado, y quien llama no puede terminar hasta que termine el otro. Usa llamadas cuando quien llama necesita una respuesta para continuar, como saber si se autorizó el pago.

Un evento en proceso invierte la dependencia. Pedidos publica OrderPlaced y no sabe ni le importa quién escucha; reparto, notificaciones e informes se suscriben. Añadir una nueva reacción significa añadir un listener, sin tocar pedidos. Usa eventos cuando quien publica no necesita respuesta y cuando varios módulos reaccionan al mismo hecho.

Los eventos introducen sutilezas que los equipos que no los conocen suelen pasar por alto. Si un listener se ejecuta dentro de la transacción de quien publica, un fallo en el listener puede revertir el pedido; si se ejecuta después del commit, el pedido queda guardado, pero el trabajo del listener puede perderse si el proceso se cae entre medias. Spring Modulith lo resuelve con un registro de publicación de eventos que anota cada par evento-listener en la misma transacción que el cambio de negocio y solo lo marca como completado cuando el listener termina con éxito, de modo que los incompletos se puedan reintentar; un equivalente hecho a mano es la bandeja de salida transaccional (transactional outbox) descrita en la Sección 6.7. Los listeners también deben ser idempotentes, para que un evento reintentado no cause daños. Ningún estilo es mejor; la mayoría de los monolitos modulares usan llamadas para las consultas y los comandos que necesitan respuesta, y eventos para notificar cosas que ya han ocurrido.

El monolito modular de Shopify

La aplicación principal de Shopify es una de las mayores bases de código de Ruby on Rails que existen, en la que han trabajado más de mil desarrolladores durante más de una década. En Deconstructing the Monolith (Shopify Engineering, febrero de 2019), Kirsten Westeinde explicó por qué la empresa no la dividió en microservicios y, en su lugar, a partir de 2017, la convirtió en un monolito modular. El problema era conocido: el código de áreas de negocio sin relación entre sí estaba enredado, los cambios tenían efectos inesperados en otras partes y la incorporación de personas nuevas era lenta. Se consideraron los microservicios y se concluyó que traían sus propios costos (pipelines e infraestructura separados, latencia y fallos de red, refactorizaciones más difíciles entre servicios), así que el equipo optó por conseguir modularidad sin múltiples unidades de despliegue.

El primer paso fue reorganizar el código, de la estructura estándar de Rails (modelos, vistas y controladores) a componentes con nombres de dominios de negocio, como pedidos, envíos, inventario y facturación, en un único gran cambio automatizado. El siguiente fue el aislamiento: cada componente debía tener una API pública y la propiedad exclusiva de sus datos, y una herramienta interna, Wedge, seguía el progreso de cada componente registrando las llamadas que cruzaban límites durante la batería de pruebas. Una actualización de septiembre de 2020 informaba de 37 componentes en un monolito central de unos 2.8 millones de líneas de Ruby, y ese mismo mes Shopify liberó como código abierto Packwerk, una herramienta de análisis estático para declarar y comprobar dependencias entre paquetes.

La historia es instructiva porque no es un éxito sencillo. La actualización de 2020 decía que las ideas originales seguían en pie, pero que casi todos los detalles habían cambiado, y que casi ningún componente tenía todavía un límite completo y sólido. Una retrospectiva de 2024 sobre Packwerk informaba de que se habían eliminado sus comprobaciones de privacidad, de que algunos paquetes organizados por nombre y no por comportamiento en tiempo de ejecución confundían a los desarrolladores, y de que aislar por completo incluso un paquete base llevaba muchos meses; Packwerk se había convertido en una protección de límites clave más que en el centro de la estrategia. La lección para los arquitectos es que un monolito modular es una dirección de viaje que necesita una inversión sostenida, y que incorporar límites a posteriori en una base de código grande ya existente es mucho más difícil que mantenerlos desde el principio.

Ejemplo práctico

Ejemplo práctico: la plataforma de reparto de comida como monolito modular

Una startup de reparto de comida con cuatro equipos (unos 25 ingenieros) quiere que cada equipo sea dueño de su parte de forma independiente, sin el costo de operar microservicios. El arquitecto propone ocho módulos en una única aplicación Python desplegable, con un servidor PostgreSQL y un esquema por módulo.

ClientesCuentas, direcciones, preferencias. API: get_customer, get_delivery_address. Equipo A.
Restaurantes; Cartas (dos módulos)Restaurantes guarda las cuentas de los restaurantes y los horarios de apertura, y actualiza las cartas a través de la API de Cartas; Cartas ofrece precios y disponibilidad a todos los demás (price_items, is_open). Equipo B.
PedidosCarrito, proceso de pago, ciclo de vida del pedido. Llama a Clientes, Cartas y Pagos; publica OrderPlaced y OrderCancelled. Equipo C.
PagosAutorización, captura y reembolsos a través de un proveedor externo. API: authorise_payment, refund; publica PaymentCaptured. Equipo C.
EntregaAsignación de repartidores y seguimiento. Escucha OrderPlaced; publica CourierAssigned y Delivered. Equipo D.
Notificaciones e InformesListeners puros: envían mensajes y mantienen modelos de lectura. No llaman a ningún otro módulo. Equipo A.
Regla de dependenciaPagos, Cartas y Clientes no dependen de nadie; Pedidos y Restaurantes los llaman; Reparto reacciona a los eventos de Pedidos; Notificaciones e Informes solo escuchan. Lo comprueba import-linter en la compilación.
DespliegueUna sola imagen de contenedor, ejecutada como seis réplicas detrás de un balanceador de carga, más un proceso worker (con el mismo código) que gestiona los listeners de eventos y los trabajos en segundo plano.

Cada equipo es dueño de módulos completos, así que la mayoría de los cambios caen en un solo módulo y un solo equipo.

La regla de dependencias hace imposibles los ciclos: si algún día Pagos necesita saber algo de los pedidos, recibe un identificador de pedido, no una importación de Pedidos.

Mapa de módulos de la plataforma de reparto de comida

Mapa de módulos de la plataforma de reparto de comida: ocho módulos en una sola aplicación desplegable; Pedidos llama a las API de Pagos, Cartas y Clientes, Restaurantes llama a Cartas, Pedidos publica eventos OrderPlaced que consumen Reparto, Notificaciones e Informes, y Reparto publica CourierAssigned hacia NotificacionesPlataforma de reparto: una aplicación desplegableClientesCartasRestaurantesPedidosPagosRepartoInformesNotificacionespagarpreciosdireccióneditarOrderPlacedOrderPlacedOrderPlacedCourierAssignedllamada en proceso a la APIevento en proceso
Las flechas continuas son llamadas a la API de otro módulo; las discontinuas son eventos en proceso. Pedidos llama a tres módulos, y sus listeners solo dependen de los eventos que publica.

Características del monolito modular

CaracterísticaValoraciónMotivo
SencillezDe media a altaUn solo despliegue y llamadas en proceso, pero encontrar buenos límites de módulo exige reflexión.
Costo totalAlta (barato)Infraestructura y operación de un monolito; algo de esfuerzo extra para las herramientas que vigilan los límites.
Modificabilidad ante cambios de negocioAltoEl particionamiento por dominio mantiene la mayoría de los cambios dentro de un módulo del que es dueño un solo equipo.
Capacidad de prueba (testabilidad)De media a altaLos módulos se pueden probar a través de sus API sustituyendo a sus vecinos; las pruebas del sistema completo se ejecutan en un solo proceso.
DesplegabilidadDe baja a mediaSigue siendo una unidad de despliegue: cada lanzamiento incluye todos los módulos, aunque ayuda que cada cambio tenga un radio de impacto menor.
Escalabilidad y elasticidadDe baja a mediaLas réplicas escalan toda la aplicación; un módulo no se puede escalar por separado, aunque los procesos worker pueden separar la carga en segundo plano.
Tolerancia a fallosBajoUn solo proceso: una caída o una fuga de memoria en cualquier módulo los afecta a todos.
RendimientoAltoLas llamadas entre módulos son en memoria, sin serialización ni latencia de red.
Capacidad de evoluciónAltoLas API explícitas y la propiedad de los datos hacen que los módulos sean fáciles de reestructurar o extraer más adelante.

Cuándo usarlo, cuándo no y antipatrones

Un monolito modular es una opción por defecto sólida para productos nuevos cuyo dominio está razonablemente claro, para organizaciones de hasta unos pocos equipos, para sistemas que necesitan consistencia fuerte entre áreas de negocio y para equipos con capacidad de operaciones limitada. También es el destino natural al modernizar un monolito enredado: introducir límites dentro de él suele ser más barato y menos arriesgado que dividirlo en servicios de una sola vez. Es menos adecuado cuando partes del sistema necesitan atributos de calidad realmente distintos (un flujo de posiciones de repartidores que debe absorber enormes volúmenes de escritura y un módulo de back-office con una carga modesta), cuando decenas de equipos deben lanzar de forma independiente muchas veces al día, o cuando algunas partes deben escribirse en lenguajes distintos o ejecutarse en lugares distintos por motivos de residencia de datos. Esas necesidades exigen cuantos separados.

Antipatrones habituales: módulos solo de nombre, donde las carpetas siguen los dominios pero cualquier cosa importa cualquier cosa; la base de datos compartida sin reglas, donde los módulos mantienen su propio código pero hacen joins con las tablas de los demás; el límite charlatán, una API de grano tan fino que un caso de uso necesita decenas de llamadas, lo que resultará doloroso si algún día se extrae el módulo; el núcleo compartido que crece, un módulo common que poco a poco absorbe lógica de negocio y acopla a él todos los módulos; y los módulos por entidad, que cortan por entidad de datos (un módulo Cliente, un módulo Pedido, un módulo Producto que todos los casos de uso necesitan a la vez) en lugar de por capacidad de negocio, lo que produce módulos que no pueden hacer nada por sí solos.

Mantener la opción de extraer servicios

Un monolito modular se presenta a veces como un paso intermedio hacia los microservicios y a veces como un destino. Puede ser cualquiera de los dos, y lo importante es que la decisión puede aplazarse hasta el último momento responsable. Si más adelante un módulo necesita escalar por separado o lanzarse de forma independiente, la extracción es mucho más fácil cuando el módulo ya tiene una API explícita, es dueño de sus datos y depende de los demás solo a través de API y eventos. El trabajo pasa a ser en gran parte mecánico: poner una interfaz de red delante de la API, mover el esquema a su propia base de datos, sustituir los eventos en proceso por un intermediario de mensajes (message broker) y sustituir cualquier transacción compartida por una saga explícita o una compensación.

Sam Newman defiende lo mismo en Monolith to Microservices (2019): un monolito bien modularizado es el mejor punto de partida para la descomposición, porque la parte difícil, encontrar los límites y hacerlos cumplir, ya está hecha. La Sección 5.8 analiza las señales de que ha llegado el momento de extraer, y la Sección 11.6 trata técnicas de migración incremental como el strangler fig (higuera estranguladora).

Comprueba lo que has entendido

¿Qué distingue a un monolito modular de un monolito en capas?

Su nivel superior está dividido por dominio de negocio en lugar de por capa técnica; cada módulo tiene una API explícita, partes internas privadas y sus propios datos, y puede estar organizado en capas por dentro.

¿Por qué las API de los módulos deben pasar identificadores y registros simples en lugar de objetos internos del dominio?

Los objetos internos devueltos permiten a quienes llaman meterse en las partes internas del módulo y los acoplan a su modelo; los datos simples dejan las partes internas libres para cambiar y la API lista para cruzar una red más adelante.

Un informe necesita datos de los esquemas de pedidos y de pagos. ¿Cuáles son dos opciones que respetan la propiedad de los datos?

Llamar a las API de ambos módulos y combinar los resultados, o crear un modelo de lectura en un módulo de informes que escuche los eventos de ambos módulos y mantenga su propia copia desnormalizada.

¿Qué riesgo introduce un listener de eventos en proceso que se ejecuta después de que se confirme la transacción de quien publica?

Si el proceso se cae después del commit pero antes de que se ejecute el listener, el trabajo del listener se pierde, a menos que el evento se registre de forma duradera, por ejemplo en un registro de publicación de eventos o en una bandeja de salida (outbox), y se reintente.

Siguiente · 5.4Tuberías y filtrosDatos que fluyen por una cadena de pasos independientes: productores, transformadores, verificadores y consumidores unidos por tuberías. De las tuberías de Unix a ETL, los compiladores y el procesamiento de flujos, con el rendimiento (throughput), el back pressure y la gestión de errores.Continuar →

Fuentes y lecturas recomendadas

¿Tienes una pregunta sobre esta sección? ArchitectBot, el asistente de estudio con IA del curso, responde primero a partir del curso.

Pregunta: “Tenemos un monolito Django con apps para pedidos, pagos y reparto, pero todas importan los modelos de las demás. ¿Cómo lo convertimos paso a paso en un verdadero monolito modular?”