Arquitectura de Software: De 0 a Arquitecto de Sistemas Empresariales

Arquitectura de Software: De 0 a Arquitecto de Sistemas Empresariales

Guía completa sobre arquitectura de software empresarial. Patrones, C4, microservicios, B2B, multi-tenant, casos reales, antipatrones y mejores prácticas. Enfocado en negocio y decisiones estratégicas.

Por Omar Flores

Arquitectura de Software: De 0 a Arquitecto de Sistemas Empresariales

Imagina que eres un ingeniero civil. Construyes una casa, un edificio de 10 pisos y una metrópolis son tres cosas completamente distintas. No es lo mismo dibujar los planos de una casa que diseñar cómo deben conectarse las tuberías, eléctricas y sistemas estructurales de una ciudad de millones de personas.

Lo mismo ocurre en software.

Una aplicación web de un solo servidor no requiere el mismo pensamiento arquitectónico que un sistema que debe procesar millones de órdenes de compra por minuto en tiempo real, servir a 5,000 clientes empresariales simultáneamente, garantizar cero errores de transacción y escalar desde 50 a 500,000 usuarios sin que nadie se dé cuenta.

Esta es la guía que necesitaba cuando comencé mi carrera. La que te mostrará no solo cómo diseñar sistemas, sino por qué se diseñan de cierta forma, cuándo aplicar cada patrón, cuándo es overkill y cuándo es insuficiente.

Iremos paso a paso. Desde lo más simple (una aplicación de tres capas) hasta sistemas que se despliegan en múltiples continentes y procesan miles de millones de transacciones al año.


Parte I: Fundamentos - Entendiendo el Juego

Sección 1: El Desafío Arquitectónico - ¿Por Qué Importa la Arquitectura?

Hace años, recibí un mensaje de un cliente desesperado:

“Nuestro sistema de inventario se cae cada viernes a las 2 PM. Tenemos 200 órdenes en cola. No sabemos qué está pasando. El código se ve bien.”

Cuando revisé el sistema, encontré algo fascinante. No era el código el problema. El sistema estaba bien escrito. El problema era la arquitectura.

La aplicación era un monolito gigante. Cuando alguien en Marketing hacía un reporte de ventas, ese reporte consultaba la misma base de datos donde se estaban procesando órdenes en tiempo real. Ambos competían por el mismo conexión de base de datos. Ambos querían el mismo CPU. Cuando el reporte se ejecutaba, todo lo demás se congelaba.

Eso es un problema arquitectónico, no un problema de código.

La arquitectura de software responde a tres preguntas fundamentales:

1. ¿Cómo organizamos el código? 2. ¿Cómo hacemos que el sistema crezca sin colapsar? 3. ¿Cómo evitamos que un cambio pequeño derribe todo?

Si no respondes estas preguntas durante el diseño, tendrás que responderlas a las 3 AM mientras 10,000 usuarios no pueden hacer sus compras.

El Cubo de Complejidad Arquitectónica

Hay tres dimensiones de complejidad en todo sistema:

┌─────────────────────────────────────┐
│  ESCALA (¿Cuántos usuarios?)        │
│  COMPLEJIDAD (¿Cuántas features?)   │
│  CONFIABILIDAD (¿Qué tan crítico?)  │
└─────────────────────────────────────┘

Un sitio web personal para tus fotos:

  • Escala: 10-100 usuarios
  • Complejidad: Baja (mostrar fotos, comentarios)
  • Confiabilidad: Baja (si se cae 1 hora, no importa)

Un sistema de banca en línea:

  • Escala: Millones de usuarios
  • Complejidad: Alta (transferencias, pagos, reportes, fraude)
  • Confiabilidad: Crítica (un error = dinero perdido)

Aquí está el secreto: la arquitectura es directamente proporcional a qué tan alto esté cada una de estas dimensiones.

Si tu aplicación está arriba en las tres dimensiones, necesitas un arquitecto. Si está abajo en las tres, un junior con 6 meses puede manejarlo.


Sección 2: Los Cuatro Pilares de la Arquitectura de Software

Cualquier decisión arquitectónica se basa en estos cuatro pilares. Todos los patrones, todas las discusiones, todo gira alrededor de estos:

1. Escalabilidad - Crecer sin Dolor

El negocio es simple: tu startup crece de 100 a 1 millón de usuarios en un año. ¿Qué pasa?

Escalabilidad Vertical (Scale Up): Compras servidores más poderosos.

graph LR
    A["100 usuarios<br/>1 servidor<br/>32GB RAM"] -->|Crece 10x| B["1,000 usuarios<br/>1 servidor<br/>128GB RAM"] -->|Crece 10x| C["10,000 usuarios<br/>PROBLEMA:<br/>No hay servidor<br/>con 1,280GB RAM"]

El problema: existe un límite físico. Los servidores más potentes del mundo no existen. O cuestan $5 millones.

Escalabilidad Horizontal (Scale Out): Compras más servidores mediocres.

graph LR
    A["100 usuarios<br/>1 servidor"] -->|Crece 10x| B["1,000 usuarios<br/>10 servidores"] -->|Crece 10x| C["10,000 usuarios<br/>100 servidores"] -->|Crece 10x| D["100,000 usuarios<br/>1,000 servidores"]

¿Por qué importa? Porque escala infinitamente. Agregas servidores baratos ($50/mes cada uno) en lugar de servidores exóticos. Es la diferencia entre crecer y estar limitado.

La mayoría de unicornios (startups de $1B+) usan escalabilidad horizontal porque necesitan crecer 10-100x en años.

2. Confiabilidad - El Costo Oculto del Uptime

Aquí es donde la arquitectura toca dinero de verdad.

graph TB
    A["99% Uptime<br/>(Startup)"] -->|"$500k/año"| B["99.9% Uptime<br/>(Escala media)"]
    B -->|"$2M/año"| C["99.99% Uptime<br/>(Fintech, Pagos)"]
    C -->|"$10M+/año"| D["99.999% Uptime<br/>(NASA, Banks)"]

    style A fill:#90EE90
    style B fill:#FFD700
    style C fill:#FF8C00
    style D fill:#FF4500

¿Qué significa cada uno en términos de negocio?

  • 99% (Startup): Tu sistema se cae 3.6 horas al año. Tolerable si eres pequeño. Pierdes clientes pero no sufres consecuencias legales.
  • 99.9% (Escala media): 21 minutos al año. Requiere redundancia (2 servidores). Si uno se cae, el otro toma.
  • 99.99% (Fintech): 2 minutos al año. Requiere data centers múltiples, replicación geo-distribuida, failover automático. Un error = pérdida de dinero.
  • 99.999%: Casi imposible. Ves esto en telecomunicaciones, aviación. Requiere equipos dedicados solo a confiabilidad.

La razón es simple: si Stripe se cae 1 hora, pierde millones en transacciones. Si tu startup se cae 1 hora, pierdes unos pocos clientes. Arquitecturas completamente diferentes.

Hay un concepto útil en ingeniería: Error Budget. Si garantizas 99.9%, tienes 21 minutos de “permiso” para cosas malas al año. ¿Qué haces con eso?

  • 15 minutos en un deployment riesgoso
  • 6 minutos en experimentos
  • 0 minutos restantes

Este presupuesto dirige todas las decisiones arquitectónicas.

3. Mantenibilidad - Entender el Código en 6 Meses

He visto código que trabajé hace 6 meses y no lo entiendo. Es caos. Es imposible agregar features sin romper todo.

La arquitectura debe permitir que:

  • Un nuevo desarrollador entienda el sistema en pocas semanas
  • Cambiar una feature no requiera editar 50 archivos
  • Los equipos pueden trabajar independientemente

4. Costo - El Rey Silencioso Que Elige Todo

Aquí viene la verdad incómoda: la arquitectura no la elige un arquitecto. La elige el presupuesto.

