Dividir un monolito en microservicios implica convertir una masa de funcionalidades entrelazadas en componentes autónomos que respetan límites de dominio y pueden evolucionar sin bloquearse. No se trata solo de trazar líneas técnicas, sino de comprender el negocio, las dependencias y el ritmo de cambio de cada parte para definir una secuencia segura de extracciones. Una buena descomposición reduce el riesgo de caer en el temido monolito distribuido y ofrece un camino gradual donde los servicios nuevos conviven con el legado hasta que sea posible retirarlo.
El primer paso consiste en mapear el monolito existente: qué módulos lo componen, qué transacciones ejecuta y cuáles son las dependencias reales entre capas. A partir de este inventario se identifican las capacidades de negocio que generan mayor fricción cuando deben evolucionar. Es habitual comenzar por funcionalidades que sufren cuellos de botella en rendimiento, requieren despliegues frecuentes o necesitan escalar de manera independiente.
La estrategia recomendada es incremental. Se extrae un servicio a la vez, publicando una API clara y creando un mecanismo de migración como el Strangler Pattern. Este patrón permite redirigir progresivamente las llamadas desde el monolito hacia el servicio recién creado, validando que las nuevas implementaciones mantengan la experiencia de usuario y la integridad de los datos.
Para evitar interrupciones, cada extracción debe ir acompañada de pruebas automatizadas, visibilidad de logs y métricas que indiquen cómo responden las peticiones. Si la operación se vuelve inestable, se puede revertir el enrutamiento sin necesidad de desmontar el servicio creado. Con el tiempo, el monolito se va reduciendo a funcionalidades residuales hasta quedar en una base mucho más pequeña o desaparecer por completo.
Las particiones más sostenibles se alinean con el lenguaje del negocio. Una partición por dominio asigna cada microservicio a un conjunto de reglas coherentes, como catálogo, pagos, envíos o atención al cliente. Este criterio mantiene la cohesión funcional y facilita que los equipos comprendan el contexto completo de su servicio.
En ocasiones resulta útil comenzar con una división por funcionalidad, enfocada en capas o capacidades técnicas particulares. Por ejemplo, separar los procesos de facturación masiva en un servicio especializado mientras el resto del monolito continúa atendiendo escenarios tradicionales. Esta estrategia genera alivio rápido en puntos de dolor concretos, pero requiere una revisión posterior para validar que los servicios resultantes mantienen una responsabilidad clara y no se convierten en utilitarios genéricos.
Al combinar ambos criterios se obtienen resultados equilibrados. Se prioriza el dominio como guía principal y las capacidades técnicas como refinamiento secundario, garantizando que los servicios resultantes respondan a una necesidad de negocio específica y, al mismo tiempo, optimicen aspectos como el rendimiento o la disponibilidad.
Cuando se definen particiones también es clave observar el flujo de datos. Si dos funcionalidades comparten transacciones críticas o tablas centrales, conviene retrasar su separación hasta contar con mecanismos de mensajería o sincronización que preserven la consistencia. De lo contrario, cada despliegue agregaría complejidad operativa y riesgo de errores.
Domain-Driven Design (DDD) provee herramientas para delimitar responsabilidades con base en el conocimiento del dominio. El análisis comienza identificando los subdominios del negocio y clasificándolos como núcleo, soporte o genéricos. Cada subdominio define un conjunto de conceptos y reglas que puede convertirse en un microservicio potencial.
El concepto clave es el bounded context: una frontera semántica donde los modelos, vocabulario y acuerdos de integración son coherentes. Al extraer un microservicio se diseña su contexto delimitado, se establece un mapa de relaciones con otros contextos y se eligen patrones de integración como traducciones, anti-corruption layers o publicación de eventos.
Las sesiones de Event Storming ayudan a visualizar flujos de negocio, descubrir comandos y eventos relevantes y detectar lugares donde la información cambia de significado. Estos momentos marcan transiciones entre contextos y son candidatos naturales para colocar fronteras. Además, trabajar con expertos del dominio asegura que la descomposición no quede guiada solo por la estructura del código, sino por la forma real en que opera la organización.
Otro elemento central es mantener un lenguaje ubicuo. El nombre de cada servicio, sus endpoints y eventos debe coincidir con el vocabulario construido durante el modelado. Esto minimiza malentendidos entre equipos y facilita la evolución del sistema cuando cambian las reglas de negocio.
Un error frecuente es fragmentar el sistema en servicios diminutos que dependen entre sí para ejecutar cualquier transacción. Este escenario genera latencias acumuladas, fallas encadenadas y equipos obligados a coordinar cambios constantes. Para evitarlo, conviene seguir las siguientes prácticas:
El objetivo es encontrar un equilibrio entre autonomía y simplicidad operativa. Un microservicio bien dimensionado encapsula un problema de negocio completo, mantiene contratos claros y publica eventos o APIs que permiten evolucionarlo sin generar efecto dominó en el resto del ecosistema.
En una migración gradual es común crear un servicio que replique parte de la funcionalidad legada y se integre mediante un adaptador. El siguiente fragmento muestra una fachada que importa perfiles de clientes desde un sistema antiguo y publica un evento para el nuevo ecosistema.
@Service
public class CustomerProfileFacade {
private final LegacyCustomerGateway legacyGateway;
private final CustomerRepository repository;
private final DomainEventPublisher publisher;
private final CustomerMapper mapper;
public CustomerProfileFacade(LegacyCustomerGateway legacyGateway,
CustomerRepository repository,
DomainEventPublisher publisher,
CustomerMapper mapper) {
this.legacyGateway = legacyGateway;
this.repository = repository;
this.publisher = publisher;
this.mapper = mapper;
}
@Transactional
public CustomerProfile importFromLegacy(String legacyId) {
LegacyCustomer legacy = legacyGateway.fetch(legacyId);
CustomerProfile profile = mapper.fromLegacy(legacy);
repository.save(profile);
publisher.publish(new CustomerImported(profile.id()));
return profile;
}
}
Este servicio funciona como una capa anti-corruption: traduce el modelo del sistema heredado a un modelo limpio alineado con el nuevo contexto de clientes. El evento CustomerImported permite que otros microservicios reaccionen sin depender de consultas sincrónicas, evitando acoplamientos innecesarios mientras se completa la migración.