De un Monolito a una Arquitectura Distribuida, sin Reescribir el Producto

Impacto

~10 microservicios, cero reescritura

Un monolito era la decisión correcta al principio, hasta que módulos con necesidades muy distintas empezaron a competir por la misma infraestructura. Introduje una capa de providers para migrar responsabilidades hacia microservicios sin reescribir el producto ni detener el desarrollo, con foco en aislar fallas: que un servicio secundario se caiga no debería tumbar el core.

Software Developer

Cuando un producto recién arranca, un monolito suele ser de las mejores decisiones que se pueden tomar: permite avanzar rápido, mantener costos bajos y evitar la complejidad operativa de tener múltiples servicios, bases de datos y deployments desde el día uno. Eso era lo que teníamos: un backend crecido dentro de una misma aplicación, con múltiples módulos compartiendo el mismo motor de base de datos. Funcionaba bien hasta que el producto creció y esos módulos, con patrones de consumo muy distintos, empezaron a competir por los mismos recursos de infraestructura. El problema ya no era el monolito en sí: era que responsabilidades con necesidades muy diferentes compartían exactamente la misma infraestructura.

Encontrar qué podíamos separar

En vez de plantear una reescritura completa, analizamos el sistema módulo por módulo, buscando funcionalidades con una responsabilidad lo bastante clara como para aislarse, especialmente las que no necesitaban acceso constante a la información más crítica del producto. La idea era mantener en la base de datos central solo lo que realmente pertenecía al core (la información sensible, las entidades fundamentales) y mover progresivamente el resto a servicios independientes. La estrategia fue deliberadamente incremental: no queríamos detener el desarrollo durante meses para construir una arquitectura nueva. Queríamos cambiarla mientras el producto seguía evolucionando.

Una capa de providers

Para hacer la transición sin acoplar el resto de la aplicación a una implementación específica, introdujimos una capa nueva dentro del modelo tradicional de controlador y modelo: los providers. Funcionaban como una abstracción entre la aplicación y la implementación de determinadas funcionalidades, de modo que una misma responsabilidad podía ejecutarse inicialmente dentro del monolito y después ser reemplazada por una implementación detrás de una API independiente, sin que la aplicación principal necesitara conocer dónde vivía esa funcionalidad. Esto fue clave para migrar de forma progresiva: la API central empezó a comunicarse con distintos providers y estos, a su vez, con servicios independientes, hasta construir una arquitectura compuesta por servicios más chicos, cada uno con su propia infraestructura y, cuando tenía sentido, su propia base de datos.

El objetivo no era tener más microservicios

La motivación no era adoptar microservicios por moda arquitectónica. Era reducir el acoplamiento y aislar responsabilidades: si una funcionalidad secundaria tenía un problema, no queríamos que pudiera comprometer el core del producto. Es el mismo principio que popularizó Netflix: si el servicio que trae la descripción de una película falla, eso no debería impedirte reproducirla. Si un servicio secundario falla, tiene que fallar ese servicio, no todo el producto. Esta separación también nos permitió que determinados servicios evolucionaran, se desplegaran y se depuraran de forma independiente.

Los servicios también necesitaban comunicarse

A medida que la arquitectura crecía, algunos microservicios necesitaban información que pertenecía a otros contextos. La solución más simple habría sido dejar que cada servicio se conectara directo a las bases de datos de los demás. Decidimos no hacerlo: en varios casos introdujimos servicios que funcionaban como bridges, exponiendo por API la información que otro servicio necesitaba sin darle acceso directo a la base de datos ajena. Un servicio se comunicaba con el bridge y este resolvía la comunicación con el servicio correspondiente o con la API central. Esto mantuvo una frontera clara entre responsabilidades y evitó que separar físicamente las bases de datos terminara creando un acoplamiento nuevo.

Una migración que evolucionaba con el producto

La arquitectura terminó pasando de una API central con múltiples módulos a una red de servicios especializados: algunos completamente independientes, otros que necesitaban comunicarse entre sí, y en algunos casos servicios que actuaban como intermediarios para mantener esas dependencias bajo control. Llegamos a tener alrededor de una decena de microservicios, pero el número nunca fue el objetivo: cada servicio existía porque había una responsabilidad que tenía sentido separar. Tampoco abandonamos el monolito de un día para otro. Durante un tiempo convivieron ambas arquitecturas: el core siguió centralizado mientras extraíamos progresivamente las responsabilidades que podían aislarse, lo que nos permitió seguir shippeando funcionalidades nuevas mientras reducíamos gradualmente la presión sobre la infraestructura central.

Resultado

La nueva arquitectura distribuyó la carga que antes recaía sobre una única base de datos y separó responsabilidades con necesidades de infraestructura distintas. Pero el beneficio más importante fue otro: el sistema dejó de tener un único punto de fallo para todas sus funcionalidades. Un servicio secundario podía tener problemas sin comprometer la disponibilidad del core, los problemas eran más fáciles de localizar, y los servicios podían recuperarse de forma independiente. La experiencia también reforzó algo que sigo teniendo en cuenta al diseñar sistemas: no existe una arquitectura correcta para siempre. El monolito fue la decisión correcta cuando necesitábamos velocidad y simplicidad. Los microservicios empezaron a tener sentido cuando el tamaño del producto hizo que algunas responsabilidades necesitaran evolucionar de forma independiente. La clave no fue elegir una arquitectura desde el principio, sino reconocer cuándo la que teníamos había dejado de ser la herramienta adecuada, y encontrar la forma de evolucionarla sin detener el producto.