Saltar al contenido principal

Anti-patrones Arquitectónicos ⚠️

Errores comunes en arquitectura de software.


1. Anemia del Dominio (Anemic Domain Model)

¿Qué es?

Entidades vacías con solo getters/setters. Toda la lógica en servicios.

// ❌ ANEMIA DEL DOMINIO
public class Order
{
public Guid Id { get; set; }
public Guid CustomerId { get; set; }
public List<OrderLineItem> LineItems { get; set; }
public OrderStatus Status { get; set; }
public decimal Total { get; set; }
// Solo data, sin lógica
}

public class OrderService
{
public void ProcessOrder(Order order)
{
// Toda la lógica aquí ❌
if (order.Total < 0) throw new Exception();
if (order.LineItems.Count == 0) throw new Exception();

if (order.Status != OrderStatus.Pending)
throw new Exception();

order.Status = OrderStatus.Confirmed;
// ... procesamiento
}
}

// Problemas:
// - Order no se puede usar sin OrderService
// - Lógica dispersa
// - Difícil de testear
// - Acoplamiento implícito

✅ Solución: Dominio Rico

public class Order
{
public Guid Id { get; private set; }
public Guid CustomerId { get; private set; }
public List<OrderLineItem> LineItems { get; private set; }
public OrderStatus Status { get; private set; }
public decimal Total { get; private set; }

// ✅ Lógica encapsulada en la entidad
public static Order CreateNew(CreateOrderDto dto)
{
if (string.IsNullOrEmpty(dto.CustomerId))
throw new ArgumentException("Customer required");

var order = new Order
{
Id = Guid.NewGuid(),
CustomerId = dto.CustomerId,
Status = OrderStatus.Pending
};

foreach (var item in dto.Items)
{
order.AddLineItem(item);
}

return order;
}

public void AddLineItem(OrderLineItemDto item)
{
if (item.Quantity <= 0)
throw new OrderException("Qty must > 0");

if (Status == OrderStatus.Shipped)
throw new OrderException("Cannot edit shipped order");

LineItems.Add(new OrderLineItem { ... });
RecalculateTotal();
}

public void Confirm()
{
if (Status != OrderStatus.Pending)
throw new OrderException("Only pending orders");

if (LineItems.Count == 0)
throw new OrderException("Order needs items");

Status = OrderStatus.Confirmed;
}

private void RecalculateTotal()
{
Total = LineItems.Sum(li => li.Total);
}
}

// ✅ Uso
var order = Order.CreateNew(dto); // Lógica encapsulada
order.AddLineItem(item); // Valida
order.Confirm(); // Confirma

Impacto:

  • ❌ Anemia: Code smell, difícil mantener, poco testeable
  • ✅ Dominio rico: Lógica clara, testeable, reutilizable

2. Feature Envy (Envidia de Características)

¿Qué es?

Una clase accede constantemente a métodos/datos de otra clase.

// ❌ FEATURE ENVY
public class OrderProcessor
{
public decimal CalculateTotalWithDiscount(Order order, Customer customer)
{
// Accesa muchos atributos de Order
decimal subtotal = order.LineItems.Sum(y => y.Price * y.Quantity);
decimal discount = 0;

// Accesa atributos de Customer (envidia)
if (customer.MembershipLevel == "Gold")
discount = subtotal * 0.20m;
else if (customer.MembershipLevel == "Silver")
discount = subtotal * 0.10m;

return subtotal - discount;
}
}

// Problema:
// - OrderProcessor "envidia" los datos de Order y Customer
// - Si Order o Customer cambian → rompe OrderProcessor

✅ Solución: Mover lógica a la clase adecuada

// ✅ La lógica pertenece a Order
public class Order
{
public decimal CalculateTotalWithDiscount(Customer customer)
{
var subtotal = LineItems.Sum(li => li.Total);
var discount = customer.CalculateDiscount(subtotal);
return subtotal - discount;
}
}

public class Customer
{
public decimal CalculateDiscount(decimal subtotal)
{
return MembershipLevel switch
{
"Gold" => subtotal * 0.20m,
"Silver" => subtotal * 0.10m,
_ => 0
};
}
}

// ✅ Uso
var total = order.CalculateTotalWithDiscount(customer);

3. God Object / Blob

¿Qué es?

Una clase hace todo: contiene 50+ propiedades, 100+ métodos.

// ❌ GOD OBJECT
public class OrderService
{
// Maneja órdenes
public void CreateOrder(CreateOrderDto dto) { }
public void UpdateOrder(...) { }
public void DeleteOrder(...) { }

// Maneja inventario
public void ReserveInventory(...) { }
public void ReleaseInventory(...) { }

// Maneja pagos
public void ProcessPayment(...) { }
public void RefundPayment(...) { }

// Envía emails
public void SendConfirmationEmail(...) { }
public void SendShippingEmail(...) { }

// Logging
public void LogOrderEvent(...) { }

// Cache
public void InvalidateOrderCache(...) { }

// ... 50 métodos más
}

