La Idea Central

La arquitectura basada en celdas divide tu sistema en unidades independientes y autónomas llamadas celdas. Cada celda sirve un subconjunto de tenants y contiene toda la infraestructura necesaria para operar independientemente, sus propias bases de datos, caches, colas y compute.

 ┌─────────────┐
 │ Router │
 │ (Asignación│
 │ de Celdas) │
 └──────┬──────┘
 │
 ┌────────────┼────────────┐
 │ │ │
 ┌─────▼─────┐ ┌───▼─────┐ ┌───▼─────┐
 │ Celda A │ │ Celda B │ │ Celda C │
 │ ┌───────┐ │ │┌───────┐│ │┌───────┐│
 │ │API │ │ ││API ││ ││API ││
 │ │DB │ │ ││DB ││ ││DB ││
 │ │Cache │ │ ││Cache ││ ││Cache ││
 │ │Cola │ │ ││Cola ││ ││Cola ││
 │ └───────┘ │ │└───────┘│ │└───────┘│
 └───────────┘ └─────────┘ └─────────┘

La propiedad crítica: las celdas no comparten nada. Si la base de datos de la Celda A se cae, las celdas B y C siguen operando.

Enrutamiento de Celdas

const CELL_ENDPOINTS = {
 0: 'https://cell-a.internal.example.com',
 1: 'https://cell-b.internal.example.com',
 2: 'https://cell-c.internal.example.com',
 3: 'https://cell-d.internal.example.com',
};

function assignCell(tenantId) {
 const hash = fnv1a(tenantId);
 return hash % Object.keys(CELL_ENDPOINTS).length;
}

export default {
 async fetch(request) {
 const tenantId = extractTenantId(request);
 const cellIndex = assignCell(tenantId);
 const cellUrl = CELL_ENDPOINTS[cellIndex];

 const cellRequest = new Request(
 `${cellUrl}${new URL(request.url).pathname}`,
 request,
 );

 try {
 const response = await fetch(cellRequest, { cf: { timeout: 10000 } });
 response.headers.set('X-Cell', `cell-${cellIndex}`);
 return response;
 } catch (error) {
 return new Response('Servicio temporalmente no disponible', { status: 503 });
 }
 },
};

Shuffle Sharding: Reduciendo el Radio de Explosión

import hashlib
import random

def shuffle_shard(tenant_id, num_cells, shard_size=2):
 seed = int(hashlib.sha256(tenant_id.encode()).hexdigest(), 16)
 rng = random.Random(seed)
 cells = list(range(num_cells))
 rng.shuffle(cells)
 return cells[:shard_size]

# Con 8 celdas y shards de 2 celdas:
# - Fallo de una celda afecta: 25% de tenants como máximo
# - Ese es tenants hacen failover a su celda secundaria
# - Dos fallos simultáneos afectan: ~3.6% de tenants

AWS Route 53 usa este patrón. Cada zona DNS se asigna a 4 de sus 2.048 shards virtuales. La probabilidad de que un "vecino ruidoso" (un DDoS en una zona) afecte a otra zona específica es insignificantemente pequeña.

Infraestructura de Celdas con Terraform

variable "cell_id" { type = string }
variable "cell_index" { type = number }

resource "aws_vpc" "cell" {
 cidr_block = "10.${var.cell_index}.0.0/16"
 tags = { Name = "cell-${var.cell_id}" }
}

resource "aws_rds_cluster" "cell_db" {
 cluster_identifier = "cell-${var.cell_id}-db"
 engine = "aurora-postgresql"
 engine_version = "16.1"
 tags = { Cell = var.cell_id }
}

resource "aws_elasticache_replication_group" "cell_cache" {
 replication_group_id = "cell-${var.cell_id}-redis"
 node_type = "cache.r7g.large"
 num_cache_clusters = 2
 tags = { Cell = var.cell_id }
}

Los VPCs están aislados, las celdas no pueden comunicarse accidentalmente entre sí. Esto es deliberado.

Estrategia de Deployment: Celdas Canary

stages:
 - name: canary-cell
 cells: ["cell-a"]
 validation:
 - health_check: 5m
 - error_rate: < 0.1%
 - latency_p99: < 200ms
 rollback_on_failure: true

 - name: second-wave
 cells: ["cell-b"]
 depends_on: canary-cell
 delay: 15m

 - name: remaining
 cells: ["cell-c", "cell-d"]
 depends_on: second-wave
 delay: 30m

Así es exactamente como un incidente de migración de base de datos funciona con celdas: la migración corre en Celda A, se rompe, se hace rollback de Celda A, y las celdas B/C/D nunca ven la migración mala. En vez de 45 minutos de downtime total, son 5 minutos de outage parcial para un cuarto de los usuarios.

Monitoreo Por Celda

groups:
 - name: cell_metrics
 rules:
 - record: cell:request_rate:5m
 expr: sum(rate(http_requests_total{job="api"}[5m])) by (cell)
 - record: cell:error_rate:5m
 expr: |
 sum(rate(http_requests_total{status=~"5.."}[5m])) by (cell)
 / sum(rate(http_requests_total[5m])) by (cell)
 - name: cell_alerts
 rules:
 - alert: CellHighErrorRate
 expr: cell:error_rate:5m > 0.01
 for: 5m
 labels:
 severity: critical

La label cell en cada métrica no es negociable. Sin ella, no es posible distinguir si un spike de error rate es un problema global o de una sola celda.

Trade-offs y Costos Operacionales

Costo: Correr N copias de todo significa que 4 celdas es aproximadamente 4x el costo de base de datos. En la práctica, las celdas agregan alrededor de 40% a los costos de infraestructura.

Complejidad operacional: Cada procedimiento operacional necesita un paso de "¿qué celda?". Migraciones de base de datos, cambios de schema, actualizaciones de config, todo necesita ser cell-aware.

Migración de datos: Mover un tenant entre celdas es la operación más difícil. Requiere una pausa de escritura coordinada, copia de datos y actualización de routing.

Features globales: Algunas cosas inherentemente cruzan celdas, búsqueda global, analytics cross-tenant. Necesitan una capa de agregación separada que lee de todas las celdas.

Para Cerrar

La arquitectura basada en celdas es la opción nuclear para aislamiento de radio de explosión. Funciona, los sistemas pasan de "un mal deployment tumba todo" a "un mal deployment afecta 25% de usuarios por 5 minutos", pero el costo operacional es real.

Se recomienda comenzar de forma simple: circuit breakers, bulkheads, deployments multi-AZ. Conviene avanzar hacia celdas cuando un dominio de fallo único que afecta a todos los usuarios se convierte en un riesgo existencial para el negocio. Al implementar celdas, es recomendable comenzar con 4, mantenerlas verdaderamente independientes, e invertir fuertemente en observabilidad por celda.

La arquitectura es la parte fácil. Convivir con ella es la parte difícil.