Todos los artículos

// Arquitectura que aguanta

SOLID en Microservicios: Cuándo Aplicar y Cuándo Es Excesivo

SOLID fue diseñado para monolitos. En microservicios, estos principios se transforman. Aprende cuándo aplicar, cuándo relajar y cuándo ignorar.

7 de junio de 20267 min de lectura

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

python
# ❌ 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
🚨¿El problema?

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

python
# ✅ 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 ExtremoEnfoque Pragmático
8 archivos para un CRUD1 archivo, funcionalidad completa
Cambio = tocar 4 archivosCambio = tocar 1 archivo
Curva de aprendizaje altaCualquier dev lo entiende en 5 min
Testing requiere mocks complejosTesting simple con fixtures
Abstracciones nunca usadasSolo lo que necesitas

Cuándo SOLID Importa

SOLID no es inútil. Importa en contextos específicos:

1. Dominio Complejo

Regla de oro

SOLID vale la pena cuando tu dominio tiene reglas de negocio complejas que cambiarán frecuentemente.

python
# ✅ 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:

python
# ✅ 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:

python
# ✅ 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

ContextoSOLID RecomendadoPragmatismo 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

1

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...
2

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

python
# ❌ SOLID excesivo: Demasiadas capas
Request → Handler → Command → Service → Repository → Domain → Mapper → DTO → Response

# ✅ Balanceado: Capas necesarias
Request → Handler → Service → Repository → Response
Regla práctica

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.

Referencias rápidas

Vista general

Recursos externos

Incluye recursos adicionales en el frontmatter para que aparezcan aquí.

Más en esta serie

Serie: Arquitectura de Software Avanzada

// newsletter

¿Te sirvió este artículo?

Recibe los siguientes en tu inbox. Sin spam, cancela cuando quieras.

Discusión

Escrito por Jorge Ochoa. ¿Encontraste un error?

Abrir en GitHub