// Problemas:
// - Una clase hace demasiado
// - Difícil de testear
// - Difícil de cambiar
// - Violación de SRP

✅ Solución: Dividir responsabilidades

// ✅ Clases especializadas
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IEmailService _email;

public async Task CreateAsync(CreateOrderDto dto)
{
var order = Order.CreateNew(dto);
await _repository.SaveAsync(order);
// Solo crea órdenes
}
}

public class InventoryService
{
public async Task ReserveAsync(Guid orderId, List<Item> items)
{
// Solo maneja inventario
}
}

public class PaymentService
{
public async Task ProcessAsync(Payment payment)
{
// Solo maneja pagos
}
}

public class OrderNotificationService
{
public async Task SendConfirmationAsync(Order order)
{
// Solo envía emails
}
}

public class CacheService
{
public void InvalidateOrder(Guid orderId)
{
// Solo maneja cache
}
}

4. Circular Dependencies (Dependencias Circulares)

¿Qué es?

A depende de B, B depende de A. Ciclo.

// ❌ CIRCULAR DEPENDENCY
using OrderService; // ProductService usa OrderService

public class OrderService
{
private ProductService _productService;

public OrderService(ProductService productService)
{
_productService = productService; // OrderService usa ProductService
}
}

public class ProductService
{
private OrderService _orderService;

public ProductService(OrderService orderService)
{
_orderService = orderService;
}
}

// Problemas:
// - No se puede instanciar (A necesita B, B necesita A)
// - Acoplamiento fuerte
// - Difícil testear

✅ Solución: Inyectar interfaz o inverter dependencia

// ✅ OPCIÓN 1: Interfaz
public interface IOrderService
{
Task<Order> GetAsync(Guid id);
}

public class OrderService : IOrderService
{
private readonly IProductService _productService;

public async Task<Order> GetAsync(Guid id)
{
// Usa interfaz, no clase concreta
}
}

public class ProductService : IProductService
{
private readonly IOrderService _orderService;

public async Task<Product> GetAsync(int id)
{
// Usa interfaz
}
}

// ✅ OPCIÓN 2: Event-based (desacoplado)
public class OrderService
{
private readonly IEventBus _eventBus;

public async Task CreateAsync(Order order)
{
// No llama ProductService directamente
await _eventBus.PublishAsync(new OrderCreatedEvent(order.Id));
}
}

public class ProductService
{
public async Task OnOrderCreated(OrderCreatedEvent @event)
{
// Reacciona al evento
}
}

5. Premature Optimization (Over-engineering)

¿Qué es?

Optimizar antes de validar si es necesario.

// ❌ OVER-ENGINEERING
// "Necesitaremos sharding en el futuro, así que voy a hacer..."

public interface IShardingStrategy { }
public class ConsistentHashSharding : IShardingStrategy { }
public class KetamaSharding : IShardingStrategy { }
public class RangeBasedSharding : IShardingStrategy { }

public class ShardingFactory
{
public IShardingStrategy Create(ShardingConfig config)
{
// Factory pattern con múltiples estrategias
}
}

// Resultado:
// - 1000+ líneas de código sin usar
// - Complejidad innecesaria
// - Tiempo desperdiciado
// - Mantenimiento extra

✅ Solución: YAGNI - You Aren't Gonna Need It

// ✅ SIMPLE
public class ProductRepository
{
private readonly AppDbContext _db;

public async Task<Product> GetAsync(int id)
{
return await _db.Products.FindAsync(id);
}
}

// CUANDO necesites sharding (en 2-3 años):
// Refactoriza en ese momento con datos reales

6. Tight Coupling (Acoplamiento Fuerte)

¿Qué es?

Las clases dependen directamente de implementaciones concretas.

// ❌ ACOPLAMIENTO FUERTE
public class OrderService
{
private readonly SmtpClient _smtpClient = new SmtpClient("smtp.gmail.com");
private readonly SqlConnection _connection = new SqlConnection("...");

public void SendConfirmation(Order order)
{
var message = new MailMessage(...);
_smtpClient.Send(message); // Acoplado a SMTP
}

public void Save(Order order)
{
var cmd = _connection.CreateCommand();
// Acoplado a SQL Server
}
}

// Problemas:
// - Imposible testear (requiere SMTP real, SQL Server real)
// - Cambiar a SendGrid requiere reescribir código
// - Cambiar a PostgreSQL requiere reescribir código