graph TB
    Budget["¿Cuánto dinero tienes?"]

    Budget -->|"<$500k/año"| Small["Startup"]
    Budget -->|"$500k - $5M/año"| Medium["Escala Media"]
    Budget -->|">$5M/año"| Large["Empresa Grande"]

    Small --> SmallArch["Monolito<br/>1 servidor<br/>PostgreSQL"]
    Medium --> MediumArch["Monolito Modular<br/>5 servidores<br/>Redis Cache"]
    Large --> LargeArch["Microservicios<br/>50+ servidores<br/>Kubernetes"]

    style SmallArch fill:#FFCCCC
    style MediumArch fill:#FFFFCC
    style LargeArch fill:#CCFFCC

¿Por qué? Porque cada arquitectura tiene un costo operacional:

ArquitecturaServidoresDevOps?PersonasCosto Anual
Monolito1No2 engineers$250k
Monolito + Cache5Básico3 engineers$500k
Microservicios50Sí (Kubernetes)10+$2M+

El trade-off de arquitectura es realmente un trade-off de costo.

Cuando alguien dice “usemos microservicios,” lo que está diciendo es “gastemos $2M en infraestructura.” Si no tienes ese presupuesto, es game over.

Aquí está la verdad pragmática: Un arquitecto senior no elige “la mejor arquitectura.” Elige la mejor arquitectura dentro del presupuesto real.

Un monolito bien construido cuesta menos, es más rápido de desarrollar, y es más fácil de debugguear. Microservicios escalan mejor, pero cuestan 10x más. Si tu presupuesto es $500k, el costo gana. Siempre.


Sección 3: Metodología C4 - Dibujando Arquitecturas que Se Entienden

C4 es una notación simple para dibujar arquitecturas. Tiene 4 niveles de zoom. Como Google Maps: zoomas out ves continentes, zoomas in ves calles.

C1 - Sistema Completo

Es la pregunta más alta: ¿Qué es este sistema?

Imagina un sistema de logística. En C1 ves:

graph TB
    Users["👥 Usuarios<br/>(Conductores, Gerentes)"]
    System["📦 Sistema de Logística<br/>(Ordenes, Rutas, Inventario)"]
    Externals["🔗 Sistemas Externos<br/>(Google Maps, Stripe)"]

    Users -->|Usa| System
    System -->|Integra| Externals

Eso es C1. Nada de detalles. Solo: “Aquí hay un sistema que hace esto.”

C2 - Contenedores

Zoomas un poco. Ves las grandes piezas. ¿De qué está hecho?

graph TB
    subgraph Usuarios["Usuarios"]
        Drivers["Aplicación Móvil<br/>(iOS/Android)"]
        Managers["Portal Web<br/>(Dashboard)"]
    end

    subgraph Backend["Backend"]
        API["API REST<br/>(Node.js)"]
        Workers["Procesadores<br/>(Bull Queue)"]
    end

    subgraph Data["Datos"]
        DB["PostgreSQL<br/>(Órdenes, Rutas)"]
        Cache["Redis<br/>(Cache, Sessions)"]
    end

    Drivers -->|HTTP/REST| API
    Managers -->|HTTP/REST| API
    API -->|Lee/Escribe| DB
    API -->|Lee/Escribe| Cache
    Workers -->|Procesa| DB

    External["Servicios Externos<br/>(Google Maps, Stripe)"]
    API -->|Calls| External

Aquí empiezas a ver estructura. “La aplicación móvil habla con una API REST, que accede a una base de datos.”

C3 - Componentes

Zoomas más. Dentro de una caja (un contenedor), ves sus componentes internos.

Miremos dentro del Backend. ¿Qué hay adentro de esa API REST?

graph TB
    subgraph API["API REST"]
        Auth["Componente Auth<br/>(JWT, Roles)"]
        Orders["Componente Orders<br/>(CRUD, Business Logic)"]
        Shipping["Componente Shipping<br/>(Rutas, Optimización)"]
        Payments["Componente Payments<br/>(Stripe Integration)"]
        Events["Componente Events<br/>(Event Bus)"]
    end

    subgraph External["Externos"]
        DB["PostgreSQL"]
        Cache["Redis"]
        Maps["Google Maps API"]
    end

    Auth -->|Valida| Orders
    Orders -->|Publica| Events
    Shipping -->|Consulta| Maps
    Payments -->|Registra| Events

    Auth -->|Lee/Escribe| DB
    Orders -->|Lee/Escribe| DB
    Cache -->|Cache| Orders

Ahora ves cómo se comunican las partes internas.

C4 - Código

El último nivel. Ya dentro de un componente, ves las clases, métodos.

// Componente Orders
interface OrderService {
  createOrder(dto: CreateOrderDTO): Promise<Order>;
  updateOrderStatus(id: string, status: Status): Promise<void>;
  getOrder(id: string): Promise<Order>;
}

class OrderServiceImpl implements OrderService {
  constructor(
    private db: DatabaseConnection,
    private eventBus: EventBus,
    private shippingService: ShippingService,
  ) {}

  async createOrder(dto: CreateOrderDTO): Promise<Order> {
    // Validación
    if (!dto.items.length) throw new Error("No items");

    // Crear orden
    const order = await this.db.orders.insert({
      customerId: dto.customerId,
      items: dto.items,
      status: "PENDING",
    });

    // Publicar evento
    await this.eventBus.publish(new OrderCreatedEvent(order));

    // Notificar shipping
    await this.shippingService.schedulePickup(order.id);

    return order;
  }
}

Los cuatro niveles en conjunto forman una visión completa: desde “qué es el sistema” hasta “qué hace este método.”

Este es el primer superpoder de un arquitecto: poder comunicar ideas complejas en diferentes niveles de detalle.


Sección 3.5: La Matriz de Decisión - Conectando Negocio con Arquitectura

Antes de saltar a patrones, necesitas entender cómo el negocio dicta la arquitectura.

graph TB
    Pregunta["¿Cuáles son tus restricciones?"]

    Pregunta --> P1["¿Cuántos usuarios?"]
    Pregunta --> P2["¿Cuánto presupuesto?"]
    Pregunta --> P3["¿Criticidad?"]
    Pregunta --> P4["¿Cuántos equipos?"]

    P1 -->|"<100k"| Small["Arquitectura Simple"]
    P1 -->|"100k - 10M"| Medium["Arquitectura Escalable"]
    P1 -->|">10M"| Large["Arquitectura Distribuida"]

    P2 -->|"<$500k"| Budget1["Monolito"]
    P2 -->|"$500k - $5M"| Budget2["Modular"]
    P2 -->|">$5M"| Budget3["Microservicios"]

    P3 -->|"Baja (startup)"| Crit1["99% ok"]
    P3 -->|"Media (escala)"| Crit2["99.9% requerido"]
    P3 -->|"Alta (fintech)"| Crit3["99.99% mandatorio"]

    P4 -->|"<5"| Team1["Un equipo<br/>Un monolito"]
    P4 -->|"5-20"| Team2["Equipos separados<br/>Módulos independientes"]
    P4 -->|">20"| Team3["Múltiples equipos<br/>Servicios independientes"]

    Small --> Decision1["Monolito simple<br/>1 BD, 1 servidor"]
    Medium --> Decision2["Monolito robusto<br/>Replicación, Cache"]
    Large --> Decision3["Microservicios<br/>Distribuido globalmente"]

La arquitectura no es una decisión técnica pura. Es una decisión de negocio que tiene implicaciones técnicas.

Déjame darte ejemplos reales de cómo esto funciona:

Caso 1: Startup SaaS (100 usuarios, $50k presupuesto)

  • Presupuesto: Muy limitado
  • Usuarios: Pocos
  • Criticidad: Si se cae, algunos usuarios molestos pero nada catastrófico
  • Equipos: 2 personas
┌─────────────────────┐
│ Decisión: Monolito  │
│ ✓ Rápido            │
│ ✓ Barato            │
│ ✓ 2 personas lo     │
│   manejan           │
│ ✗ No escala a 1M    │
│   pero no importa   │
│   aún               │
└─────────────────────┘

