TechLead
Leccion 30 de 30
8 min de lectura
Diseño de Sistemas

Compensaciones en el Diseño de Sistemas

Domina el análisis de compensaciones en diseño de sistemas cubriendo consistencia vs disponibilidad, SQL vs NoSQL, monolito vs microservicios y marcos de decisión.

Compensaciones en el Diseño de Sistemas

Cada decisión de diseño de sistemas involucra compensaciones. No hay arquitectura perfecta, solo arquitecturas optimizadas para restricciones específicas. Los ingenieros senior se distinguen no por conocer más tecnologías, sino por su capacidad de evaluar compensaciones sistemáticamente y tomar decisiones defendibles. Este tema cubre las compensaciones más importantes que encontrarás y marcos para evaluarlas.

Consistencia vs. Disponibilidad

El teorema CAP establece que en presencia de una partición de red, un sistema distribuido debe elegir entre consistencia y disponibilidad. En la práctica, las particiones son inevitables, así que la pregunta real es: cuando ocurre una partición, ¿retornas datos obsoletos (disponibilidad) o un error (consistencia)?

Elegir Consistencia (CP) Elegir Disponibilidad (AP)
Transacciones financieras Feeds de redes sociales
Gestión de inventario Navegación de catálogo de productos
Autenticación de usuarios Recomendaciones de contenido
Elección de líder Dashboards de analítica
Bloqueos distribuidos Resolución DNS

El Matiz del Mundo Real

La mayoría de sistemas reales no son puramente CP o AP. Usan diferentes niveles de consistencia para diferentes operaciones. Una plataforma de e-commerce podría usar consistencia fuerte para procesamiento de pagos (CP) pero consistencia eventual para reseñas de productos (AP). La idea clave es que la consistencia no es una propiedad del sistema completo; es una decisión por operación.

Consistencia Fuerte vs. Eventual

Este es un espectro, no una elección binaria. Entender las opciones entre los dos extremos es crucial.

// Consistency spectrum from strongest to weakest

enum ConsistencyLevel {
  // Linearizability: reads always see the most recent write
  LINEARIZABLE = "LINEARIZABLE",

  // Sequential consistency: operations appear in some total order
  SEQUENTIAL = "SEQUENTIAL",

  // Causal consistency: causally related operations are seen in order
  CAUSAL = "CAUSAL",

  // Read-your-writes: a client always sees its own writes
  READ_YOUR_WRITES = "READ_YOUR_WRITES",

  // Eventual consistency: all replicas converge eventually
  EVENTUAL = "EVENTUAL",
}

// Practical example: user profile update
interface UserProfileService {
  // Strong consistency: read from primary database
  updateProfile(userId: string, data: ProfileData): Promise<void>;
  getOwnProfile(userId: string): Promise<ProfileData>;

  // Eventual consistency: read from nearest replica
  getPublicProfile(userId: string): Promise<ProfileData>;
}

Latencia vs. Rendimiento

Optimizar para latencia (qué tan rápido completa una solicitud individual) a menudo conflictúa con optimizar para rendimiento (cuántas solicitudes por segundo maneja el sistema).

Optimizar para Latencia Optimizar para Rendimiento
Procesar cada solicitud inmediatamente Agrupar solicitudes en lotes para procesamiento
Mantener datos en memoria Escrituras secuenciales a disco (solo agregación)
Usar caché agresivamente Usar colas para suavizar picos de tráfico
Menos saltos de red Pipeline de operaciones entre servicios
Procesamiento en tiempo real Procesamiento por lotes / streams
// Example: Database write strategies

// Optimize for latency: write-through
async function writeThrough(key: string, value: string): Promise<void> {
  await database.write(key, value);  // 5ms
  await cache.set(key, value);       // 1ms
  // Total: ~6ms per write, but only handles ~160 writes/sec per connection
}

// Optimize for throughput: write-behind (buffering)
class WriteBuffer {
  private buffer: Map<string, string> = new Map();
  private flushInterval = 100; // ms

  async write(key: string, value: string): Promise<void> {
    this.buffer.set(key, value);
    await cache.set(key, value);
    // Total: ~1ms per write, can handle thousands of writes/sec
    // Trade-off: data loss risk if process crashes before flush
  }

  private async flush(): Promise<void> {
    const batch = Array.from(this.buffer.entries());
    this.buffer.clear();
    await database.batchWrite(batch);
  }
}

Monolito vs. Microservicios

Esta es quizás la compensación más debatida en arquitectura de software. La respuesta correcta depende en gran medida del tamaño del equipo, complejidad del sistema y estructura organizacional.

Dimensión Monolito Microservicios
Despliegue Unidad desplegable única Despliegue independiente por servicio
Velocidad de desarrollo (equipo pequeño) Más rápido - menos overhead Más lento - complejidad de infraestructura
Velocidad de desarrollo (equipo grande) Más lento - conflictos de merge, coordinación Más rápido - equipos trabajan independientemente
Depuración Más fácil - proceso único, stack traces locales Más difícil - trazado distribuido requerido
Escalado Escala toda la aplicación Escala servicios individuales independientemente
Consistencia de datos Fácil - base de datos única, transacciones ACID Difícil - transacciones distribuidas, consistencia eventual
Mejor para tamaño de equipo 1-20 ingenieros 50+ ingenieros con límites de dominio claros

El Punto Medio Pragmático

Comienza con un monolito modular: una sola unidad desplegable con límites internos de módulos bien definidos. Cuando un módulo específico necesite escalado independiente o un equipo separado, extráelo en un servicio. Este enfoque te da la simplicidad de un monolito con un camino a microservicios cuando surja la necesidad. La descomposición prematura en microservicios es uno de los errores arquitectónicos más comunes y costosos.

SQL vs. NoSQL

