Antipatrones arquitectónicos frecuentes | Nicolás Garzón
Un antipatrón no es una tecnología prohibida. Es una solución o estructura recurrente que parece útil en cierto momento, pero produce consecuencias negativas previsibles cuando se aplica fuera de contexto o se deja crecer sin control.
Los antipatrones se diagnostican por sus causas y efectos, no por nombres llamativos.
Texto
Copiar presión o decisión inicial
→ estructura repetida
→ beneficio inmediato
→ costo acumulado
→ síntomas observables
→ intervenciónMicroservicios, una base compartida o una abstracción no son antipatrones por sí mismos. Se convierten en problema cuando contradicen drivers, ownership o capacidad operativa.
La corrección tampoco consiste en aplicar el patrón opuesto. Un Big Ball of Mud no se arregla automáticamente separándolo en cincuenta servicios; puede transformarse en un Distributed Monolith más costoso.
Para cada síntoma pregunta:
¿Qué comportamiento observamos?
¿Qué decisión o incentivo lo produce?
¿Qué riesgo crea?
¿Cómo lo comprobamos?
¿Cuál es el cambio mínimo que mejora la estructura?
¿Cómo evitamos que reaparezca?
Un sistema donde responsabilidades, dependencias y ownership no pueden reconocerse con claridad. Cualquier parte puede conocer o modificar cualquier otra.
Crecimiento rápido sin límites.
Controladores que acumulan lógica.
Modelos compartidos globalmente.
Atajos temporales que nunca se retiran.
Falta de pruebas y ownership.
Prioridad constante de features sin refactoring estructural.
Cambios pequeños modifican muchos archivos no relacionados.
Existen ciclos.
Nadie sabe dónde colocar una regla.
Múltiples formas de hacer la misma operación.
Los tests requieren levantar todo el sistema.
Incidentes tienen causas difíciles de localizar.
El lead time crece, el miedo a cambiar aumenta y cada nueva persona aprende excepciones en lugar de un modelo mental.
Grafo de dependencias.
Frecuencia de archivos que cambian juntos.
Ciclos.
Hotspots de código.
Entrevistas sobre ownership.
Análisis de incidentes.
No intentes “limpiar todo”.
Elige una capacidad que cambie con frecuencia.
Define su modelo y autoridad.
Crea una API pública interna.
Centraliza reglas y escrituras.
Migra consumidores.
Bloquea nuevas dependencias ilegales.
Repite.
Un conjunto de servicios desplegados por separado, pero acoplados de forma que deben desarrollarse, probarse o desplegarse juntos.
Texto
Copiar servicio A → B → C → D
↘ base compartida ↙
Dividir por capas técnicas.
Extraer código antes de definir ownership.
Compartir base de datos.
Contratos chatty.
Transacciones distribuidas implícitas.
Un solo equipo poseyendo todos los servicios.
Releases coordinados.
Ambientes completos para probar cualquier cambio.
Cascadas de fallos.
Versiones que deben ser compatibles en bloque.
Servicios que consultan tablas ajenas.
Una solicitud realiza una cadena larga de llamadas.
Se pagan red, observabilidad, pipelines y fallos parciales sin obtener autonomía.
Identificar capacidades y datos propios.
Reducir llamadas mediante operaciones de mayor nivel.
Eliminar escrituras cruzadas.
Permitir despliegues compatibles.
Reunir servicios que siempre cambian juntos si no existe razón de separación.
Volver a un monolito modular puede ser una mejora, no un fracaso.
Un componente central que conoce y coordina demasiadas capacidades.
ApplicationService con cientos de métodos.
Servicio “Core” del que dependen todos.
Gateway que ejecuta reglas de negocio.
Tabla settings que controla toda la aplicación.
Deseo de reutilización global.
Evitar decidir ownership.
Centralización gradual de excepciones.
Confundir coordinación con autoridad.
Muchos equipos modifican el mismo componente.
Cambios no relacionados generan conflictos.
Dependencias entrantes masivas.
Pruebas y despliegues lentos.
El módulo contiene vocabularios de varios dominios.
Clasifica responsabilidades por razón de cambio. Extrae capacidades con modelo y datos propios. Mantén un orquestador solo cuando el flujo realmente necesita coordinación; no debe poseer reglas de cada participante.
Múltiples módulos o servicios utilizan las mismas tablas como contrato de integración, especialmente si todos pueden escribirlas.
Joins simples.
Transacciones locales.
Menos APIs.
Desarrollo rápido.
Una base compartida dentro de un monolito no es incorrecta. El antipatrón es ownership implícito o múltiple.
Cambiar una columna rompe varios componentes.
No se puede saber quién escribe.
Reglas duplicadas.
Migraciones coordinadas.
Reportes dependen de internals.
Asignar autoridad por datos.
Revocar escrituras ajenas.
Exponer operaciones o eventos.
Crear proyecciones para lectura.
Usar schemas y permisos como defensa.
Migrar incrementalmente.
Una interacción necesita muchas llamadas pequeñas para completar una intención.
Texto
Copiar getOrder
→ getCustomer
→ getItems
→ getInventory por item
→ getPrice por item
API diseñada como acceso remoto a objetos.
Límites incorrectos.
Operaciones CRUD demasiado pequeñas.
Datos requeridos repartidos sin considerar el flujo.
Latencia acumulada.
Más puntos de fallo.
Retries complejos.
Acoplamiento temporal.
Carga de red.
Diseña operaciones por intención o vistas agregadas. Usa batching, composición o proyecciones cuando corresponda. No muevas simplemente las mismas llamadas a GraphQL sin resolver ownership y costo.
Componentes deben ejecutarse en un orden o momento específico aunque el contrato no lo hace explícito.
Texto
Copiar crear usuario
→ esperar 2 segundos
→ consultar perfil
→ iniciar configuración
Sleeps.
Jobs que “normalmente ya terminaron”.
Fallos intermitentes.
Estados incompletos sin representar.
Modela estados, eventos, confirmaciones o workflows. Haz visible cuándo una operación está pending, completed o failed.
Varias capas reintentan una dependencia degradada y multiplican la carga.
Texto
Copiar cliente 3 intentos
× gateway 3
× servicio 3
= hasta 27 llamadasUna degradación parcial se convierte en colapso.
Un responsable claro de retry.
Presupuesto total.
Backoff y jitter.
Circuit breaker.
Load shedding.
Idempotencia.
Aumentar timeouts suele retrasar el colapso, no resolverlo.
Aplicar la tecnología conocida a problemas diferentes porque el equipo domina esa herramienta.
Broker para todo tipo de comunicación.
Base documental para cualquier dato.
Kubernetes para una aplicación pequeña.
Eventos para consultas que necesitan respuesta inmediata.
La justificación describe la herramienta, no el problema. No se comparan alternativas ni trade-offs.
Volver a drivers, ejecutar spikes y permitir un conjunto tecnológico pequeño pero no dogmático.
Copiar prácticas de otra organización sin copiar su contexto, escala, equipos ni capacidades.
Microservicios porque una big tech los usa.
CQRS en cada módulo.
Event Sourcing sin necesidad de historial como fuente.
Data mesh sin ownership de datos.
Ceremonia, operación y vocabulario que no compran una capacidad real.
Driver.
Escenario.
Alternativas.
Costo.
Evidencia.
Señal de revisión.
Una interfaz pretende ocultar un mecanismo, pero los consumidores necesitan conocer sus detalles.
TypeScript
Copiar paymentPort. charge ( {
stripePaymentMethodId,
stripeConnectedAccount,
} ) ; Aunque se llame PaymentPort, el dominio depende del proveedor.
Abstracción creada después de que los tipos ya se filtraron.
Intentar un wrapper uno a uno.
Diferencias semánticas ignoradas.
Modelar conceptos del dominio, traducir en adapters y reconocer capacidades no portables. Una abstracción honesta no promete reemplazo gratuito.
El sistema publica eventos sin ownership, schemas ni garantías, y termina usando el broker como base de datos informal.
Eventos ambiguos como DataUpdated.
Consumidores dependen de campos internos.
Nadie sabe qué eventos son públicos.
No existe versionado.
Replay produce efectos duplicados.
Se necesita observar el broker para entender estado.
Eventos con significado de dominio.
Contratos y ownership.
Outbox.
Idempotencia.
Versionado.
Catálogo de eventos.
Separar hechos públicos de detalles internos.
Objetos de dominio solo contienen datos y todas las reglas viven en servicios procedurales.
No todo CRUD necesita un modelo rico. El antipatrón aparece cuando existen invariantes importantes y se duplican fuera de la entidad o agregado.
Setters públicos para estados sensibles.
Servicios gigantes.
Reglas repetidas.
Objetos que pueden construirse inválidos.
Mover invariantes y transiciones al modelo que posee el estado. Mantener orquestación externa y mecanismos fuera.
Diseñar abstracciones y extensibilidad para futuros hipotéticos sin evidencia.
Muchos interfaces con una implementación estable.
Framework interno antes del segundo caso.
Configuración para posibilidades no solicitadas.
Tiempo dedicado a escenarios sin driver.
Más navegación, bugs y mantenimiento hoy; el futuro real puede necesitar otra abstracción.
Diseñar para cambio probable y preservar seams. Elegir decisiones reversibles. Introducir generalización después de observar variación real.
El opuesto también existe: ignorar riesgos conocidos por “mantenerlo simple”.
Dinero sin idempotencia.
Multi-tenancy sin constraints.
Operaciones críticas sin auditoría.
Base sin backups probados.
La simplicidad debe ser suficiente para los drivers, no ausencia de diseño.
Separar procesos antes de necesitar independencia de despliegue, escala o ownership.
Se introducen red, consistencia eventual, observabilidad y operación.
Fortalecer módulos dentro del proceso y extraer cuando existan señales reales.
Adoptar una dependencia estratégica sin comprender datos, semántica, precio y salida.
Tipos del proveedor en todo el dominio.
No existe exportación probada.
Costos no medidos.
Cambiar parece imposible, pero nunca se registró la decisión.
Contrato propio donde aporte valor, due diligence, observabilidad, triggers y estrategia de migración.
Recolectar logs y dashboards sin capacidad de responder preguntas operativas.
Alertas no accionables.
Métricas sin ownership.
Logs sin correlación.
Incidentes diagnosticados mediante búsquedas manuales largas.
Empezar por journeys y preguntas: impacto, tenant, dependencia, estado y acción. Diseñar SLI, trazas y runbooks.
Concentrar autenticación o validación en gateway y asumir que el tráfico interno es confiable.
Una ruta interna, job o componente comprometido puede saltarse controles.
Defensa en profundidad: identidad, autorización en la capacidad, scope de datos, permisos y auditoría.
Mapa de dependencias.
Cambios conjuntos.
Latencia por hop.
Propietarios de datos.
Incidentes.
Tiempo de entrega.
Despliegues coordinados.
Uso de fallback.
Excepciones y bypasses.
Después conecta síntoma con causa. Un endpoint lento puede deberse a chatty interface, query sin índice, cola o proveedor; “añadir caché” sin diagnóstico puede introducir otro problema.
Síntoma: confirmar una venta requiere llamadas a Pedidos, Inventario, Precios, Clientes y Notificaciones.
Boundaries por entidades CRUD.
Operación de venta no modelada como flujo.
Inventario remoto demasiado temprano.
Notificación en camino crítico.
Definir escenario y presupuesto.
Mantener invariantes de venta e inventario en una transacción local si el contexto lo permite.
Obtener pricing mediante una operación agregada.
Publicar notificación después de confirmar mediante outbox.
Medir p95, conflictos y errores.
No se corrige añadiendo retries a todas las llamadas.
Drivers y escenarios claros.
ADR con alternativas.
Ownership de datos.
Fitness functions.
Revisiones de arquitectura.
Métricas de cambio y operación.
Refactoring incremental.
Retiro de soluciones temporales.
Los antipatrones reaparecen cuando los incentivos que los crearon permanecen.
Un antipatrón depende del contexto y sus consecuencias.
Diagnostica causas antes de aplicar patrones.
Distribuir un mal diseño suele hacerlo más costoso.
Ownership implícito produce muchos problemas recurrentes.
Las soluciones temporales necesitan retiro.
Simplicidad no significa ignorar riesgos.
La corrección debe ser incremental y verificable.
¿Qué diferencia existe entre una base compartida y Shared Database Integration?
¿Cómo distinguirías microservicios reales de un Distributed Monolith?
¿Por qué una abstracción con interfaz puede seguir siendo leaky?
¿Qué causa una retry storm?
¿Cómo corregirías un Big Ball of Mud sin reescribir todo?
Ver respuestas orientativas
La segunda implica ownership y escrituras cruzadas; compartir infraestructura no es suficiente.
Por despliegues, datos, contratos y ownership realmente independientes.
Porque sus tipos y semántica pueden seguir exponiendo el mecanismo.
Retries multiplicados en varias capas sin presupuesto ni control.
Seleccionando capacidades, creando seams, migrando dependencias y aplicando fitness functions.
Modelo mental completo de Software Architecture integra drivers, boundaries, datos, comunicación, operación, equipos y evolución en un proceso de decisión coherente.