Caso 2: Marketplace (1M usuarios, $2M presupuesto)

  • Presupuesto: Moderado
  • Usuarios: Muchos y creciendo
  • Criticidad: Si se cae, pierdo ventas (dinero real)
  • Equipos: 15 personas
┌───────────────────────────┐
│ Decisión: Monolito        │
│ Modular + Algunos         │
│ Microservicios            │
│ ✓ Core monolito (rápido)  │
│ ✓ Payments = microservice │
│   (criticidad alta)       │
│ ✓ Equipos independientes  │
│ ✓ Escalable parcialmente  │
└───────────────────────────┘

Caso 3: Fintech Global (100M usuarios, $50M presupuesto)

  • Presupuesto: Ilimitado
  • Usuarios: Masivos y distribuidos globalmente
  • Criticidad: Un error = dinero perdido, demandas, regulación
  • Equipos: 200+ personas
┌─────────────────────────┐
│ Decisión: Microservicios │
│ Distribuido Globalmente  │
│ ✓ Cada región su         │
│   instancia              │
│ ✓ Servicios especializados
│ ✓ Equipos independientes │
│ ✓ Fallos aislados        │
│ ✓ Compliance por país    │
└─────────────────────────┘

La lección: No hay “mejor” arquitectura. Hay la arquitectura correcta para TUS restricciones.


Resumen de la Parte I

Hemos establecido los fundamentos:

  1. La arquitectura importa porque responde cómo organizas, escalas y mantienes sistemas
  2. Hay cuatro pilares: Escalabilidad, Confiabilidad, Mantenibilidad, Costo
  3. C4 es tu herramienta para comunicar arquitectura en diferentes niveles
  4. Las restricciones dictan la arquitectura, no al revés

Parte II: Patrones Arquitectónicos - Las Grandes Decisiones

Sección 4: Monolito vs Microservicios - La Guerra Eterna

Quiero que olvides todo lo que has escuchado sobre microservicios.

Olvida que alguien te dijo “los microservicios son el futuro” o “el monolito es obsoleto.” Ambos son herramientas. Un martillo no es mejor que un destornillador. Depende de qué estés construyendo.

El Monolito: Simple y Potente

Un monolito es todo en un lugar: un servidor, una base de datos, una aplicación.

graph LR
    A["Usuarios"]
    B["Un Servidor<br/>(Todo el código)"]
    C["Una BD<br/>(Todos los datos)"]
    A -->|HTTP| B
    B -->|SQL| C

¿Cuándo un monolito es ORO PURO?

Si tienes <$500k presupuesto y <100k usuarios, un monolito es la decisión correcta. Aquí por qué:

  • Velocidad: Features en producción en días
  • Costo: 1 servidor + 1 BD = barato
  • ACID garantizado: Un cliente compra → dinero se debita → inventario se reduce en SIMULTÁNEO
  • Debugging: Un stacktrace cuenta toda la historia
  • Equipos pequeños: 2-5 personas entienden todo

¿Cuándo un monolito se vuelve un problema?

graph TB
    A["Monolito Creciente"]

    A --> B1["Despliegues tardan 2+ horas"]
    A --> B2["Merge conflicts cada mañana"]
    A --> B3["Un cambio rompe 3 cosas"]
    A --> B4["50+ personas esperando código"]

    B1 -->|SI TIENES ESTOS| C["REFACTORIZA YA"]
    B2 -->|SI TIENES ESTOS| C
    B3 -->|SI TIENES ESTOS| C
    B4 -->|SI TIENES ESTOS| C

    style C fill:#FF6B6B

Si ninguno de estos síntomas existe, no rompas lo que funciona.

Los Microservicios: Necesarios Pero Caros

Microservicios = 20 aplicaciones independientes, cada una con BD, equipo, deploy.

graph TB
    Usuarios["Usuarios"]

    subgraph Services["20 Microservicios"]
        S1["Orders"]
        S2["Shipping"]
        S3["Payments"]
        S4["Auth"]
    end

    Usuarios --> S1
    S1 -->|Mensaje| EventBus["Event Bus<br/>(Kafka)"]
    EventBus --> S2
    EventBus --> S3

¿Cuándo valen la pena?

Solo cuando tienes DINERO y NECESIDAD:

ProblemaValor
Equipo > 50 personasDemasiados conflictos de código
Usuarios > 10MUn servidor NO puede procesar eso
Presupuesto > $5MPuedes pagar DevOps expertos
Servicios críticos distintosPagos (99.99%) vs Reportes (99%)
Necesidad de escalar partesSolo pagos es lento, ¿por qué escalar todo?

Netflix (250M usuarios) = microservicios. Notion (100M usuarios) = monolito. Ambos ganan dinero. La diferencia: Netflix tienes $billions en ingresos, Notion tiene modelo más eficiente.

Realidad incómoda: Microservicios cuesta 5-10x más. El dinero elige.

El Secreto: Monolito Modular (El Camino Ganador)

Aquí está lo que nadie dice: las empresas exitosas usan un HÍBRIDO.

graph TB
    subgraph Monolito["Una Aplicación (Una BD)"]
        M1["Módulo Orders"]
        M2["Módulo Shipping"]
        M3["Módulo Auth"]
    end

    subgraph Micro["Pero algunos servicios son independientes"]
        P1["Payments<br/>(criticidad ultra alta)"]
        P2["Analytics<br/>(puede estar lento)"]
    end

    M1 -.->|API clara| M2
    M1 -->|HTTP| P1
    M1 -->|Async| P2

    style Monolito fill:#CCFFCC
    style Micro fill:#FFFFCC

Shopify hace esto. Stripe hace esto. Es la arquitectura pragmática ganadora.

Ventajas:

  • Simplicidad del monolito para lo que funciona
  • Flexibilidad de microservicios donde la necesitas
  • Costo controlado
  • Evoluciona con tu negocio

La Tabla de Decisión REAL

graph TB
    Users["¿Cuántos<br/>Usuarios?"]

    Users -->|"<100k"| A1["Monolito"]
    Users -->|"100k-10M"| A2["Monolito<br/>Modular"]
    Users -->|">10M"| A3["Híbrido"]

    A1 -->|"<$500k"| R1["✓ Monolito<br/>simple"]
    A1 -->|">$500k"| R1b["✓ Monolito<br/>robusto"]

    A2 -->|"$500k-$2M"| R2["✓ Monolito<br/>Modular"]
    A2 -->|">$2M"| R2b["✓ O microservicios"]

    A3 -->|">$5M"| R3["✓ Híbrido<br/>inteligente"]
    A3 -->|"<$5M"| R3b["✗ Espera,<br/>baja presupuesto"]

La lección de oro: Empieza simple. Solo refactoriza cuando sientas el dolor REAL, no el teórico.

graph TB
    A["Usuarios"]

    subgraph Servicios["Microservicios"]
        S1["Servicio Orders<br/>(Node.js)"]
        S2["Servicio Shipping<br/>(Go)"]
        S3["Servicio Payments<br/>(Python)"]
        S4["Servicio Auth<br/>(Rust)"]
    end

    subgraph Datos["Datos"]
        D1["Orders DB<br/>(PostgreSQL)"]
        D2["Shipping DB<br/>(MongoDB)"]
        D3["Payments DB<br/>(PostgreSQL)"]
    end

    A -->|HTTP| S1
    A -->|HTTP| S2
    A -->|HTTP| S3

    S1 -->|HTTP| S2
    S1 -->|HTTP| S3
    S1 -->|Auth| S4

    S1 -.->|SQL| D1
    S2 -.->|NoSQL| D2
    S3 -.->|SQL| D3

Cada equipo tiene su propio servicio, su propia base de datos, su propio lenguaje, su propio ciclo de deploy.

Ventajas de Microservicios:

  1. Escalabilidad de equipos - 100 personas en 20 servicios = menos conflictos
  2. Flexibilidad tecnológica - El equipo de pagos usa Rust, el de shipping usa Go
  3. Despliegues independientes - Cambio el servicio de órdenes sin tocar pagos
  4. Escalabilidad selectiva - Si ordenes es lento, duplico solo ese servicio
  5. Resiliencia - Si shipping se cae, órdenes sigue funcionando
  6. Evolución constante - Puedo cambiar un servicio completo sin afectar otros

