Anti-patrones en Arquitectura de Software

Errores comunes en arquitectura de software distribuida: cómo identificarlos, su impacto real y las estrategias para corregirlos.

Qué son los anti-patrones

Un anti-patrón es una solución recurrente que parece razonable al principio pero que genera problemas significativos a mediano y largo plazo. A diferencia de un simple error, los anti-patrones son trampas comunes en las que caen equipos experimentados, muchas veces por presión de tiempo, falta de contexto o decisiones que fueron correctas en un momento pero dejaron de serlo.

Reconocer estos anti-patrones a tiempo te permite evitar deuda técnica costosa y tomar decisiones arquitectónicas más informadas.

Por qué surgen los anti-patrones

Los anti-patrones no aparecen por incompetencia. Surgen por razones comprensibles:

CausaEjemplo
Presión de tiempo”Agreguemos esta funcionalidad al servicio existente, no hay tiempo para crear uno nuevo”
Falta de contexto”Todos los servicios pueden leer de esta base de datos, es más fácil”
Decisiones que envejecen”Cuando éramos 3 desarrolladores esto funcionaba bien, ahora somos 30”
Optimización prematura”Separemos todo en microservicios desde el día 1”
Inercia organizacional”Siempre lo hemos hecho así”

1. Monolito Distribuido (Distributed Monolith)

Problema

Ocurre cuando un sistema se divide en múltiples servicios que dependen tan fuertemente entre sí que no pueden desplegarse, escalarse ni evolucionar de forma independiente. En la práctica, tienes la complejidad operativa de los microservicios sin ninguno de sus beneficios.

┌──────────┐     ┌──────────┐     ┌──────────┐
│Servicio A│────►│Servicio B│────►│Servicio C│
│          │◄────│          │◄────│          │
└──────────┘     └──────────┘     └──────────┘
     │                │                │
     └────────────────┴────────────────┘
        Despliegue conjunto obligatorio
        Modelos de datos compartidos
        Llamadas síncronas en cadena

Las señales más claras son:

  • Necesitas desplegar varios servicios a la vez para que un cambio funcione
  • Un cambio en el contrato de un servicio rompe a los demás
  • Los servicios comparten librerías internas con lógica de negocio
  • Los tests de integración requieren levantar todo el sistema

Impacto

  • Despliegues frágiles: Un cambio en un servicio requiere coordinar el despliegue de otros, eliminando la agilidad que motivó la separación
  • Fallos en cascada: La dependencia síncrona entre servicios hace que la caída de uno arrastre a los demás
  • Complejidad sin beneficio: El equipo paga el costo operativo de múltiples servicios sin ganar independencia real
  • Velocidad de desarrollo reducida: Los equipos no pueden trabajar en paralelo porque los cambios se bloquean mutuamente

Cómo detectarlo

  • Pregunta: ¿puedo desplegar este servicio sin tocar ningún otro? Si la respuesta es “no” con frecuencia, tienes un monolito distribuido
  • Revisa si los servicios comparten esquemas de base de datos o librerías con lógica de dominio
  • Observa si los releases requieren coordinación entre múltiples equipos para un solo cambio funcional
  • Mide el coupling score: cuenta cuántos servicios se despliegan juntos en cada release

Corrección

EstrategiaDescripción
Definir contratos clarosUsar APIs versionadas con contratos explícitos. Cada servicio expone una interfaz estable y evoluciona internamente sin romper consumidores
Comunicación asíncronaReemplazar llamadas síncronas en cadena por eventos. Publicar un evento en lugar de hacer una llamada directa
Bounded Contexts (DDD)Aplicar DDD para identificar los límites reales del dominio. Cada servicio debe ser dueño de su contexto
Evaluar re-consolidaciónSi los servicios están tan acoplados que no pueden separarse, considera volver a unirlos. Un monolito bien diseñado es mejor que un monolito distribuido
Anti-Corruption LayerUsar ACL entre servicios para traducir modelos y reducir el acoplamiento directo

2. Base de Datos Compartida (Shared Database)

Problema

Múltiples servicios leen y escriben directamente en la misma base de datos, accediendo a las mismas tablas. Esto crea un acoplamiento invisible: los servicios no dependen de APIs explícitas sino de la estructura interna de las tablas.

┌──────────┐   ┌──────────┐   ┌──────────┐
│Servicio A│   │Servicio B│   │Servicio C│
└────┬─────┘   └────┬─────┘   └────┬─────┘
     │              │              │
     ▼              ▼              ▼
