Identidad: idFromName frente a newUniqueId
El binding de un Durable Object no es un objeto: es un namespace, una fábrica de instancias. Para alcanzar un objeto concreto necesitas su id, y hay dos formas de acuñarlo. idFromName deriva un id determinista a partir de un nombre: el mismo nombre da siempre el mismo objeto, calculado sin coordinación desde cualquier rincón del planeta. newUniqueId acuña una instancia nueva e irrepetible cuando no hay un nombre natural. Elegir entre ambos, y elegir el nombre, es el acto de diseño más profundo del nivel: decidir por qué entidad particionas tu coordinación.
Ya sabes qué es un Durable Object: una instancia única con nombre y estado. Falta la mecánica de su nombre. El binding que declaraste, env.SALA, no es una sala: es un namespace, un directorio de todas las salas posibles. Para llegar a una concreta necesitas su identidad, y ahí se abre una bifurcación cargada de sentido: puedes derivar el id de un nombre que ya tienes —determinista, reproducible, sin acuerdo previo— o acuñar uno nuevo que jamás se repetirá. Esa elección no es un detalle de API; es donde decides cómo se reparte tu mundo en actores.
- Distinguir el namespace (
env.SALA) del objeto: uno es el directorio, el otro la instancia. - Usar
idFromNamepara derivar un id determinista y estable a partir de un nombre. - Usar
newUniqueIdpara acuñar una instancia nueva e irrepetible cuando no hay nombre natural. - Aplicar el patrón “un objeto por recurso”: elegir la clave por la que particionas tu coordinación.
El namespace no es el objeto
El binding env.SALA es de tipo DurableObjectNamespace. No representa una sala, sino el conjunto de todas las salas: es una fábrica y un directorio a la vez. De él no sales directamente a un objeto; primero calculas una identidad, un DurableObjectId, y con ella pides el stub, que ya es el asa viva hacia una instancia concreta.
const id = env.SALA.idFromName("sala-42");
const stub = env.SALA.get(id);
Son dos pasos y conviene entender por qué. La identidad —qué objeto— se separa de la conexión —un asa hacia él—. El id es un valor opaco y transportable: puedes calcularlo, compararlo, guardarlo, pasarlo entre funciones, todo sin tocar aún el objeto. Solo cuando haces get(id) obtienes el stub por el que hablarás con la instancia. Esa separación es la que permite que dos Workers cualesquiera lleguen al mismo objeto: si calculan el mismo id, comparten el mismo destino.
idFromName: identidad determinista
idFromName(nombre) transforma una cadena en un id de forma determinista: la misma cadena produce siempre el mismo id, y por tanto el mismo objeto. Y —esto es lo decisivo— ese cálculo no requiere ninguna coordinación. Un Worker en Sídney y otro en Oslo, sin hablarse, sin consultar un registro, calculan idFromName("sala:42") y ambos aterrizan en la misma instancia. El nombre es la cita a ciegas donde todos se encuentran.
// dos Workers distintos, misma sala, mismo objeto exacto
const id = env.SALA.idFromName(`sala:${salaId}`);
const stub = env.SALA.get(id);
await stub.entrar(usuario);
No hay un “crear la sala” explícito: la nombras y existe en su primer uso. Nombrar es idempotente —invocar idFromName("sala:42") un millón de veces da siempre el mismo objeto—, así que no hay carrera de creación ni duplicados posibles. La convención sabia es prefijar el nombre por su tipo, sala:${id} o usuario:${uid}, para que los espacios de nombres de conceptos distintos no colisionen dentro del mismo namespace.
Piensa en lo que te ahorra el determinismo. Sin él, para que dos servidores coincidieran en “quién atiende la sala 42” harías falta un directorio compartido: una tabla que mapea salas a nodos, con su consistencia, su latencia y su punto único de fallo. idFromName disuelve ese directorio en una función pura. La respuesta a “dónde vive la sala 42” no se consulta, se calcula, y el cálculo da igual en todas partes. Es descubrimiento de servicio sin servicio de descubrimiento.
newUniqueId: una instancia recién nacida
A veces no hay un nombre natural. Quieres, sencillamente, un objeto nuevo, garantizado distinto de todos los demás: una sesión efímera, un documento recién creado, una partida que empieza. Para eso está newUniqueId, que acuña un id fresco que nunca se repetirá.
const id = env.SESION.newUniqueId();
const stub = env.SESION.get(id);
// el id no se deriva de nada conocido: guardalo si querras volver
await env.KV.put(`sesion:${usuario}`, id.toString());
Aquí aparece la contrapartida. Como el id no se deriva de nada que tú conozcas, si quieres volver a ese mismo objeto más tarde tienes que persistirlo tú: guardas su representación textual con id.toString() y luego lo reconstruyes con env.SESION.idFromString(hex). newUniqueId te da unicidad garantizada a cambio de cargar tú con la memoria del nombre; idFromName hace justo lo contrario: el nombre es la memoria, y no tienes nada que guardar.
// mas tarde, recuperar aquel objeto desde el id que guardaste
const hex = await env.KV.get(`sesion:${usuario}`);
const id = env.SESION.idFromString(hex);
const stub = env.SESION.get(id);
Pregúntate una sola cosa: ¿la entidad ya tiene un nombre en tu dominio? Si el objeto corresponde a algo que puedes nombrar de forma estable —un usuario por su id, una sala por su código, un documento por su slug—, usa idFromName y deja que el nombre haga de dirección. Si el objeto es una instancia nueva sin identidad previa que otros necesiten recalcular, usa newUniqueId y guarda su id donde lo vayas a necesitar. En la práctica, idFromName es el caso mayoritario, porque casi todo lo que coordinas ya tiene nombre.
Antes de seguir, ten a la vista el juego completo de operaciones con las que manejas la identidad de un objeto:
idFromName
Deriva un id determinista de un nombre. Mismo nombre, mismo objeto, sin coordinación. Es el caso mayoritario.
newUniqueId
Acuña un id nuevo e irrepetible. Para instancias sin nombre natural; guarda tú el id si querrás volver a ellas.
idFromString
Reconstruye un id desde su texto. Es la vuelta a un objeto cuyo id guardaste antes con toString.
get(id)
Del id al stub: el asa viva por la que de verdad hablas con la instancia.
Un objeto por sala, por usuario, por recurso
Aquí está el corazón de diseño del nivel. Elegir el nombre es elegir la partición. Cuando dices “un objeto por sala” o “un objeto por usuario”, estás decidiendo la unidad por la que se reparte tu coordinación, y esa decisión gobierna tanto la corrección como la escala. Cada objeto es una isla de estado serializado y consistente; nombrar por la entidad correcta trocea un problema global en millones de problemas diminutos e independientes.
flowchart TB B[env.SALA namespace] --> N1[idFromName sala-1] --> D1[DO sala-1] B --> N2[idFromName sala-2] --> D2[DO sala-2] B --> N3[idFromName sala-3] --> D3[DO sala-3] style D1 fill:#89b4fa,color:#11111b style D2 fill:#89b4fa,color:#11111b style D3 fill:#89b4fa,color:#11111b
El grano es todo. Si eliges un grano demasiado grueso —un único objeto para toda la app— creas un cuello de botella que serializa el mundo entero. Si lo eliges demasiado fino, partes la coordinación justo donde la necesitabas junta: dos jugadores de la misma sala en objetos distintos no se ven. El arte consiste en hacer coincidir el objeto con la frontera natural de consistencia de tu dominio: aquello que de verdad tiene que verse y ordenarse en conjunto, y nada más.
Ejemplos que aciertan el grano abundan: un objeto por documento colaborativo, donde todos sus editores convergen y dos documentos distintos no se estorban; un objeto por partida, donde los jugadores de una comparten tablero y los de otra corren aparte; un objeto por cuenta, donde los movimientos de una se serializan y los de otra van en paralelo. En cada caso, el nombre del objeto es la respuesta a una sola pregunta: qué cosas tienen que ponerse de acuerdo entre sí.
Detrás de la humilde elección entre idFromName y newUniqueId late una de las ideas más hondas de los sistemas distribuidos, disfrazada de detalle de API. Durante décadas, coordinar estado entre muchos nodos exigió maquinaria pesada: un servicio de descubrimiento que dijera quién es el dueño de qué, un algoritmo de consenso que eligiera un líder, una tabla de particiones custodiada con su propia consistencia. Todo ese aparato existe para responder una única pregunta —¿a quién le hablo para tocar esta porción de estado?— y todo él es frágil, caro y difícil de razonar. El Durable Object lo colapsa en una función determinista. idFromName convierte “quién es el dueño de la sala 42” en un cálculo que cualquiera hace en solitario y que da idéntico en todas partes, sin registro, sin líder, sin acuerdo. La consecuencia es que el reparto de tu sistema deja de ser un componente de infraestructura y se vuelve una decisión de nomenclatura: eliges el nombre y, con él, quedan fijadas a la vez la identidad, la partición y la frontera de consistencia. Por eso la pregunta que de verdad importa cuando diseñas con Durable Objects no es técnica sino casi ontológica: ¿cuál es la unidad natural de coordinación de mi dominio? Una sala, un usuario, un carrito, un documento, un tenant. Aciértala y la app se reparte sola en millones de actores independientes que escalan en paralelo; equivócala —un objeto global para todo, o un objeto por cosa que en realidad debía verse junta— y ninguna optimización posterior te salvará, porque el error no está en el código sino en dónde trazaste la frontera. Nombrar bien no es poner etiquetas: es diseñar el sistema.
- Escribe el código que, desde un Worker, obtenga el
stubde la sala cuyo código llega en la URL usandoidFromNamecon un nombre prefijado por tipo. - Crea una sesión con
newUniqueId, guarda su id en KV asociado al usuario y demuestra que puedes recuperar el mismo objeto después conidFromString. - Elige una app real y decide la unidad de coordinación de tres de sus funciones. Justifica el grano: por qué ni más grueso ni más fino.
- Explica por qué
idFromNamehace innecesario un registro central de “quién atiende qué”, y qué problema clásico de los sistemas distribuidos evita con ello.