Desventajas de Microservicios:

  1. Complejidad distribuida - Ahora tienes 20 aplicaciones fallando de formas nuevas
  2. Latencia de red - Llamar otra aplicación es 100x más lento que una función local
  3. Transacciones distribuidas - Garantizar que múltiples servicios se sincronizen es pesadilla
  4. Monitoreo - Un usuario reporta “sistema lento” ¿De cuál de los 20 servicios es culpa?
  5. Debugging - Un request pasa por 10 servicios. ¿Dónde está el problema?
  6. Infraestructura - Kubernetes, load balancing, circuit breakers. Necesitas DevOps serios

La Tabla de Decisión

Hay un patrón en qué empresas grandes usan qué:

EmpresaUsuariosEmpleadosArquitecturaRazón
Netflix250M5000MicroserviciosEscala masiva, múltiples lenguajes, deploy independiente
Airbnb100M4000Monolito + ServiciosEmpezaron monolito, ahora algunos servicios críticos independientes
Stripe50M txn/día2000Monolito + ServiciosBackend monolito (ACID), servicios específicos para escala
Shopify1.7M negocios9000MicroserviciosMúltiples regiones, tecnologías especializadas
Notion100M2000MonolitoSimplicidad, equipo más pequeño
GitHub100M usuarios3000Monolito + ServiciosRuby on Rails principal + servicios de CI/CD

¿Ves el patrón? Todas empezaron con monolito.

Shopify, Netflix, Amazon: todas un monolito. Luego, cuando tuvieron miles de millones de dólares, problemas de escala y cientos de ingeniero, pasaron a microservicios.

La Pregunta Correcta No Es “¿Monolito o Microservicios?”

Es: “¿Cuándo es el monolito tan grande que cuesta más mantenerlo que refactorizarlo?”

La respuesta es: cuando tienes estos síntomas:

  1. Despliegues toman horas - Porque cada pequeño cambio requiere testear TODO
  2. Equipos esperan en conflictos de git - Más de 5 personas editando el mismo archivo
  3. Un crash de una feature afecta otras - El servicio de búsqueda se cae y reportes también
  4. No puedes cambiar la BD sin afectar 20 cosas - El modelo de datos está acoplado

Si NINGUNO de estos síntomas existe, tu monolito es oro puro. Es simple, es rápido, es barato.

Si TODOS existen, es hora de empezar a extraer servicios.

El Enfoque Correcto: Modular Monolith

Hay un intermedio que muchos ignoran: Monolito Modular.

graph TB
    subgraph Monolito["Una Aplicación<br/>(Un Servidor, Una BD)"]
        M1["Módulo Orders<br/>(Interfaz clara)"]
        M2["Módulo Shipping<br/>(Interfaz clara)"]
        M3["Módulo Payments<br/>(Interfaz clara)"]
        M4["Módulo Auth<br/>(Interfaz clara)"]
    end

    M1 -->|API Interna| M2
    M1 -->|API Interna| M3
    M1 -->|API Interna| M4

Es un monolito, pero está organizado como si fuera microservicios. Cada módulo:

  • Tiene su propia carpeta
  • Define una interfaz clara
  • No accede directamente a otros módulos
  • Podría ser extraído a un microservicio después

Empresas como Shopify usan esto. Les permite:

  1. La rapidez del monolito (una BD, ACID)
  2. La flexibilidad de microservicios (módulos independientes)
  3. Migrar servicios cuando sea necesario sin rediseñar todo

Ejemplo Práctico: Sistema de E-commerce

Año 1 - Monolito:

ecommerce/
├── users/
│   ├── service.ts
│   ├── controller.ts
│   └── repository.ts
├── products/
├── orders/
├── payments/
└── database.ts (una conexión)

Año 3 - 50 ingenieros, problemas de escala:

Extractamos Payments a un microservicio porque:

  • Pagos necesita latencia baja (Stripe es lento)
  • Pagos es crítico (un error = dinero perdido)
  • El equipo de pagos quiere deployar 10 veces al día
  • Órdenes necesita escalar sin escalar pagos

Pero Orders, Shipping, Users siguen en el monolito porque:

  • Las transacciones entre ellos son frecuentes
  • ACID es crítico
  • El costo de distribuirlos es mayor que sus problemas

Año 5 - 200 ingenieros, múltiples regiones:

Ahora extractamos:

  • Pagos (microservicio en USA)
  • Shipping (microservicio, necesita escalas independientemente)
  • Search (microservicio, usa Elasticsearch)
  • Auth (microservicio compartido)

Pero Orders, Users, Inventory siguen acoplados porque son el corazón del negocio.


Resumen: Cuándo cada uno

Elige Monolito si:

  • Equipo < 20 personas
  • Características < 50 distintas
  • Escala < 1 millón usuarios
  • Presupuesto < $500k/año

Elige Monolito Modular si:

  • Planeas crecer
  • Tienes múltiples equipos
  • Algunos módulos escalan diferente

Elige Microservicios si:

  • Equipo > 100 personas
  • Características > 500
  • Escala > 100 millones usuarios
  • Presupuesto > $5M/año
  • Ya tienes DevOps experientado

En la próxima sección exploraremos el patrón BFF (Backend for Frontend), crucial cuando tienes múltiples clientes (web, móvil, smart TV) accediendo al mismo backend.


Sección 5: BFF Pattern - Backend for Frontend

El problema es clásico: Tu servicio de pagos tiene 5 clientes diferentes:

  • App móvil (ancho de banda limitado, batería limitada)
  • App web (más datos, UI compleja)
  • Dashboard ejecutivo (necesita reportes complejos)
  • Integraciones API (JSON puro, requisitos estrictos)
  • Smart TV (pantalla pequeña, datos minimales)

Todos consultando el MISMO backend.

graph TB
    subgraph Clients["Clientes Diferentes"]
        Mobile["📱 App Móvil<br/>(Datos comprimidos)"]
        Web["💻 Web<br/>(HTML, Datos complejos)"]
        Executive["📊 Dashboard Ejecutivo<br/>(Reportes, Analytics)"]
        API["🔗 API Partners<br/>(JSON puro)"]
        TV["📺 Smart TV<br/>(Datos mínimos)"]
    end

    subgraph Problem["❌ PROBLEMA: UN SOLO BACKEND"]
        Backend["API REST Única<br/>(Confusión total)"]
    end

    Mobile --> Backend
    Web --> Backend
    Executive --> Backend
    API --> Backend
    TV --> Backend

    Backend -->|"¿Qué datos<br/>devuelvo?"| Confusion["El código se<br/>vuelve loco"]

El código termina como un nido de ratas:

// Lógica if/if/if/if para cada cliente
if (isMobile) {
  /*datos comprimidos*/
}
if (isWeb) {
  /*datos complejos*/
}
if (isExecutive) {
  /*reportes*/
}
// Cambiar un cliente = afecta a todos

BFF Solution: Cada cliente su propio backend

graph TB
    subgraph Clients["Clientes"]
        Mobile["📱 Móvil"]
        Web["💻 Web"]
        Executive["📊 Ejecutivo"]
    end

    subgraph BFFs["Backends Especializados (BFF)"]
        BFFMobile["BFF Mobile<br/>(Optimizado para<br/>móvil)"]
        BFFWeb["BFF Web<br/>(Optimizado para<br/>web)"]
        BFFExec["BFF Ejecutivo<br/>(Optimizado para<br/>reportes)"]
    end

    subgraph Core["Backend Core"]
        Orders["Servicio Orders<br/>(Lógica de negocio)"]
        Reports["Servicio Reports<br/>(Cálculos)"]
    end

    Mobile --> BFFMobile
    Web --> BFFWeb
    Executive --> BFFExec

    BFFMobile -->|gRPC| Orders
    BFFWeb -->|gRPC| Orders
    BFFExec -->|gRPC| Reports

