Appearance
Patrón: Descomposición de Servicios y Orquestación
Tipo: Patrón Arquitectural Contexto: Servicios con múltiples responsabilidades (SRP violations) Problema: Servicios monolíticos difíciles de mantener, testear y reutilizar
Problema
Los servicios monolíticos violan el Single Responsibility Principle (SRP) cuando acumulan múltiples responsabilidades:
- Orquestación del flujo completo
- Preparación de datos
- Validaciones de negocio
- Procesamiento de operaciones
- Integración con servicios externos
- Auditoría y logging
- Manejo de errores
Síntomas de servicios monolíticos:
- Más de 500-1000 líneas de código
- Métodos con responsabilidades no relacionadas entre sí
- Alta cantidad de dependencias inyectadas (10+)
- Difícil testear sin mockear demasiadas dependencias
- Cambios en una área afectan todo el servicio
- Lógica no reutilizable en otros contextos
Solución: Service Decomposition + Orchestrator
Descomponer el servicio monolítico en servicios especializados con una única responsabilidad, coordinados por un orchestrator dedicado.
Principios de Descomposición
- Identificar responsabilidades distintas en el servicio monolítico
- Agrupar responsabilidades relacionadas en servicios cohesivos
- Extraer a servicios especializados (máximo 300 líneas cada uno)
- Crear orchestrator para coordinar el flujo
Tipos de Servicios Especializados
| Tipo de Servicio | Responsabilidad | Ejemplo |
|---|---|---|
| Preparation Service | Obtener y preparar datos | Obtener entidades, construir contexto |
| Validation Service | Validaciones de negocio | Validar precios, estado, condiciones |
| Processing Service | Procesamiento principal | Cálculos, generación de comprobantes |
| Integration Service | Comunicación externa | APIs externas, servicios de terceros |
| Audit Service | Trazabilidad y logging | Registro de operaciones, cambios |
El Orchestrator
Rol: Coordinador del flujo completo sin lógica de negocio propia.
Responsabilidades:
- Coordinar la secuencia de ejecución de servicios especializados
- Gestionar transacciones globales (begin/commit/rollback)
- Manejar el contexto compartido entre servicios
- Decidir qué hacer ante errores (abortar, continuar, compensar)
- Retornar resultado agregado al controller
NO debe contener:
- Lógica de negocio (delega a servicios especializados)
- Validaciones (delega a ValidationService)
- Cálculos (delega a ProcessingService)
- Consultas directas a base de datos (delega a PreparationService)
Flujo de Orquestación
Controller
|
v
Orchestrator::execute(request)
|
|-- beginTransaction()
|
|-- PreparationService::prepare(request)
| -> Obtiene datos necesarios
| -> Construye contexto
|
|-- ValidationService::validate(data)
| -> Valida reglas de negocio
| -> Arroja excepciones si falla
|
|-- ProcessingService::process(data, context)
| -> Ejecuta operación principal
| -> Retorna resultado
|
|-- IntegrationService::notify(result)
| -> Comunica con sistemas externos
|
|-- AuditService::log(operation, result)
| -> Registra auditoría
|
|-- commit() / rollback()
|
\-- return ResponseInyección de Dependencias
El orchestrator recibe todos los servicios especializados por constructor:
Orchestrator
|
|-- PreparationService
|-- ValidationService
|-- ProcessingService
|-- IntegrationService
\-- AuditServiceVentajas:
- Cada servicio especializado es testeable independientemente
- El orchestrator solo necesita mockear servicios especializados (5 mocks vs 15+ mocks en servicio monolítico)
- Servicios especializados son reutilizables en otros contextos
Estrategia de Migración
1. Identificar Responsabilidades
Analizar el servicio monolítico y listar todas sus responsabilidades:
- Revisar métodos públicos y privados
- Agrupar métodos por área de responsabilidad
- Identificar dependencias externas por área
2. Crear Servicios Especializados
Orden recomendado (de menor a mayor complejidad):
- AuditService - Sin dependencias complejas, fácil de extraer
- ValidationService - Lógica de validación aislada
- PreparationService - Preparación de datos
- ProcessingService - Lógica principal (más complejo)
- Orchestrator - Coordinador final
3. Migración Incremental
- Coexistencia temporal: Crear servicios nuevos sin eliminar el original
- Migrar método por método: Extraer lógica hacia servicios especializados
- Tests unitarios: Escribir tests para cada servicio especializado
- Reemplazar en controller: Cambiar servicio original por orchestrator
- Tests de integración: Verificar flujo completo
- Eliminar servicio original: Una vez validado el orchestrator
4. Mapeo de Métodos
Documentar qué métodos del servicio original van a qué servicio especializado:
ServicioOriginal -> Servicio Destino
--------------------------------------------------------------
obtenerDatos() -> PreparationService::obtenerDatos()
validarPrecio() -> ValidationService::validarPrecio()
procesarOperacion() -> ProcessingService::procesarOperacion()
enviarNotificacion() -> IntegrationService::enviarNotificacion()
registrarAuditoria() -> AuditService::registrarAuditoria()Granularidad Adecuada
Evitar over-engineering:
- No crear un servicio por cada método
- Agrupar responsabilidades relacionadas cohesivamente
- Objetivo: 4-6 servicios especializados (no 15+)
Tamaño recomendado:
- Orchestrator: 100-200 líneas
- Servicios especializados: máximo 300 líneas cada uno
Señal de granularidad excesiva:
- Servicios con 1-2 métodos únicamente
- Servicios que solo llaman a otros servicios sin lógica propia
- Demasiada comunicación entre servicios especializados
Ventajas
- Testabilidad mejorada: Cada servicio requiere menos mocks
- Mantenibilidad mejorada: Cambios aislados a un servicio
- Reutilización: Servicios especializados usables en otros contextos
- Claridad: Responsabilidad evidente desde el nombre del servicio
- Single Responsibility: Cada servicio tiene una única razón para cambiar
Desventajas
- Más clases: 5-6 servicios en lugar de 1 servicio monolítico
- Coordinación: Orchestrator debe conocer orden de ejecución
- Setup DI: Más bindings en el container de inyección de dependencias
- Esfuerzo inicial: Requiere tiempo de refactorización (días)
Consideraciones
Transacciones
Las transacciones siempre deben gestionarse en el orchestrator:
beginTransaction()antes del primer serviciocommit()si todo fue exitosorollback()ante cualquier error
NO distribuir transacciones entre servicios especializados (crea ambigüedad sobre quién controla el estado transaccional).
Compatibilidad hacia Atrás
- El controller invoca el orchestrator (interfaz idéntica al servicio original)
- La API HTTP no cambia
- Consumidores externos no se ven afectados
Patrones Relacionados
- Dependency Inversion Principle: Servicios especializados dependen de interfaces, no implementaciones
- Five-Layer Architecture: Orchestrator y servicios especializados ambos en Service Layer
- Service Layer Pattern: Encapsula lógica de negocio y orquestación
Cuándo Aplicar
Aplicar cuando:
- Servicio supera 500-1000 líneas
- Tests unitarios requieren 10+ mocks
- Cambios en una área afectan todo el servicio
- Difícil entender la responsabilidad del servicio
NO aplicar cuando:
- Servicio tiene menos de 300 líneas
- Servicio ya tiene una única responsabilidad clara
- No hay plan de reutilizar lógica en otros contextos
Ejemplo Concreto: Portal de Clientes (refactor G3)
Situacion antes del refactor
PortalPaymentService tenia 11 dependencias en el constructor, mezclando:
- Orquestacion de pagos (iniciar, webhook, cancelar)
- Generacion y configuracion de recibos PDF
- Consulta de estado de configuracion (
recibo_configured)
Testear cualquier metodo del servicio requeria mockear 11 dependencias, la mayoria no relevantes para el metodo bajo prueba.
Decision de descomposicion
| Servicio | Responsabilidad | Dependencias |
|---|---|---|
PortalPaymentService | Orquestacion de pagos, webhooks, estado, comandos tipados | ≤6 |
PortalReciboService | Generacion de recibos PDF, consulta y validacion de config | ≤5 |
Criterio de corte
El corte se hizo por bounded context: pago y recibo son contextos distintos con ciclos de vida independientes. Un pago puede existir sin un recibo (estado APPROVED antes de la conciliacion). Un recibo no puede existir sin un pago aprobado. Esa asimetria indica que son responsabilidades separadas.
Comandos tipados introducidos
En lugar de pasar arrays asociativos o multiples parametros a los metodos del servicio, el refactor introdujo comandos tipados (value objects immutables):
IniciarPagoCommand-- encapsula los datos necesarios para iniciar un pago- Equivalentes para otros flujos del servicio
Esto hace que la firma de los metodos sea mas estable ante cambios futuros: agregar un campo al comando no rompe la firma del metodo.
Resultado
- Tests unitarios de
PortalPaymentService: de 11 mocks necesarios a ≤6 PortalReciboServicetesteable en completo aislamiento de la logica de pagos- Ver ADR-028
Referencias
- SOLID Principles - SRP: https://en.wikipedia.org/wiki/Single-responsibility_principle
- Orchestration vs Choreography: https://learn.microsoft.com/en-us/azure/architecture/patterns/choreography
- Service Layer Pattern: https://martinfowler.com/eaaCatalog/serviceLayer.html