Factor SQL (PostgreSQL, MySQL) NoSQL (MongoDB, Cassandra, DynamoDB)
Modelo de datos Esquema rígido, relacional Esquema flexible, documento/clave-valor/columna
Flexibilidad de consultas Consultas ricas con JOINs, agregaciones Consultas limitadas, optimizadas para patrones de acceso específicos
Transacciones ACID completo entre múltiples tablas Limitado (documento o partición individual)
Escalado horizontal Posible pero complejo Integrado, a menudo automático
Mejor para Relaciones complejas, consultas ad-hoc, integridad de datos Alto rendimiento de escritura, esquemas flexibles, patrones de acceso conocidos
// Decision guide
function chooseDatabaseType(requirements: {
  needsJoins: boolean;
  needsACID: boolean;
  schemaIsStable: boolean;
  writeVolume: "low" | "medium" | "high" | "extreme";
  queryPatterns: "varied" | "known";
  dataRelationships: "simple" | "complex";
}): string {
  if (requirements.needsACID && requirements.needsJoins) {
    return "SQL (PostgreSQL recommended)";
  }

  if (requirements.writeVolume === "extreme" &&
      requirements.queryPatterns === "known" &&
      !requirements.needsJoins) {
    return "NoSQL (Cassandra for wide-column, DynamoDB for key-value)";
  }

  if (requirements.dataRelationships === "complex") {
    return "SQL - complex relationships are hard to model in NoSQL";
  }

  return "Either works - choose based on team experience";
}

Push vs. Pull Architectures

Aspecto Push (Fan-out en escritura) Pull (Fan-out en lectura)
Cuándo ocurre el trabajo Cuando se escriben datos Cuando se leen datos
Latencia de lectura Muy rápida (pre-computada) Más lenta (computada bajo demanda)
Costo de escritura Alto (fan-out a todos los seguidores) Bajo (solo almacenar el elemento)
Almacenamiento Mayor (datos duplicados en cada feed) Menor (copia única)
Mejor para Usuarios con pocos seguidores, lectura intensiva Usuarios celebridad con millones de seguidores

Enfoque Híbrido (Lo Que Usa Twitter)

Usa push para usuarios regulares (distribuir sus tweets a los feeds de seguidores al escribir) y pull para celebridades (obtener sus tweets al momento de lectura y fusionar). Esto evita el "problema de celebridades" donde un solo tweet de un usuario con 50 millones de seguidores requeriría 50 millones de operaciones de escritura.

Cómo Evaluar Compensaciones Sistemáticamente

Cuando enfrentes una decisión arquitectónica, usa este enfoque estructurado para evitar decisiones por instinto y sesgos.

interface ArchitecturalDecision {
  title: string;
  context: string;
  options: Option[];
  decision: string;
  rationale: string;
  consequences: string[];
}

interface Option {
  name: string;
  pros: string[];
  cons: string[];
  score: {
    complexity: number;
    scalability: number;
    reliability: number;
    cost: number;
    teamExperience: number;
  };
}

// Example ADR (Architecture Decision Record)
const example: ArchitecturalDecision = {
  title: "Database for user sessions",
  context: "We need to store user sessions with ~1M concurrent users, " +
           "sub-10ms read latency, and automatic expiration.",
  options: [
    {
      name: "Redis",
      pros: [
        "Sub-millisecond reads",
        "Built-in TTL for expiration",
        "Team has experience",
      ],
      cons: [
        "Data loss on restart (unless persistence enabled)",
        "Memory cost for 1M sessions",
      ],
      score: { complexity: 1, scalability: 4, reliability: 3, cost: 3, teamExperience: 5 },
    },
    {
      name: "PostgreSQL",
      pros: [
        "Durable storage",
        "Rich querying for analytics",
        "Already in our stack",
      ],
      cons: [
        "Higher latency (~5ms)",
        "Need to implement expiration (cron job or pg_cron)",
      ],
      score: { complexity: 2, scalability: 3, reliability: 5, cost: 2, teamExperience: 5 },
    },
  ],
  decision: "Redis",
  rationale: "Session data is ephemeral - durability is not required. " +
             "The sub-ms latency is critical for user experience. " +
             "Memory cost for 1M sessions (~500MB) is acceptable.",
  consequences: [
    "Must handle Redis failover (use Redis Sentinel or Cluster)",
    "Cannot perform complex queries on session data",
    "Need monitoring for memory usage",
  ],
};

Marco de Decisión para Arquitectos

El Marco STAR para Decisiones Técnicas

  • S - Situación: ¿Cuáles son las restricciones? Escala, tamaño del equipo, timeline, presupuesto, sistemas existentes
  • T - Compensaciones: ¿Qué ganas y qué pierdes con cada opción?
  • A - Acción: ¿Cuál es la recomendación y por qué? Haz la decisión reversible si es posible
  • R - Revisión: Establece una fecha para revisar la decisión. ¿Fue correcta? ¿Qué cambiarías?

Principios Clave

  • Optimiza para el caso común. Diseña para el percentil 95 de tu carga de trabajo, no los casos extremos
  • Haz las decisiones reversibles. Prefiere opciones que puedan cambiarse después sobre opciones que te encierren
  • La tecnología aburrida es buena tecnología. Usa herramientas bien entendidas a menos que tengas una razón convincente para no hacerlo
  • Mide, no asumas. Haz benchmarks antes de optimizar. Perfila antes de refactorizar. Prueba de carga antes de escalar
  • Documenta tus decisiones. Los futuros ingenieros (incluyendo tu yo futuro) necesitarán entender no solo qué construiste, sino por qué lo construiste así
  • No hay mejor arquitectura. Solo hay la mejor arquitectura para tus restricciones, requisitos y equipo específicos. El contexto es todo

Continuar Aprendiendo