┌──────────────────────────────────────┐
│         Base de Datos Compartida     │
│                                      │
│  ┌─────────┐ ┌─────────┐ ┌────────┐ │
│  │ Tabla X │ │ Tabla Y │ │ Tabla Z│ │
│  └─────────┘ └─────────┘ └────────┘ │
│                                      │
│  Todos leen y escriben en todas      │
└──────────────────────────────────────┘

Las señales más claras son:

  • Varios servicios hacen queries directos a las mismas tablas
  • Un cambio de esquema requiere modificar múltiples servicios
  • No hay un “dueño” claro de cada tabla o conjunto de datos
  • Las migraciones de base de datos son eventos de alto riesgo

Impacto

  • Acoplamiento oculto: Los servicios parecen independientes pero están atados por la estructura de la base de datos. Un ALTER TABLE puede romper servicios que ni siquiera sabías que usaban esa tabla
  • Escalabilidad limitada: No puedes escalar la base de datos de forma independiente por servicio
  • Migraciones peligrosas: Cualquier cambio de esquema es un riesgo porque no sabes quién depende de qué
  • Imposibilidad de evolucionar: Cada servicio queda atado a la tecnología y estructura de la base de datos compartida

Cómo detectarlo

  • Revisa si más de un servicio tiene connection strings a la misma base de datos
  • Busca queries que acceden a tablas que “pertenecen” a otro dominio funcional
  • Pregunta: ¿quién es el dueño de esta tabla? Si la respuesta es “varios servicios”, tienes este anti-patrón
  • Ejecuta un análisis de dependencias: mapea qué servicios acceden a qué tablas

Corrección

EstrategiaDescripción
Database per ServiceCada servicio tiene su propia base de datos. Solo el servicio dueño puede leer y escribir sus datos
Exponer datos vía APISi el Servicio B necesita datos del Servicio A, los solicita a través de la API de A
Eventos para sincronizaciónUsar eventos de dominio para que los servicios mantengan copias locales de los datos que necesitan
Outbox PatternPublicar eventos de forma confiable cuando los datos cambian, para que otros servicios se sincronicen
Migración incrementalNo intentes separar todo de golpe. Identifica las tablas más problemáticas y migra servicio por servicio

3. God Service (Servicio Dios)

Problema

Un solo servicio concentra demasiadas responsabilidades y se convierte en el punto central por el que pasa casi toda la lógica del sistema. Suele empezar como un servicio legítimo que va acumulando funcionalidades porque “es más fácil agregarlo aquí”.

                    ┌─────────────────────┐
                    │    God Service       │
                    │                     │
                    │  • Autenticación    │
                    │  • Usuarios         │
                    │  • Pedidos          │
                    │  • Pagos            │
                    │  • Notificaciones   │
                    │  • Reportes         │
                    │  • Inventario       │
                    └──────────┬──────────┘

              ┌────────────────┼────────────────┐
              ▼                ▼                ▼
        ┌──────────┐   ┌──────────┐    ┌──────────┐
        │Servicio X│   │Servicio Y│    │Servicio Z│
        │(simple)  │   │(simple)  │    │(simple)  │
        └──────────┘   └──────────┘    └──────────┘

        Todos dependen del God Service

Las señales más claras son:

  • Un servicio tiene significativamente más endpoints que los demás
  • La mayoría de los cambios funcionales requieren modificar ese servicio
  • El servicio tiene múltiples responsabilidades no relacionadas entre sí
  • El equipo que lo mantiene es un cuello de botella constante

Impacto

  • Cuello de botella de desarrollo: Todos los equipos necesitan modificar el mismo servicio
  • Punto único de fallo: Si el God Service cae, gran parte del sistema deja de funcionar
  • Escalabilidad desigual: No puedes escalar solo pagos si está empaquetado con reportes
  • Complejidad creciente: El servicio se vuelve difícil de entender, testear y mantener
  • Despliegues riesgosos: Cada despliegue toca muchas funcionalidades

Cómo detectarlo

  • Mide las líneas de código o endpoints por servicio. Si uno tiene 10x más que el promedio, es sospechoso
  • Revisa los commits: si un servicio concentra la mayoría de los cambios de múltiples equipos, probablemente es un God Service
  • Pregunta: ¿puedo describir la responsabilidad de este servicio en una frase? Si necesitas una lista larga, tiene demasiadas responsabilidades
  • Analiza las dependencias: si la mayoría de los servicios dependen de uno solo, es un God Service