¿Por qué es mejor?

AspectoSin BFFCon BFF
Cambiar app móvilAfecta web y ejecutivoSolo afecta BFF Mobile
Ancho de banda móvilDatos complejos = lentoDatos mínimos = rápido
Dashboard ejecutivoReportes lentos en endpoint generalReportes optimizados en BFF Exec
Código limpioSpaghetti con if/elseCada BFF responsable de su cliente
EscalabilidadProblemas de móvil afectan a webSe escalan independientemente

Ejemplo de diferencia:

Endpoint /orders SIN BFF:
- Móvil necesita 10KB
- Devolvemos 500KB (web + reportes)
- Móvil usa 5% del ancho de banda
- Paga más en datos celulares

Endpoint /orders CON BFF Mobile:
- Devolvemos 10KB (exactamente lo necesario)
- Móvil usa 100% de lo que recibe
- Más rápido, menos datos, mejor UX

Cuándo usar BFF:

  • Tienes 3+ clientes fundamentalmente diferentes
  • Requisitos de datos distintos (móvil = mínimo, ejecutivo = máximo)
  • Equipos independientes (equipo móvil, equipo web)

Cuándo NO usarlo:

  • Tienes solo web
  • Todos los clientes necesitan los mismos datos

  1. Escala selectiva - Si el dashboard está lento, escalo solo ese BFF
  2. Tecnologías diferentes - BFF móvil en Node.js, BFF ejecutivo en Go
  3. UI al backend - BFF web puede hacer SSR, BFF mobile puede cachear datos

Desventajas de BFF:

  1. Duplicación - El mismo endpoint existe en múltiples BFFs
  2. Complejidad - Más aplicaciones que mantener
  3. Sincronización - Si el core cambia, todos los BFFs deben cambiar

BFF es perfecto cuando:

  • Tienes 3+ clientes distintos
  • Tienen necesidades radicalmente diferentes
  • Equipos independientes (equipo mobile, equipo web)

BFF es overkill cuando:

  • Solo tienes web
  • Todos los clientes necesitan los mismos datos

Sección 6: Event-Driven Architecture - Comunicación Desacoplada

Volvamos al problema del viernes a las 2 PM. Ahora estamos en microservicios.

El servicio de Orders crea órdenes. El servicio de Inventory maneja stock. ¿Cómo hablan sin acoplarse?

graph TB
    A["Problema:<br/>¿Cómo se comunican<br/>Órdenes e Inventory?"]

    A -->|Opción 1| B1["Llamadas Directas<br/>(Orders → Inventory)"]
    A -->|Opción 2| B2["Eventos<br/>(Orders publica,<br/>Inventory escucha)"]

    B1 -->|"Si Inventory<br/>es lento"| C1["❌ Orders se<br/>congela<br/>Acopladas"]

    B2 -->|"Si Inventory<br/>es lento"| C2["✅ Orders sigue<br/>funcionando<br/>Desacopladas"]

    style C1 fill:#FFB6B6
    style C2 fill:#B6E4B6

Opción 1: Sincronía (Llamadas directas)

Orders dice: “Oye Inventory, reserva stock para esta orden.”

Inventory responde: “Ok, stock reservado” o “No hay stock, fallaste.”

Problema: si Inventory tarda 10 segundos en responder, Orders espera 10 segundos. Si Inventory cae, todo falla.

Opción 2: Asincronía (Eventos)

Orders dice: “Se creó una orden” y listo. No espera respuesta.

Inventory escucha el evento “OrderCreated”, reserva stock, y publica “StockReserved”.

Shipping escucha “StockReserved” y programa el envío.

graph LR
    A["Orders publica:<br/>OrderCreated"]

    B["Event Bus<br/>(Kafka, RabbitMQ)"]

    A --> B

    B -->|Escucha| C["Inventory"]
    B -->|Escucha| D["Shipping"]
    B -->|Escucha| E["Payments"]
    B -->|Escucha| F["Analytics"]

    C -->|Publica| B
    D -->|Publica| B
    E -->|Publica| B
    F -->|Publica| B

¿Por qué importa esto en el negocio?

EscenarioSincronía (Llamadas)Asincronía (Eventos)
Inventory se caeOrders se cae, cliente no puede comprarOrders sigue funcionando, ordenes en cola
Inventory está lentoTodos los requests lentosSolo inventory lento, otros rápidos
Agregar nuevo servicioModificar Orders, agregar lógicaNuevo servicio escucha sin cambiar nada
Crecimiento exponencialCambio masivo en códigoAgrogo nodos que escuchan

Uber? Event-driven. Toda orden genera eventos. Netflix? Event-driven. Cada view, cambio, fallo es un evento.

Event Sourcing: El siguiente nivel

En lugar de guardar solo el estado actual:

Orden: status=SHIPPED

Guardas todo lo que pasó:

1. OrderCreated (usuario hace click en comprar)
2. PaymentProcessed (tarjeta aprobada)
3. InventoryReserved (stock disponible)
4. ShippingScheduled (empaquetado)
5. OrderShipped (salió del almacén)

Ahora tienes un registro COMPLETO. Si algo falla, puedes recrear el estado desde los eventos.


Parte III: Casos de Uso Empresariales - Del Teórico al Real

Sección 7: B2B SaaS - Servicio Empresarial Multi-Cliente

Imagina que eres Slack. 500,000 empresas usan tu producto. Cada una con:

  • Diferentes planes (free, pro, enterprise)
  • Diferentes permisos
  • Datos completamente aislados
  • Requisitos de compliance distintos (HIPAA, GDPR, SOC2)

Arquitectura C2 - Contenedores principales:

graph TB
    subgraph Clients["Clientes"]
        Client1["Empresa A"]
        Client2["Empresa B"]
        Client3["Empresa C"]
    end

    subgraph Platform["Plataforma SaaS"]
        Web["Web App"]
        Mobile["Mobile App"]
        API["API REST"]
    end

    subgraph Core["Core Services"]
        Auth["Auth Service<br/>(Identificar cliente)"]
        Workspace["Workspace Service<br/>(Datos del cliente)"]
        Messages["Messages Service<br/>(Mensajes)"]
        Files["Files Service<br/>(Documentos)"]
    end

    subgraph Data["Data Layer"]
        DB["PostgreSQL<br/>(Separado por tenant)"]
        Cache["Redis<br/>(Por tenant)"]
        S3["S3<br/>(Archivos)"]
    end

    Client1 -->|Login| Web
    Client2 -->|Login| API
    Client3 -->|Login| Mobile

    Web -->|auth_token| Auth
    Auth -->|Qué datos del cliente| Workspace
    Workspace -->|Lee datos| DB

Sección 7.1: Multi-Tenancy - Aislar Datos de 500,000 Empresas

La pregunta crítica: ¿Cómo aíslo los datos de cada cliente?

Hay tres enfoques:

1. Database per Tenant (Máxima seguridad)

Empresa A: base_datos_a
Empresa B: base_datos_b
Empresa C: base_datos_c

Ventajas:

  • Completa aislación
  • Fácil compliance (backups por cliente)
  • Puedo elegir diferente BD para cada cliente

Desventajas:

  • Pesadilla operacional (500,000 bases de datos)
  • Costos altos
  • Migrations son complejas

Quién lo usa: Salesforce, algunos SaaS enterprise.

2. Schema per Tenant (Balance)

postgres://
├── empresa_a_schema
├── empresa_b_schema
└── empresa_c_schema

Una base de datos, pero cada cliente tiene su schema.

Ventajas:

  • Mejor que DB per tenant
  • Buena seguridad
  • Fácil backup y disaster recovery

Desventajas:

  • Migrations afectan todos los schemas
  • Query planing es más complejo

Quién lo usa: Heroku, Fly.io.

3. Row-Level Security (RLS)

-- Una tabla, datos de todos los clientes
CREATE TABLE messages (
  id SERIAL,
  tenant_id UUID,
  content TEXT,
  created_at TIMESTAMP
);

