Arquitectura basada en celdas para sistemas distribuidos resilientes
Diseña arquitecturas por celdas que aíslan fallos en radios pequeños, permiten escalado independiente y evitan caídas en cascada en sistemas distribuidos.

Cuando una sola migración de base de datos derriba toda tu plataforma, o un despliegue defectuoso afecta a todos los usuarios a la vez, el problema no es la migración ni el despliegue: es la arquitectura. La arquitectura basada en celdas resuelve esto particionando tu sistema en celdas independientes, cada una atendiendo a un subconjunto de usuarios con su propia infraestructura aislada.
Si la celda 7 tiene un despliegue defectuoso, solo se ven afectados los usuarios enrutados a la celda 7. Las demás celdas siguen funcionando con normalidad. Este patrón impulsa algunos de los sistemas más fiables a gran escala.
Qué define a una celda
Una celda es una copia completa e independiente de tu stack de servicios que maneja un subconjunto del tráfico. Cada celda tiene su propio cómputo, almacenamiento, cachés y colas. Las celdas no comparten nada entre sí en tiempo de ejecución.
interface Cell {
id: string;
region: string;
capacity: number; // max tenants or users
currentLoad: number;
services: CellService[];
status: "healthy" | "degraded" | "draining" | "offline";
}
interface CellService {
name: string;
instances: number;
database: string; // Cell-dedicated database
cache: string; // Cell-dedicated cache cluster
messageQueue: string; // Cell-dedicated queue
}
// ❌ Shared infrastructure — single point of failure
const sharedSetup = {
apiServers: "api-cluster (all users)",
database: "main-db (single instance, all data)",
cache: "redis-cluster (shared)",
// One bad query, one migration, one cache flush → everyone affected
};// ✅ Cell-isolated infrastructure — blast radius contained
const cells: Cell[] = [
{
id: "cell-01",
region: "us-east-1",
capacity: 10000,
currentLoad: 7500,
status: "healthy",
services: [
{
name: "api",
instances: 4,
database: "cell-01-postgres",
cache: "cell-01-redis",
messageQueue: "cell-01-sqs",
},
],
},
{
id: "cell-02",
region: "us-east-1",
capacity: 10000,
currentLoad: 6200,
status: "healthy",
services: [
{
name: "api",
instances: 4,
database: "cell-02-postgres",
cache: "cell-02-redis",
messageQueue: "cell-02-sqs",
},
],
},
];El principio clave: las celdas no comparten nada en tiempo de ejecución. Ni bases de datos compartidas, ni cachés compartidas, ni colas compartidas. La comunicación entre celdas ocurre únicamente a través de canales asíncronos diseñados explícitamente.
Enrutamiento de celdas
Una capa de enrutamiento ligera dirige cada solicitud a la celda correcta según el ID de tenant, el ID de usuario u otra clave de partición estable. Este enrutador debe ser extremadamente fiable, ya que es el único componente compartido.
interface CellRouter {
routingTable: Map<string, string>; // tenantId → cellId
defaultCell: string;
}
class Router {
private assignments: Map<string, string>;
private cells: Map<string, Cell>;
constructor(cells: Cell[]) {
this.assignments = new Map();
this.cells = new Map(cells.map(c => [c.id, c]));
}
routeRequest(tenantId: string): string {
// Check existing assignment
const assigned = this.assignments.get(tenantId);
if (assigned) {
const cell = this.cells.get(assigned);
if (cell && cell.status === "healthy") {
return assigned;
}
// Cell is unhealthy — don't reroute automatically
// Drain explicitly to maintain data locality
if (cell && cell.status === "degraded") {
return assigned; // Still route, cell is partially working
}
}
// New tenant — assign to cell with lowest load ratio
return this.assignToCell(tenantId);
}
private assignToCell(tenantId: string): string {
let bestCell: Cell | null = null;
let bestRatio = Infinity;
for (const cell of this.cells.values()) {
if (cell.status !== "healthy") continue;
const ratio = cell.currentLoad / cell.capacity;
if (ratio < bestRatio) {
bestRatio = ratio;
bestCell = cell;
}
}
if (!bestCell) {
throw new Error("No healthy cells available");
}
this.assignments.set(tenantId, bestCell.id);
bestCell.currentLoad++;
return bestCell.id;
}
drainCell(cellId: string, targetCellId: string): string[] {
const movedTenants: string[] = [];
const targetCell = this.cells.get(targetCellId);
if (!targetCell || targetCell.status !== "healthy") {
throw new Error(`Target cell ${targetCellId} is not healthy`);
}
for (const [tenant, cell] of this.assignments) {
if (cell === cellId) {
this.assignments.set(tenant, targetCellId);
targetCell.currentLoad++;
movedTenants.push(tenant);
}
}
const sourceCell = this.cells.get(cellId);
if (sourceCell) {
sourceCell.status = "draining";
sourceCell.currentLoad = 0;
}
return movedTenants;
}
}La propia capa de enrutamiento debe ser stateless y leer las asignaciones desde un almacén rápido y replicado. Añade una latencia mínima: una sola consulta por solicitud.
Despliegues seguros con celdas
Las celdas habilitan estrategias de despliegue incremental que son imposibles con infraestructura compartida. Despliega en una celda, observa y luego extiende el despliegue progresivamente.
interface DeploymentPlan {
version: string;
stages: DeploymentStage[];
rollbackTriggers: RollbackTrigger[];
}
interface DeploymentStage {
cells: string[];
trafficPercentage: number;
observationPeriod: string;
successCriteria: SuccessCriterion[];
}
interface SuccessCriterion {
metric: string;
threshold: number;
comparison: "less_than" | "greater_than";
}
interface RollbackTrigger {
metric: string;
threshold: number;
window: string;
}
const deploymentPlan: DeploymentPlan = {
version: "v2.5.0",
stages: [
{
cells: ["cell-canary"],
trafficPercentage: 2,
observationPeriod: "30m",
successCriteria: [
{ metric: "error_rate", threshold: 0.01, comparison: "less_than" },
{ metric: "p99_latency_ms", threshold: 500, comparison: "less_than" },
],
},
{
cells: ["cell-01", "cell-02"],
trafficPercentage: 20,
observationPeriod: "1h",
successCriteria: [
{ metric: "error_rate", threshold: 0.005, comparison: "less_than" },
{ metric: "p99_latency_ms", threshold: 400, comparison: "less_than" },
],
},
{
cells: ["cell-03", "cell-04", "cell-05", "cell-06"],
trafficPercentage: 60,
observationPeriod: "2h",
successCriteria: [
{ metric: "error_rate", threshold: 0.005, comparison: "less_than" },
{ metric: "p99_latency_ms", threshold: 400, comparison: "less_than" },
],
},
{
cells: ["cell-07", "cell-08", "cell-09", "cell-10"],
trafficPercentage: 100,
observationPeriod: "4h",
successCriteria: [
{ metric: "error_rate", threshold: 0.005, comparison: "less_than" },
{ metric: "p99_latency_ms", threshold: 400, comparison: "less_than" },
],
},
],
rollbackTriggers: [
{ metric: "error_rate", threshold: 0.02, window: "5m" },
{ metric: "p99_latency_ms", threshold: 1000, window: "5m" },
],
};Si la celda canary muestra errores elevados, reviertes una sola celda mientras el 98 % de los usuarios no nota ningún impacto. Compara eso con revertir un despliegue monolítico que afecta a todo el mundo.
Patrones de datos entre celdas
La parte difícil de la arquitectura de celdas es manejar datos que abarcan varias celdas. La comunicación entre usuarios, los datos de referencia compartidos y la analítica necesitan estrategias entre celdas.
interface CrossCellPattern {
pattern: string;
useCase: string;
tradeoff: string;
}
const crossCellPatterns: CrossCellPattern[] = [
{
pattern: "Async replication of reference data",
useCase: "Product catalog, feature flags, configuration",
tradeoff: "Eventually consistent — cells may see stale data briefly",
},
{
pattern: "Event bus for cross-cell notifications",
useCase: "User A in cell-01 messages User B in cell-03",
tradeoff: "Adds latency vs direct call, but preserves isolation",
},
{
pattern: "Global read replica for analytics",
useCase: "Cross-cell reporting and dashboards",
tradeoff: "Read-only aggregate view, not suitable for transactional queries",
},
];
// Event-based cross-cell communication
interface CrossCellEvent {
sourceCell: string;
targetCell: string;
eventType: string;
payload: Record<string, unknown>;
timestamp: Date;
idempotencyKey: string;
}
function publishCrossCellEvent(event: CrossCellEvent): void {
// Publish to global event bus (SNS, EventBridge, Kafka)
// Target cell's consumer picks it up independently
// Idempotency key prevents duplicate processing
globalEventBus.publish({
topic: `cross-cell.${event.eventType}`,
message: event,
deduplicationId: event.idempotencyKey,
});
}Conclusiones clave
La arquitectura basada en celdas cambia simplicidad operativa por resiliencia. Cada celda es una unidad autocontenida con sus propios almacenes de datos, cómputo y colas, sin compartir nada en tiempo de ejecución. Una capa de enrutamiento ligera asigna tenants a celdas de forma permanente, creando localidad de datos y aislamiento de fallos. Los despliegues avanzan por las celdas de forma progresiva: primero el canary, luego olas cada vez más amplias, con rollback automático si las métricas de salud se degradan. Las necesidades entre celdas, como la mensajería y la analítica, fluyen por canales asíncronos, nunca por bases de datos compartidas. El resultado es un sistema donde el radio de impacto de cualquier fallo —un despliegue defectuoso, un problema de base de datos, un fallo de infraestructura— se limita a una sola celda en lugar de a toda la plataforma. Empieza con dos celdas y un enrutador. La disciplina de mantener un aislamiento real desde el primer día es lo que hace que el patrón funcione.