Corrección

EstrategiaDescripción
Identificar bounded contexts (DDD)Mapear los dominios reales dentro del God Service. Cada dominio es candidato a ser un servicio independiente
Strangler Fig PatternExtraer funcionalidades una por una, creando nuevos servicios que interceptan las llamadas al God Service
Principio de responsabilidad únicaCada servicio debe tener una razón clara para existir
Priorizar por impactoEmpieza extrayendo la funcionalidad que cambia con más frecuencia o que tiene requisitos de escalabilidad diferentes
API Gateway / BFFUsar un gateway para enrutar requests a servicios especializados en lugar de al God Service

4. Servicios Parlanchines (Chatty Services)

Problema

Los servicios se comunican entre sí con demasiadas llamadas de granularidad fina para completar una sola operación. En lugar de una solicitud que obtenga toda la información necesaria, un servicio hace múltiples llamadas pequeñas.

Servicio A necesita mostrar el perfil de un usuario:

  Servicio A → Servicio Usuarios: GET /users/123
  Servicio A → Servicio Usuarios: GET /users/123/preferences
  Servicio A → Servicio Pedidos:  GET /orders?user=123
  Servicio A → Servicio Pedidos:  GET /orders/456/details
  Servicio A → Servicio Pedidos:  GET /orders/789/details
  Servicio A → Servicio Pagos:    GET /payments?user=123
  Servicio A → Servicio Reviews:  GET /reviews?user=123

  Total: 7 llamadas de red para una sola pantalla

Las señales más claras son:

  • Una operación de negocio genera más de 3-4 llamadas entre servicios
  • La latencia es alta aunque cada servicio individual responde rápido
  • Los logs muestran ráfagas de llamadas correlacionadas para un solo request

Impacto

  • Latencia acumulada: 7 llamadas de 50ms cada una suman 350ms en el mejor caso (secuencial)
  • Fragilidad: Con 7 llamadas y 99% de disponibilidad por servicio, la disponibilidad efectiva baja a ~93%
  • Carga de red excesiva: El tráfico interno puede superar al tráfico externo
  • Dificultad para depurar: Rastrear un error a través de múltiples llamadas requiere trazabilidad distribuida

Cómo detectarlo

  • Cuenta las llamadas entre servicios que genera un solo request del usuario
  • Mide la latencia end-to-end vs. la latencia individual de cada servicio
  • Revisa si los servicios exponen endpoints muy granulares

Corrección

EstrategiaDescripción
BFF (Backend for Frontend)Crear un servicio intermedio que agregue datos de múltiples servicios en una sola respuesta
Endpoints agregadosDiseñar endpoints que devuelvan toda la información necesaria para un caso de uso
Caché localMantener copias locales de datos que cambian poco
Llamadas en paraleloSi las llamadas son independientes, ejecutarlas en paralelo
Eventos para datos derivadosUsar eventos para que cada servicio mantenga una vista materializada de los datos que necesita

5. Modelo Anémico (Anemic Domain Model)

Problema

Las entidades del dominio son contenedores de datos sin comportamiento — solo tienen getters y setters. Toda la lógica de negocio vive en servicios externos que manipulan estas entidades pasivas.

// ❌ Modelo anémico
class Pedido {
  id: string;
  estado: string;
  líneas: LíneaPedido[];
  total: number;
  // Solo getters y setters, sin lógica
}

class PedidoService {
  confirmar(pedido: Pedido) {
    if (pedido.estado !== 'borrador') throw new Error('...');
    if (pedido.líneas.length === 0) throw new Error('...');
    pedido.estado = 'confirmado';
    pedido.total = this.calcularTotal(pedido.líneas);
    // Toda la lógica está aquí, no en Pedido
  }
}
// ✅ Modelo rico
class Pedido {
  confirmar() {
    if (this.estado !== 'borrador')
      throw new Error('Solo se puede confirmar un pedido en borrador');
    if (this.líneas.length === 0)
      throw new Error('El pedido debe tener al menos una línea');
    this.estado = 'confirmado';
    this.total = this.calcularTotal();
    this.registrarEvento(new PedidoConfirmado(this.id));
  }
}

Impacto

  • Lógica dispersa: Las reglas de negocio se duplican en múltiples servicios
  • Difícil de testear: Los tests necesitan configurar servicios externos para validar reglas simples
  • Violación de encapsulamiento: Cualquiera puede modificar el estado de la entidad sin pasar por las reglas
  • Difícil de descubrir: No es obvio qué operaciones son válidas sobre una entidad