-- Política: Solo ves tus datos
CREATE POLICY tenant_isolation ON messages
  USING (tenant_id = auth.jwt_claims()->>'tenant_id');

Una tabla para todos. PostgreSQL garantiza que solo ves tus datos.

Ventajas:

  • Simplicidad operacional
  • Costos bajos
  • Escalabilidad máxima

Desventajas:

  • Require que hagas bien la seguridad (un error = breach)
  • Menos control por cliente

Quién lo usa: Vercel, Supabase, AWS RDS.

Recomendación: Usa RLS. Es simple y escala. Si necesitas compliance especial (HIPAA), luego migas a schema per tenant.

Sección 7.2: Autenticación Multi-Tenant

Cuando un usuario hace login, debes saber:

  1. ¿Quién es el usuario?
  2. ¿De qué tenant?
  3. ¿Qué permisos tiene?
// Middleware de autenticación
async function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.split(" ")[1];

  if (!token) {
    return res.status(401).json({ error: "No token" });
  }

  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  // decoded = {
  //   userId: "user_123",
  //   tenantId: "company_456",
  //   email: "engineer@company.com",
  //   role: "ADMIN"
  // }

  // Guardar en el request
  req.user = decoded;

  next();
}

// Ahora en tus rutas
app.get("/messages", authMiddleware, async (req, res) => {
  const messages = await db
    .query("SELECT * FROM messages WHERE tenant_id = $1", [req.user.tenantId])
    .limit(50);

  return res.json(messages);
});

El JWT token contiene el tenant_id. Así cada request sabe a qué cliente pertenece.


Sección 8: Sistema de Logística en Tiempo Real

Vamos a un caso concreto: un sistema de logística que necesita:

  • Procesar 10,000 órdenes por hora
  • Mostrar ubicación de envíos en tiempo real (GPS)
  • Calcular rutas óptimas (¿cuál es el orden de entregas?)
  • Manejar choques sin derrocar el sistema

Arquitectura C3 - Componentes principales:

graph TB
    subgraph Clients["Clientes"]
        Driver["App Conductor<br/>(Ubicación en tiempo real)"]
        Manager["Dashboard Gerente<br/>(Órdenes + Mapas)"]
        Customer["App Cliente<br/>(Dónde está mi paquete)"]
    end

    subgraph Frontend["Frontend Layer"]
        Web["Portal Web"]
        Mobile["App Móvil"]
    end

    subgraph BFF["Backend for Frontend"]
        BFF_Driver["BFF Driver"]
        BFF_Manager["BFF Manager"]
        BFF_Customer["BFF Customer"]
    end

    subgraph Services["Servicios Core"]
        Orders["Servicio Orders"]
        Routing["Servicio Routing<br/>(Rutas óptimas)"]
        Tracking["Servicio Tracking<br/>(GPS real-time)"]
        Notifications["Servicio Notificaciones"]
    end

    subgraph RealTime["Comunicación en Tiempo Real"]
        WebSocket["WebSocket Server"]
        Pub["Redis Pub/Sub"]
    end

    subgraph Data["Persistencia"]
        DB["PostgreSQL<br/>(Órdenes, Rutas)"]
        TimeSeries["TimescaleDB<br/>(GPS, Histórico)"]
        Cache["Redis<br/>(Cache + Pub/Sub)"]
    end

    Driver -->|Envía GPS cada 5s| BFF_Driver
    Manager -->|Consulta estado| BFF_Manager
    Customer -->|¿Dónde está?| BFF_Customer

    BFF_Driver -->|Publica evento| Pub
    BFF_Manager -->|Escucha cambios| WebSocket

    Tracking -->|Suscrito a GPS| Pub
    Tracking -->|Almacena| TimeSeries

    Orders -->|Crea orden| DB
    Routing -->|Calcula ruta| Orders

Sección 8.1: El Desafío del GPS en Tiempo Real

Un conductor envía su ubicación cada 5 segundos. Con 1,000 conductores, son 12,000 updates por minuto.

Si haces esto:

// ❌ MAL - Escribir cada punto GPS en la BD
app.post("/driver/location", async (req, res) => {
  const { driverId, lat, lng } = req.body;

  // 12,000 escrituras por minuto en la BD = CUELLO DE BOTELLA
  await db.query("INSERT INTO gps_points VALUES ($1, $2, $3)", [
    driverId,
    lat,
    lng,
  ]);

  res.json({ ok: true });
});

Crash garantizado.

Solución correcta:

// ✅ BIEN - Usar Redis Pub/Sub para broadcast en tiempo real
const redis = require("redis");
const publisher = redis.createClient();

app.post("/driver/location", async (req, res) => {
  const { driverId, lat, lng, timestamp } = req.body;

  // Publicar a todos los que escuchan este conductor
  await publisher.publish(
    `driver:${driverId}:location`,
    JSON.stringify({
      lat,
      lng,
      timestamp,
    }),
  );

  // Guardar en cache (muy rápido)
  await redis.set(
    `driver:${driverId}:last_location`,
    JSON.stringify({ lat, lng, timestamp }),
    "EX",
    300,
  );

  res.json({ ok: true });
});

// En el dashboard del gerente, WebSocket escucha
io.on("connection", (socket) => {
  socket.on("subscribe_driver", (driverId) => {
    const subscriber = redis.createClient();
    subscriber.subscribe(`driver:${driverId}:location`);

    subscriber.on("message", (channel, message) => {
      socket.emit("driver_moved", JSON.parse(message));
    });
  });
});

Ahora:

  • GPS llega a Redis (ultra rápido)
  • Se broadcast a todos via WebSocket (tiempo real)
  • Se guarda en TimescaleDB cada 1 minuto (para análisis históricos)

Sección 8.2: Rutas Óptimas - El Problema del Viajante de Comercio

Un conductor tiene 50 entregas en la ciudad. ¿En qué orden?

Si calcula todas las posibilidades: 50! combinaciones. Es imposible.

Necesitas algoritmos:

// Algoritmo heurístico: Nearest Neighbor
function calculateRoute(deliveries, startPoint) {
  const route = [startPoint];
  const remaining = [...deliveries];

  while (remaining.length > 0) {
    const current = route[route.length - 1];

    // Encuentra el más cercano
    const nearest = remaining.reduce((best, delivery) => {
      const distance = haversineDistance(current, delivery.location);
      return distance < best.distance ? { delivery, distance } : best;
    });

    route.push(nearest.delivery);
    remaining.splice(remaining.indexOf(nearest.delivery), 1);
  }

  return route;
}

No es perfecto, pero es rápido (O(n²) en lugar de O(n!)).

Para mejor optimization, usas:

  • Algoritmos genéticos: Crear “poblaciones” de rutas y evolucionar
  • Simulated Annealing: Cambios pequeños y aleatorios hasta mejorar
  • Machine Learning: Entrenar un modelo con rutas históricas

Empresas como Google Maps usan combinaciones de todos.

Sección 8.3: Manejo de Fallos

¿Qué pasa si un conductor no entrega? ¿La app se cae?

graph TB
    A["Orden lista para entregar"]
    B{Conductor disponible?}
    C["Asignar a conductor"]
    D["Enviar notificación"}
    E{¿Entrega exitosa?}
    F["Marcar como entregada"]
    G["Error: Reintentar o escalar"]
    H["Guardar en Dead Letter Queue"]

    A --> B
    B -->|Sí| C
    B -->|No| H
    C --> D
    D --> E
    E -->|Sí| F
    E -->|No| G
    G -->|Reintento disponible| C
    G -->|No hay reintentos| H

Si algo falla, entra a una Dead Letter Queue. Luego:

  • Retry automático cada 5 minutos
  • Notificación al manager después de 3 reintentos fallidos
  • Escala a un gerente de operaciones si falla completamente

Sección 9: ERP Modular - Empresa con Múltiples Departamentos

Un ERP (Enterprise Resource Planning) es lo opuesto a un SaaS simple. Una sola empresa con múltiples departamentos (Ventas, Finanzas, Producción, RH, etc.) que necesitan compartir datos.

