SOLID en Microservicios: Cuándo Aplicar y Cuándo Es Excesivo
Los principios SOLID son el dogma de la arquitectura de software moderna. Pero aplicarlos ciegamente en microservicios puede llevar a sobre-ingeniería, complejidad innecesaria y sistemas difíciles de mantener. La clave está en entender cuándo pragmatismo vence principios.
El Dogma SOLID
Todo desarrollador senior conoce los principios SOLID:
- Single Responsibility Principle (SRP)
- Open/Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
El problema surge cuando aplicamos estos principios religiosamente en cada componente de un microservicio.
El Exceso de Abstracción
Ejemplo: Un Servicio de Usuarios Sobre-Ingenierado
# ❌ SOLID excesivo: 8 archivos para un CRUD de usuarios
# interfaces/user_repository.py
class UserRepositoryInterface(ABC):
@abstractmethod
async def save(self, user: User) -> None: pass
@abstractmethod
async def find_by_id(self, id: UserId) -> Optional[User]: pass
# repositories/postgresql_user_repository.py
class PostgresqlUserRepository(UserRepositoryInterface):
async def save(self, user: User) -> None:
# PostgreSQL implementation
pass
# repositories/mongodb_user_repository.py
class MongodbUserRepository(UserRepositoryInterface):
async def save(self, user: User) -> None:
# MongoDB implementation
pass
# repositories/cache_user_repository.py
class CacheUserRepository(UserRepositoryInterface):
def __init__(self, inner: UserRepositoryInterface):
self.inner = inner
async def save(self, user: User) -> None:
await self.inner.save(user)
# Cache logic
pass
# services/user_service.py
class UserService:
def __init__(self, repo: UserRepositoryInterface):
self.repo = repo
async def create_user(self, dto: CreateUserDTO) -> User:
# Business logic
pass
# dtos/create_user_dto.py
class CreateUserDTO(BaseModel):
email: str
name: str
# validators/email_validator.py
class EmailValidator:
def validate(self, email: str) -> bool:
# Email validation logic
pass
# handlers/create_user_handler.py
class CreateUserHandler:
async def handle(self, command: CreateUserCommand) -> User:
# Command handling
pass
Para un microservicio que hace CRUD de usuarios, tienes 8 archivos, múltiples interfaces y capas de abstracción. Cambiar una validación de email requiere tocar 4 archivos. Tu equipo junior tarda 2 días en entender el código.
Versión Pragmática
# ✅ Pragmático: 1 archivo, funcionalidad clara
# users.py
from pydantic import BaseModel, EmailStr
from fastapi import FastAPI, Depends
from sqlalchemy.ext.asyncio import AsyncSession
class CreateUserRequest(BaseModel):
email: EmailStr
name: str
class UserResponse(BaseModel):
id: int
email: str
name: str
app = FastAPI()
@app.post("/users", response_model=UserResponse)
async def create_user(
request: CreateUserRequest,
db: AsyncSession = Depends(get_db)
):
# Validación inline (suficiente para 95% de casos)
if "@" not in request.email:
raise HTTPException(400, "Invalid email")
# Business logic inline
existing = await db.execute(
select(User).where(User.email == request.email)
)
if existing.scalar_one_or_none():
raise HTTPException(409, "Email already exists")
user = User(email=request.email, name=request.name)
db.add(user)
await db.commit()
await db.refresh(user)
return user
| ✓Enfoque SOLID Extremo | Enfoque Pragmático |
|---|---|
| 8 archivos para un CRUD | 1 archivo, funcionalidad completa |
| Cambio = tocar 4 archivos | Cambio = tocar 1 archivo |
| Curva de aprendizaje alta | Cualquier dev lo entiende en 5 min |
| Testing requiere mocks complejos | Testing simple con fixtures |
| Abstracciones nunca usadas | Solo lo que necesitas |
Cuándo SOLID Importa
SOLID no es inútil. Importa en contextos específicos:
1. Dominio Complejo
SOLID vale la pena cuando tu dominio tiene reglas de negocio complejas que cambiarán frecuentemente.
# ✅ SOLID en dominio complejo: Facturación con múltiples estrategias
from abc import ABC, abstractmethod
class TaxCalculator(ABC):
@abstractmethod
def calculate(self, amount: Decimal, country: str) -> Decimal: pass
class USATaxCalculator(TaxCalculator):
def calculate(self, amount: Decimal, country: str) -> Decimal:
# USA-specific tax rules (sales tax, state tax, etc.)
return amount * Decimal("0.08")
class EUVATCalculator(TaxCalculator):
def calculate(self, amount: Decimal, country: str) -> Decimal:
# EU VAT rules (different per country)
vat_rates = {"ES": 0.21, "DE": 0.19, "FR": 0.20}
return amount * Decimal(str(vat_rates.get(country, 0.21)))
class BrazilICMSCalculator(TaxCalculator):
def calculate(self, amount: Decimal, country: str) -> Decimal:
# Brazil ICM rules (complex state-specific rules)
# Simplified for example
return amount * Decimal("0.17")
class InvoiceService:
def __init__(self, tax_calculator: TaxCalculator):
self.tax_calculator = tax_calculator
def create_invoice(self, amount: Decimal, country: str):
subtotal = amount
tax = self.tax_calculator.calculate(amount, country)
total = subtotal + tax
return Invoice(subtotal=subtotal, tax=tax, total=total)
# Factory para crear el calculator correcto
def get_tax_calculator(country: str) -> TaxCalculator:
calculators = {
"US": USATaxCalculator(),
"ES": EUVATCalculator(),
"DE": EUVATCalculator(),
"BR": BrazilICMSCalculator(),
}
return calculators.get(country, EUVATCalculator())
2. Sistemas Requeridos para Compliance
En sistemas bancarios, de salud o con regulaciones estrictas, SOLID y Clean Architecture son obligatorios:
# ✅ SOLID en sistema compliance: Auditoría bancaria
class TransactionRepository(ABC):
@abstractmethod
async def save(self, transaction: Transaction) -> None: pass
class AuditLogRepository(ABC):
@abstractmethod
async def log(self, event: AuditEvent) -> None: pass
class ComplianceService:
def __init__(
self,
tx_repo: TransactionRepository,
audit_repo: AuditLogRepository
):
self.tx_repo = tx_repo
self.audit_repo = audit_repo
async def process_transaction(self, tx: Transaction):
# DIP: dependemos de abstracciones, no de implementaciones
await self.tx_repo.save(tx)
# LSP: podemos cambiar AuditLogRepository sin romper
await self.audit_repo.log(AuditEvent(
type="TRANSACTION_PROCESSED",
data=tx.to_dict(),
timestamp=datetime.now()
))
3. Microservicios que Evolucionan Rápidamente
Si tu microservicio cambiará de DB, añadirá nuevas features de pago, integrará con múltiples proveedores, SOLID ayuda:
# ✅ SOLID en servicio evolutivo: Integración con múltiples proveedores de pagos
class PaymentGateway(ABC):
@abstractmethod
async def charge(self, amount: Decimal, token: str) -> ChargeResult: pass
class StripePaymentGateway(PaymentGateway):
async def charge(self, amount: Decimal, token: str) -> ChargeResult:
# Stripe implementation
pass
class PayPalPaymentGateway(PaymentGateway):
async def charge(self, amount: Decimal, token: str) -> ChargeResult:
# PayPal implementation
pass
class PaymentService:
def __init__(self, gateway: PaymentGateway):
self.gateway = gateway
async def process_payment(self, amount: Decimal, token: str):
# OCP: Open for extension, closed for modification
# Podemos añadir nuevos gateways sin tocar PaymentService
result = await self.gateway.charge(amount, token)
return result
Matriz de Decisión
| ✓Contexto | SOLID Recomendado | Pragmatismo Preferido |
|---|---|---|
| Dominio complejo (facturación, compliance) | ✅ SOLID | ❌ |
| Microservicio CRUD simple | ❌ | ✅ Pragmático |
| Equipo junior en crecimiento | ⚠️ Balanceado | ✅ Más pragmático |
| Requerimientos cambiantes frecuentes | ✅ SOLID | ⚠️ Balanceado |
| Time-to-market crítico (startup) | ❌ | ✅ Pragmático |
| Sistema regulatorio (banca, salud) | ✅ SOLID | ❌ |
Trade-offs: Cuándo Escoger Qué
Trade-off 1: Velocidad vs Mantenibilidad
Startup: Escoge pragmatismo
# 3 meses construyendo infraestructura
# 0 features shippeadas
# Equipo frustrado
class AbstractFactory(ABC):
@abstractmethod
def create(self) -> Any: pass
class UserRepositoryFactory(AbstractFactory):
def create(self) -> UserRepository:
return PostgresqlUserRepository(...)
# Y así para cada entidad...Scale-up: Introduce SOLID selectivamente
Cuando tengas 50+ endpoints, 5 devs trabajando en el microservicio, introduce SOLID en áreas críticas.
Trade-off 2: Abstracción vs Complejidad Cognitiva
# ❌ SOLID excesivo: Demasiadas capas
Request → Handler → Command → Service → Repository → Domain → Mapper → DTO → Response
# ✅ Balanceado: Capas necesarias
Request → Handler → Service → Repository → Response
Máximo 4 capas de abstracción. Si necesitas 5+, probablemente tu microservicio es demasiado grande o tu diseño es excesivamente complejo.
Lecciones Aprendidas
- SOLID no es un dogma, es una herramienta. Úsalo cuando el contexto lo requiera. Un microservicio CRUD no necesita interfaces para repositorios.
- Startups = pragmatismo primero. Time-to-market > arquitectura perfecta. Refactoriza cuando tengas deuda técnica real.
- Dominios complejos = SOLID vale la pena. Si tu lógica de negocio tiene reglas que cambiarán cada 3 meses, SOLID te ahorrará meses de refactorización.
- Balance es clave. No es SOLID vs pragmatismo, es SOLID + pragmatismo. Aplica principios selectivamente, no religiosamente.
- Menos código > más código. Cada interfaz y capa de abstracción añade complejidad. Solo añádelos si el ROI es claro.
Conclusión
SOLID es poderoso, pero el contexto determina cuándo aplicarlo. En microservicios simples, el pragmatismo gana. En dominios complejos y sistemas de compliance, SOLID es esencial.
El mejor arquitecto no es el que aplica todos los principios religiosamente, es el que sabe cuándo ignorarlos para entregar valor más rápido. Arquitectura es sobre trade-offs, no sobre dogmas.
¿Qué sigue? En el próximo post, exploraremos cómo deployar agentes IA en Kubernetes, desde resource management hasta auto-scaling para sistemas multi-agente.
¿Has visto SOLID mal aplicado en proyectos? ¿Cómo balanceas principios y pragmatismo en tu equipo? Comparte en los comentarios.