Casos: coordinación, contadores exactos, rate limiting y cerrojos
Cuatro patrones canónicos donde el Durable Object gana su sueldo, y la única idea que los une. Coordinación: un objeto por sala o por cola como punto de encuentro que ordena a los participantes. Contadores exactos: incrementos que no se pierden porque el leer-modificar-escribir es atómico. Rate limiting por usuario: un cubo de fichas por identidad, exacto y particionado. Cerrojos distribuidos: el objeto como punto único de serialización con nombre. Todos son la misma figura —identidad, un solo hilo y estado coubicado— vestida de cuatro maneras.
Ya tienes las tres propiedades que definen a un Durable Object: identidad global, ejecución de un solo hilo y estado coubicado con el cómputo. Este cierre las pone a trabajar. Vas a ver cuatro patrones que aparecen una y otra vez en producción —coordinar, contar sin errar, limitar por usuario, bloquear en exclusiva— y descubrirás que no son cuatro trucos distintos, sino una sola figura repetida. Aprender a reconocer esa figura es el objetivo final: saber, ante un problema nuevo, si lo que pide es precisamente un actor con nombre.
- Usar un objeto por sala como punto de encuentro para coordinar participantes y colas.
- Construir un contador exacto que no pierde incrementos gracias a la serialización.
- Implementar un rate limiter por usuario, particionado por identidad.
- Reconocer el objeto como cerrojo distribuido natural y cuándo conviene.
Coordinación
Un objeto por sala o por cola: el punto de encuentro que ordena a los participantes y guarda el estado compartido.
Contador exacto
Incrementos que nunca se pierden, porque leer-modificar-escribir es atómico dentro del objeto.
Rate limiting
Un cubo de fichas por usuario: exacto por el hilo único, escalable por estar particionado por identidad.
Cerrojo
El objeto como punto único de serialización con nombre: quien tiene el cerrojo es lo que el objeto decide.
Coordinación: salas y colas
El caso insignia. Una sala —de chat, de juego, de edición colaborativa— es un estado que muchos tocan a la vez y que todos deben ver igual. El objeto por sala es el punto de encuentro: guarda el estado compartido y la lista de participantes, y como es de un solo hilo, cada entrada, cada salida y cada mensaje se aplican en un orden bien definido. Nadie ve una versión inconsistente porque hay un único lugar que decide la secuencia.
export class Sala extends DurableObject {
async entrar(usuario: string): Promise<string[]> {
const miembros = (await this.ctx.storage.get<string[]>("miembros")) ?? [];
if (!miembros.includes(usuario)) miembros.push(usuario);
await this.ctx.storage.put("miembros", miembros);
return miembros;
}
}
Una cola de trabajo es la misma idea con otra ropa: un objeto que serializa a productores y consumidores en un punto, de modo que los turnos se reparten en orden y sin duplicados. Donde haga falta que muchos converjan y se ordenen, el objeto es el árbitro natural.
Contadores exactos
Ya viste por qué un contador en un almacén replicado pierde incrementos bajo concurrencia. El objeto lo cura de raíz: el leer-modificar-escribir es atómico dentro de él, así que la cuenta es exacta por construcción. Esto no es un lujo estético; es una propiedad de corrección que un almacén eventualmente consistente no puede darte y que aquí sale gratis.
export class Inventario extends DurableObject {
async reservar(n: number): Promise<boolean> {
const libres = (await this.ctx.storage.get<number>("libres")) ?? 0;
if (libres < n) return false;
await this.ctx.storage.put("libres", libres - n);
return true;
}
}
Fíjate en que aquí “exacto” es literalmente el requisito: unidades de inventario, asientos de un vuelo, entradas de un concierto, cuota consumida. Vender el mismo asiento dos veces por una carrera no es un redondeo tolerable, es un fallo de negocio. El objeto garantiza que la comprobación y el descuento ocurren como un solo acto indivisible.
Rate limiting por usuario
Un limitador de tasa quiere responder, muchas veces por segundo, si una petición cabe dentro del presupuesto de un usuario. El patrón clásico es un cubo de fichas, y el objeto lo hace natural: un Durable Object por usuario o por clave de API sostiene el cubo, y como es de un solo hilo, comprobar y descontar fichas es atómico —no hay doble gasto—.
const CAP = 100; // fichas maximas
const TASA = 0.001; // fichas por milisegundo
export class Limite extends DurableObject {
async permitir(coste: number): Promise<boolean> {
const ahora = Date.now();
const b = (await this.ctx.storage.get<{ f: number; t: number }>("b")) ??
{ f: CAP, t: ahora };
b.f = Math.min(CAP, b.f + (ahora - b.t) * TASA);
b.t = ahora;
const ok = b.f >= coste;
if (ok) b.f -= coste;
await this.ctx.storage.put("b", b);
return ok;
}
}
La magia doble: exacto por el hilo único, escalable por la partición. Como hay un objeto por usuario, el limitador de cada uno es independiente y corre en paralelo con el de todos los demás. No existe un cuello de botella global que decida por el mundo entero; la identidad reparte la carga en tantos limitadores diminutos como usuarios haya.
Cerrojos distribuidos
Un cerrojo distribuido es la esencia misma del objeto llevada a su forma más pura. Como es un punto único de serialización con nombre, “quién tiene el cerrojo” es, sin más, lo que el objeto decide: solo una llamada a la vez lo toca, así que solo una puede ganar. No necesitas un sistema externo de coordinación; el actor ya es la sección crítica.
export class Cerrojo extends DurableObject {
async tomar(quien: string, ttlMs: number): Promise<boolean> {
const ahora = Date.now();
const d = await this.ctx.storage.get<{ due: string; hasta: number }>("d");
if (d && d.hasta > ahora && d.due !== quien) return false;
await this.ctx.storage.put("d", { due: quien, hasta: ahora + ttlMs });
return true;
}
}
El matiz maduro es el arrendamiento con caducidad: en vez de un cerrojo eterno que un cliente caído dejaría bloqueado para siempre, das el cerrojo por un tiempo —un ttl— que expira solo. Y recuerda el coste: el objeto es un punto único, así que úsalo para coordinar, no para cuellos de botella de altísimo tráfico donde su serialización se volvería el límite.
Reduce los cuatro a su esqueleto y aparece una sola figura: una porción de estado compartido a la que le pones un dueño con nombre, y que desde ahí resuelve cualquiera de los cuatro problemas.
flowchart TB P[porcion de estado compartido] --> DO[Durable Object con nombre] DO --> C1[coordinar una sala] DO --> C2[contar sin errar] DO --> C3[limitar por usuario] DO --> C4[bloquear en exclusiva] style DO fill:#cba6f7,color:#11111b
Vuelve la vista atrás sobre los cuatro y verás que no son cuatro herramientas, sino una sola idea con cuatro disfraces. La sala coordina participantes, el contador coordina incrementos, el limitador coordina el gasto de un presupuesto, el cerrojo coordina el acceso a un recurso; en los cuatro, el problema real es idéntico —hay una porción de estado que varios actores tocan a la vez y que exige un único punto de verdad que ponga orden— y la solución es idéntica: darle a esa porción un dueño con nombre que la posea en exclusiva y la toque en un solo hilo. Esta es la abstracción que hay que dejar impresa, porque reescribe una familia entera de problemas que la industria resolvía con andamiajes pesados. Durante años, “contador exacto distribuido” significaba pelear con la consistencia de un almacén replicado; “rate limiting” significaba montar Redis con scripts atómicos; “cerrojo distribuido” invocaba a ZooKeeper, a etcd, a Consul, sistemas de consenso enteros erigidos para decidir quién manda. El Durable Object colapsa todo ese aparato en un gesto de una sencillez casi insultante: llamar a un método sobre un objeto con nombre. Y la jugada maestra que lo hace escalar es el reparto por identidad. Un contador global sería un cuello de botella; un millón de contadores, uno por entidad, son un millón de puntos de coordinación independientes que corren en paralelo. La serialización local, que garantiza la corrección, y la partición por nombre, que garantiza la escala, son las dos caras de la misma moneda. Por eso el ingeniero que ha interiorizado este nivel deja de preguntarse “qué sistema de coordinación instalo” y empieza a preguntarse “cuál es la porción de estado que debe coordinarse, y qué nombre le doy”. Cuando ese cambio de pregunta se vuelve reflejo, ya no ves salas, contadores, límites y cerrojos como problemas distintos: ves la misma figura, el actor con identidad, esperando a que la nombres.
- Implementa una
Salaque mantenga la lista de miembros y aplica entradas y salidas concurrentes; comprueba que el orden final es coherente. - Escribe un
Inventarioque reserve unidades y demuestra, bajo carga concurrente, que jamás vende más de las que hay. - Construye un rate limiter de cubo de fichas por usuario y razona por qué tener un objeto por usuario evita un cuello de botella global.
- Diseña un cerrojo con arrendamiento por
ttly explica qué desastre evita la caducidad frente a un cerrojo eterno. - Toma un problema real de tu trabajo y decide si es “la misma figura”: qué porción de estado debe coordinarse y qué nombre le darías al objeto que la posee.