Ejemplo: Cuando se crea una orden de venta (Ventas), automáticamente se crea:

  • Una orden de producción (Producción)
  • Un asiento contable (Finanzas)
  • Un plan de envío (Logística)

Arquitectura C2:

graph TB
    subgraph Departamentos["Departamentos"]
        Ventas["Ventas<br/>(Órdenes, CRM)"]
        Produccion["Producción<br/>(Manufactura)"]
        Finanzas["Finanzas<br/>(Contabilidad)"]
        Logistica["Logística<br/>(Envíos)"]
        RH["RH<br/>(Nómina)"]
    end

    subgraph Core["Core ERP"]
        Master["Master Data Service<br/>(Clientes, Productos, Precios)"]
        Event["Event Bus<br/>(Órdenes, Cambios)"]
    end

    subgraph Data["Datos Compartidos"]
        DB["PostgreSQL<br/>(Mismo esquema)"]
        Warehouse["Data Warehouse<br/>(Analytics)"]
    end

    Ventas -->|Lee clientes| Master
    Ventas -->|Publica: OrderCreated| Event

    Produccion -->|Escucha: OrderCreated| Event
    Finanzas -->|Escucha: OrderCreated| Event
    Logistica -->|Escucha: OrderCreated| Event

    Master -->|Datos| DB
    All -->|Sync cada noche| Warehouse

A diferencia de un SaaS multi-tenant, un ERP es monolito en datos pero modular en servicios.

Sección 9.1: La Complejidad de Cambios en Cascada

Cuando cambias el precio de un producto:

Cambio precio --> Órdenes futuras usan nuevo precio
                --> Pero ¿qué pasa con órdenes pendientes?
                --> ¿Facturas enviadas?
                --> ¿Acuerdos con clientes?

Es un caos. La solución es auditoría completa:

// Cuando alguien cambia un precio
async function updateProductPrice(productId, newPrice, userId) {
  const oldPrice = await db.products.findById(productId);

  // 1. Cambiar el precio
  await db.products.update(productId, { price: newPrice });

  // 2. Registrar el cambio (auditoría)
  await db.audit_log.insert({
    type: "PRICE_CHANGE",
    entityId: productId,
    oldValue: oldPrice,
    newValue: newPrice,
    userId: userId,
    timestamp: new Date(),
    reason: req.body.reason,
  });

  // 3. Notificar a sistemas downstream
  await eventBus.publish("product.price_changed", {
    productId,
    oldPrice: oldPrice,
    newPrice: newPrice,
  });

  // 4. Recalcular órdenes abiertas
  const openOrders = await db.orders.where({
    status: "PENDING",
    items: { some: { productId } },
  });

  for (const order of openOrders) {
    await recalculateOrderTotal(order.id);
  }
}

Sección 10: CQRS - Command Query Responsibility Segregation

Imagina un dashboard que necesita mostrar:

  • Total de órdenes procesadas
  • Revenue en tiempo real
  • Cliente top 10
  • Tasa de completitud

Las queries son complejas (múltiples joins, agregaciones). Hacer esto en la BD transaccional es lento.

CQRS dice: Tienes dos caminos

  1. Command Path (escritura): Procesar órdenes, cambiar estado
  2. Query Path (lectura): Mostrar dashboards, reportes
graph TB
    subgraph Write["Write Side (Órdenes)"]
        Client["Cliente"]
        API["API"]
        Events["Event Bus"]
        DB["PostgreSQL<br/>(Fuente de verdad)"]
    end

    subgraph Read["Read Side (Dashboards)"]
        Projections["Projections<br/>(Datos pre-procesados)"]
        Cache["Redis<br/>(Cache)"]
        Analytics["MongoDB<br/>(Optimizado para lectura)"]
    end

    Client -->|Crear orden| API
    API -->|Guarda| DB
    DB -->|Publica| Events
    Events -->|Procesa| Projections
    Projections -->|Actualiza| Analytics
    Projections -->|Invalida| Cache

    Dashboard["Dashboard"]
    Dashboard -->|¿Órdenes totales?| Analytics

La magia: Projections transforman los datos para lectura:

// Cuando se crea una orden
eventBus.on("order.created", async (event) => {
  // Actualizar projection de "órdenes por día"
  await analyticsDB.update(
    { _id: `day_${event.date}` },
    { $inc: { count: 1, total_amount: event.amount } },
    { upsert: true },
  );

  // Invalidar cache
  await redis.del("dashboard:summary");
});

// Cuando alguien consulta el dashboard
app.get("/dashboard/summary", async (req, res) => {
  // Revisar cache
  let data = await redis.get("dashboard:summary");
  if (data) {
    return res.json(JSON.parse(data));
  }

  // Si no está en cache, consultar MongoDB (lectura, muy rápido)
  data = await analyticsDB.collection("order_stats").findOne({});

  // Guardar en cache por 1 minuto
  await redis.set("dashboard:summary", JSON.stringify(data), "EX", 60);

  return res.json(data);
});

Ventajas:

  • Dashboards rápidos (lecturas optimizadas)
  • Escrituras sin bloqueos (órdenes procesadas sin esperar analytics)
  • Escalabilidad (replica DB de lectura 100 veces si quieres)

Desventajas:

  • Complejidad (ahora tienes 2 sistemas de datos)
  • Consistencia eventual (el dashboard muestra datos de 30 segundos atrás)

Parte IV: Antipatrones - Las Decisiones que Lamentarás

Sección 11: Antipatrones Arquitectónicos

Cada patrón tiene su espejo oscuro. Aquí están los más comunes.

Antipatrón 1: La Arquitectura Grande y Monolítica (Big Ball of Mud)

Empiezas con monolito. Genial, rápido. Pero después de 3 años:

src/
├── users.ts (5,000 líneas)
├── products.ts (8,000 líneas)
├── orders.ts (12,000 líneas)
├── payments.ts (3,000 líneas)
├── notifications.ts (2,000 líneas)
├── analytics.ts (4,000 líneas)
└── utils.ts (15,000 líneas, todo se usa en todo)

Cada archivo usa 20 otros. No hay bordes claros. Es caos.

Síntomas:

  • Cambiar una línea en users.ts requiere entender payments.ts
  • Tests tardan 30 minutos en correr
  • Merge conflicts cada mañana
  • “Nadie entiende cómo funciona X”

Solución:

  • Restructurar el monolito en módulos claros (Monolito Modular)
  • O extraer servicios críticos

Antipatrón 2: Premature Microservices

Tu startup tiene 3 ingenieros. Decides: “Vamos a usar Kubernetes con 20 microservicios.”

Ahora gastan:

  • 2 semanas en CI/CD pipelines
  • 1 mes en infraestructura
  • Debugging toma 10 horas (el problema está en uno de 20 servicios)

Síntomas:

  • Equipo pequeño, arquitectura para 200 personas
  • “¿Por qué tarda tanto agregar una feature simple?”
  • Infraestructura cuesta más que ingenieros

Solución:

  • Empieza monolito
  • Cuando sientas el dolor (verdadero dolor, no teórico), refactoriza

Antipatrón 3: Shared Database (Acoplamiento de Datos)

Servicio A ← PostgreSQL → Servicio B

Ambos servicios usan la misma tabla. Ahora:

  • Cambiar un schema afecta ambos
  • Cambios en A requieren que B esté listo
  • No puedes escalar A sin escalar B

Solución:

  • Cada servicio su BD
  • Si necesitan compartir datos, vía APIs

Antipatrón 4: Cache-Driven Architecture

Cuando tu BD es lenta, agregas Redis. Cuando Redis se llena, agregas memcached. Ahora tienes:

Aplicación → Redis → Memcached → DB

Problema: Invalidación de cache es NP-hard (incluso Phil Karlton dijo “solo hay dos cosas difíciles en CS: invalidación de cache y nombrar cosas”).

Si invalidas cache de forma inconsistente:

  • Cliente A ve datos old
  • Cliente B ve datos nuevos
  • Transacciones fallan