✅ Solución: Dependency Inversion

// ✅ DESACOPLADO
public interface IEmailProvider
{
Task SendAsync(Email email);
}

public interface IRepository<T>
{
Task SaveAsync(T entity);
}

public class OrderService
{
private readonly IEmailProvider _emailProvider;
private readonly IRepository<Order> _repository;

public OrderService(IEmailProvider emailProvider, IRepository<Order> repository)
{
_emailProvider = emailProvider;
_repository = repository;
}

public async Task SendConfirmationAsync(Order order)
{
await _emailProvider.SendAsync(...); // Interfaz
}

public async Task SaveAsync(Order order)
{
await _repository.SaveAsync(order); // Interfaz
}
}

// Para testing:
var mockProvider = new MockEmailProvider();
var mockRepository = new MockRepository<Order>();
var service = new OrderService(mockProvider, mockRepository);

7. CQRS Incorrecto

❌ Anti-patrón

// ❌ CQRS = Command Query Responsibility Segregation
// Pero muchos lo implementan así:

// Write DB: SUPER normalizado
CREATE TABLE orders_with_line_items_and_customer_details_and_prices (...)

// Read DB: SUPER desnormalizado
CREATE TABLE orders_denormalized_for_ui (...)

// Resultado:
// - Sincronización compleja
// - Eventual consistency confusa
// - No resuelve el problema real

✅ CQRS apropiado

// CQRS se justifica SOLO SI:
// - Patrones lectura/escritura son MUY diferentes
// - Necesitas múltiples vistas de los mismos datos
// - Escalabilidad asimétrica es crítica

// Si NO tienes eso:
// ✅ Usa un modelo simple (Read y Write idénticos)
// No es "CQRS" pero es suficiente

8. Microservicios Prematuros

❌ Anti-patrón

START_UP

"Vamos a usar Kubernetes, microservicios, event sourcing"

1 año después:
- 10 servicios con 2 desarrolladores
- No pueden desplegar nada
- Debugging imposible
- ❌ FRACASO

✅ Evolución correcta

START_UP (0-6 meses)
✅ Monolito
└─ Focus: MVP rápido

GROWTH (6-18 meses)
✅ Monolito modular (DDD)
└─ Equipos crecen, estructura mejora

SCALE (18+ meses)
✅ Considera CQRS, asincrónico
✅ Evalúa microservicios SI:
- Equipos >5 personas por dominio
- Escalabilidad diferenciada EVIDENCIA
- DevOps infraestructura MADURA

9. Ignorar Observabilidad

❌ Anti-patrón

// El código:

public void ProcessOrder(Order order)
{
// Lógica
_repository.Save(order);
// Nada de logs, metrics, traces
}

// En producción:
// "¿Por qué los órdenes no se procesan?"
// "No sé, no hay logs"
// "¿Cuál es el latency?"
// "No hay métricas"

✅ Observabilidad desde el inicio

public async Task ProcessOrderAsync(Order order)
{
using (_logger.BeginScope("OrderId: {0}", order.Id))
{
_metrics.IncrementOrdersStarted();

try
{
_logger.LogInformation("Processing order");
await _repository.SaveAsync(order);

_metrics.RecordOrderProcessingTime(stopwatch.Elapsed);
_logger.LogInformation("Order processed successfully");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order");
_metrics.IncrementOrdersFailed();
throw;
}
}
}

Checklist: Evitar Anti-patrones

□ ¿Hay entidades sin lógica? (Anemia)
└─ Mover lógica a la entidad

□ ¿Las clases acceden constantemente a otras? (Feature Envy)
└─ Mover método a la clase adecuada

□ ¿Una clase hace todo? (God Object)
└─ Dividir responsabilidades

□ ¿Hay dependencias circulares?
└─ Usar interfaces o events

□ ¿He optimizado sin medir?
└─ YAGNI: Simplificar primero

□ ¿Todo está acoplado a concretos?
└─ Inyectar interfaces

□ ¿Estoy en una startup con microservicios?
└─ Vuelve a monolito modular

□ ¿No hay logs/metrics?
└─ Agregar observabilidad

Resumen

Anti-patrónSíntomaSolución
AnemiaEntidades sin lógicaDominio rico
Feature EnvyA envidia métodos de BMover lógica a clase correcta
God Object100+ métodosDividir responsabilidades
Circular DepsA ↔ BInyectar interfaces
Over-engineeringCódigo sin usarYAGNI
Tight CouplingAcoplado a concretosDependency Inversion
CQRS incorrectoUsado equivocadamenteEvaluar si es realmente necesario
Micros prematurosComplejidad innecesariaMonolito modular primero
No observarNo sé qué passaLogs, metrics, traces

Última actualización: 2026-03-27