Corrección

EstrategiaDescripción
Mover lógica a las entidadesLas reglas de negocio que operan sobre los datos de una entidad deben vivir en la entidad
Aplicar DDDUsar agregados con invariantes que protejan la consistencia
Value ObjectsEncapsular conceptos como Dinero, Email, Dirección con validación propia
Domain EventsLas entidades publican eventos cuando cambian de estado

6. Integración por Base de Datos (Database Integration)

Problema

Los servicios se integran leyendo y escribiendo en tablas de otros servicios en lugar de comunicarse por APIs o eventos. Es una variante más sutil de la base de datos compartida.

Servicio A necesita saber si un usuario es premium:

  ❌ SELECT tipo FROM usuarios WHERE id = 123
     (accede directamente a la tabla del Servicio B)

  ✅ GET /api/users/123/subscription
     (consulta al Servicio B por su API)

Impacto

  • Acoplamiento invisible: No hay contrato explícito, solo conocimiento implícito de la estructura de tablas
  • Imposible versionar: No puedes evolucionar el esquema sin romper a los consumidores ocultos
  • Sin control de acceso: Cualquier servicio puede leer o escribir cualquier dato

Corrección

EstrategiaDescripción
APIs explícitasCada servicio expone una API para acceder a sus datos
Eventos de dominioPublicar cambios como eventos para que otros servicios se suscriban
ACL PatternCrear una capa de traducción entre el modelo interno y los consumidores

Resumen comparativo

Anti-patrónSeñal principalRiesgo mayorPrimera acción
Monolito DistribuidoNo puedes desplegar un servicio soloComplejidad sin beneficioDefinir contratos y bounded contexts
Base de Datos CompartidaVarios servicios acceden a las mismas tablasMigraciones peligrosasAsignar ownership de tablas
God ServiceUn servicio concentra toda la lógicaPunto único de falloIdentificar dominios para extraer
Servicios ParlanchinesMuchas llamadas para una operaciónLatencia y fragilidadCrear endpoints agregados o BFF
Modelo AnémicoEntidades sin comportamientoLógica dispersa y duplicadaMover lógica a las entidades (DDD)
Integración por BDQueries directos a tablas de otros serviciosAcoplamiento invisibleExponer APIs explícitas

Cómo prevenir anti-patrones

Los anti-patrones rara vez aparecen de golpe. Se acumulan gradualmente. Algunas prácticas que ayudan a prevenirlos:

  • Revisiones de arquitectura periódicas: Evalúa regularmente si los servicios mantienen su independencia
  • Fitness functions: Define métricas automatizadas que detecten acoplamiento (número de dependencias, tamaño de servicios, frecuencia de despliegues conjuntos)
  • ADRs (Architecture Decision Records): Documenta las decisiones arquitectónicas y sus razones
  • Ownership claro: Cada servicio, tabla y API debe tener un equipo responsable
  • Principio de la navaja: Si no puedes justificar por qué un servicio es independiente, probablemente no debería serlo
  • Monitoreo de acoplamiento: Mide cuántos servicios se despliegan juntos, cuántas llamadas síncronas hay entre servicios, y cuántas tablas se comparten

Relación con otros patrones

Los anti-patrones se previenen y corrigen aplicando los patrones correctos:

Anti-patrón                    Patrón que lo corrige
─────────────────────────────────────────────────────
Monolito Distribuido    →  DDD (Bounded Contexts)
                        →  Event-Driven Architecture
                        →  ACL Pattern

Base de Datos Compartida →  Database per Service
                         →  Outbox Pattern (sincronización)
                         →  Event-Driven Architecture

God Service             →  Microservices Pattern
                        →  DDD (descomposición por dominio)
                        →  API Gateway / BFF

Servicios Parlanchines  →  BFF Pattern
                        →  API Gateway Pattern
                        →  Event-Driven Architecture

Modelo Anémico          →  DDD (diseño táctico)
                        →  Aggregates + Domain Events

Integración por BD      →  ACL Pattern
                        →  Outbox Pattern
                        →  Shared Kernel (controlado)

Próximos pasos

Ahora que conoces los anti-patrones más comunes, profundiza en los patrones que los previenen: DDD para definir bounded contexts claros, Microservices Pattern para descomponer correctamente, Event-Driven Architecture para desacoplar servicios, y BFF Pattern para resolver el problema de los servicios parlanchines.