Solución:

  • Primero optimiza la BD (índices, queries)
  • Luego cache si realmente lo necesitas
  • Si usas cache, ten una estrategia de invalidación clara

Antipatrón 5: ¿Serverless para TODO?

Lambda/Cloud Functions es increíble para casos específicos (webhooks, processamiento asincronos). Pero no para:

  • Aplicaciones con estado
  • Queries a BD muy frecuentes (cold starts = 1 segundo)
  • Lógica compleja con múltiples pasos

Síntoma:

  • “Por qué nuestro dashboard tarda 5 segundos en cargar? Tiene 3 Lambdas.”
  • Cold starts destruyendo UX

Solución:

  • Usa Serverless estratégicamente (background jobs, webhooks)
  • Servidor tradicional para request-response

Antipatrón 6: No Medir Nada

Lanzas tu sistema. “Está rápido” dicen. Después:

  • 10,000 usuarios llegó y todo se cae
  • ¿Dónde? ¿DB? ¿Red? ¿Código?
  • No tienes idea

Solución:

  • Observabilidad desde día 1:
    • Logs (ELK Stack, Datadog)
    • Métricas (Prometheus, CloudWatch)
    • Traces (Jaeger, DataDog)

Antipatrón 7: Integración Síncrona en Cadena

Usuario → API → Servicio A → Servicio B → Servicio C

Si C es lento, todo se congela. Si C cae, todo falla.

Solución:

  • Usa mensajes asincronos
  • Define timeouts
  • Circuit breakers para fallos en cascada

Sección 12: Las Preguntas que Debes Hacer

Cuando alguien te propone una arquitectura, estas preguntas revelan si saben lo que hacen:

1. “¿Cuál es el plan de escala a 10x usuarios?”

Si no tienen respuesta, es arquitectura de juguete.

2. “¿Cómo manejamos fallos?”

  • ¿Qué pasa si la BD cae?
  • ¿Qué pasa si una red se desconecta?
  • ¿Recovery RTO y RPO?**

3. “¿Cuál es el costo de esta arquitectura?”

Microservicios escalan mejor pero cuestan 10x. ¿Vale la pena?

4. “¿Quién la mantiene?”

¿Necesitas un DevOps dedicado? ¿El equipo entiende?

5. “¿Cuándo la cambiar?”

Ninguna arquitectura es permanente. ¿Cuáles son los síntomas de cambio?


Parte V: Decisiones Arquitectónicas en el Mundo Real

Sección 13: Estudio de Casos - Cómo lo Hacen las Grandes Empresas

Stripe - El Procesador de Pagos Global

Stripe procesa $1 billones en transacciones anuales. Su arquitectura:

  • Backend principal: Monolito en Ruby on Rails (sí, Ruby)
  • Porque: ACID es crítico para dinero. Un monolito garantiza consistencia.
  • Escala horizontal: Múltiples instancias del monolito detrás de un load balancer
  • Servicios independientes: Webhooks, reporting, analytics (microservicios)
  • Lección: No necesitas microservicios para ser global. Necesitas una BD bien diseñada y operaciones excelentes.

Netflix - El Pionero de Microservicios

Netflix fue de monolito (2008) a cientos de microservicios (2020).

  • Por qué: Escala masiva (500M+ suscriptores en 190 países)
  • Tecnología: Spring Boot + Kubernetes + Chaos Engineering
  • Lección: Microservicios te permiten innovar rápido y deployar independientemente. Pero requiere madurez operacional.

Amazon - Dos Pizzas

Amazon tiene una regla: un equipo debe poder ser alimentado con dos pizzas. Si es más grande, divide.

Esto llevó a:

  • Docenas de servicios (EC2, S3, RDS, DynamoDB, etc.)
  • Cada uno operado por un pequeño equipo
  • Cada uno con su propia BD, API, ciclo de release
  • Lección**: Equipos pequeños + servicios independientes = innovación rápida

GitHub - Estable en Monolito

GitHub es principalmente un monolito de Ruby on Rails.

  • Escala global pero único datastore principal
  • Servicios satelitales para search (Elasticsearch), actions (CI/CD)
  • Lección**: Si tu monolito es bien escrito y bien operado, no necesitas microservicios.

Sección 14: Decision Framework - Eligiendo Arquitectura

Aquí hay un framework que puedes usar para cualquier proyecto:

┌─────────────────────────────────────────┐
│ 1. ¿Cuál es el scope del proyecto?      │
│    (monolito vs múltiples servicios)    │
└────────────┬────────────────────────────┘

        ┌────▼────────────────────────────┐
        │ 2. ¿Cuál es la escala esperada? │
        │    (<1M, 1M-100M, >100M users)  │
        └────┬─────────────────────────────┘

        ┌────▼──────────────────────────────┐
        │ 3. ¿Cuántos equipos?              │
        │    (<5, 5-20, >20 equipos)        │
        └────┬───────────────────────────────┘

        ┌────▼────────────────────────────┐
        │ 4. ¿Cuáles son las criticidades?│
        │    (si algo cae, qué se rompe?) │
        └────┬─────────────────────────────┘

        ┌────▼──────────────────────────┐
        │ 5. ¿Cuál es el presupuesto?   │
        │    (infraestructura + personas)│
        └──────────────────────────────┘

Ejemplo: Sistema de Logística

1. Scope: Sistema complejo
   - Múltiples clientes (drivers, managers, customers)
   - Datos en tiempo real
   → Necesita modularidad

2. Escala: 1-100M users
   - Comienza con 10,000 conductores
   - Crece a 1 millón en 3 años
   → Horizontal scalability es crítica

3. Equipos: 5-10 inicialmente
   - Backend team
   - Frontend team
   - DevOps team
   → Necesita cierta independencia

4. Criticidad:
   - Si tracking se cae: clientes no ven dónde está paquete (malo pero tolerable)
   - Si órdenes se cae: nadie puede mandar paquetes (crítico)
   - Si routing se cae: rutas desoptimizadas (annoying pero funciona)
   → Orders es el corazón

5. Presupuesto: $2-5M anuales
   - Infraestructura: $800k
   - Equipo: $3M
   → Pueden hacer DevOps bien

DECISIÓN:
- Core monolito modular para Orders + Inventory
- Microservicios independientes para: Tracking, Routing, Notifications
- Multi-BFF para drivers app, manager dashboard, customer tracking
- Event-driven para comunicación

Conclusión

Hemos viajado desde “¿qué es arquitectura?” hasta decisiones que toman empresas de miles de millones de dólares.

La realidad es esta: No hay arquitectura perfecta. Solo hay trade-offs.

Monolito es simple pero no escala. Microservicios escalan pero son complejos. Serverless es barato pero lento. Caches son rápidos pero fallan.

Lo que diferencia a un arquitecto junior de uno senior es esto:

  1. Conocer los trade-offs (en tu cabeza, no en documentos)
  2. Saber cuándo cambiar (antes de que el sistema explote, pero no antes de tiempo)
  3. Comunicar decisiones (a ingenieros, managers, ejecutivos)
  4. Hacer pragmático (perfecto es enemigo de hecho)

La próxima vez que alguien te diga “debemos usar microservicios,” pregunta:

  • ¿Por qué?
  • ¿Cuándo lo sabremos?
  • ¿Quién lo mantiene?
  • ¿Cuánto cuesta?

Las respuestas revelan si es arquitectura bien pensada o moda.


Recursos Adicionales

  • “Designing Data-Intensive Applications” - Martin Kleppmann (lectura obligatoria)
  • “Building Microservices” - Sam Newman
  • “Release It!” - Michael Nygard (estabilidad y operaciones)
  • C4 Model: c4model.com
  • Kafka Streams Architecture: kafka.apache.org
  • Event Sourcing: martinfowler.com/eaaDev/EventSourcing.html

Última nota: Cuando diseñes un sistema nuevo, empieza simple. Diseña como si fuera a durar 5 años. Porque probablemente durará 20.

Tags

#arquitectura #diseño-sistemas #c4 #microservicios #enterprise #software-design #patrones #business-architecture