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