Imponer unicidad sobre múltiples atributos en DynamoDB
DynamoDB garantiza la unicidad de exactamente una cosa: la clave primaria. No hay
restricción UNIQUE (email), ni UNIQUE (username), ni nada que abarque dos atributos.
Si vienes de SQL, esa ausencia es la primera sorpresa — y el primer sitio donde la gente
despliega en silencio una condición de carrera.
¿Cómo se impone una restricción de unicidad sobre múltiples atributos en DynamoDB?
DynamoDB no tiene restricción UNIQUE más allá de la clave primaria, así que la unicidad la gestionas tú: modela cada valor protegido como su propio item marcador cuya clave es ese valor, luego escribe el registro y todos los marcadores juntos en un único TransactWriteItems, con cada put protegido por attribute_not_exists. La colisión que el motor ya impone se convierte en tu restricción.
- No hay restricción de unicidad — solo la clave primaria está forzada a ser única por el motor. Cualquier otro atributo "debe ser único" es tu trabajo.
- Modela cada regla de unicidad como su propio item. Un item marcador dedicado cuya clave es el valor que proteges convierte "¿este email está ocupado?" en una colisión de clave que el motor ya impone.
- Escríbelos atómicamente con
TransactWriteItems. Una transacción, cada put protegido porattribute_not_exists, para que todos los marcadores y el registro real se confirmen juntos o ninguno lo haga. - No hagas check-then-write. Una lectura antes de insertar es una condición de carrera de manual; dos registros concurrentes ambos leen "libre" y ambos escriben.
Por qué el enfoque obvio está mal
El instinto es hacer un Query (o peor, un Scan) buscando el email, no ver nada, y
luego PutItem la nueva cuenta. Eso es una carrera de check-then-act.
Dos personas registran ada@lovelace.io en el mismo milisegundo. Ambas lecturas
devuelven vacío. Ambas escrituras tienen éxito. Ahora tienes dos cuentas con un email — y
nada en la tabla lo señala.
Un GSI sobre email tampoco te salva. Los GSI son
eventualmente consistentes, así que la lectura que controla tu
escritura puede estar obsoleta por diseño. La solución no es una comprobación más rápida;
es hacer que la escritura misma se niegue a aterrizar sobre un valor ocupado.
Modela cada restricción como un item marcador
El motor ya impone una regla de unicidad gratis: no puedes escribir dos items con la misma clave. Así que codifica cada regla de unicidad como una clave.
Junto al item real de cuenta, escribe un item marcador por cada atributo protegido. La clave de partición del marcador es el valor con espacio de nombres. Si el valor está ocupado, la clave existe, y un put protegido no puede sobrescribirlo.
Para un registro que debe mantener únicos tanto email como username, tres items se
mueven juntos — con clave en una disposición de tabla única (ver
single-table design):
| Item | PK | SK | Propósito |
|---|---|---|---|
| Registro de cuenta | ACCT#a1f9c3 | PROFILE | La cuenta real |
| Bloqueo de email | UNIQ#EMAIL#ada@lovelace.io | LOCK | Reserva el email |
| Bloqueo de username | UNIQ#HANDLE#ada | LOCK | Reserva el username |
La propia PK de la cuenta es un id generado (ACCT#a1f9c3) — nunca el email — para que
el usuario pueda cambiar su email después sin reescribir la clave primaria. Los items de
bloqueo no llevan datos de perfil; existen solo para que su clave esté ocupada.
Escribe los tres atómicamente
TransactWriteItems
aplica hasta 100 escrituras como una unidad de todo o nada. Protege cada put con
attribute_not_exists(PK) para que falle si esa clave ya está presente.
Si alguna condición falla — el bloqueo de email, el bloqueo de handle o la cuenta misma —
DynamoDB revierte la transacción entera y lanza TransactionCanceledException. Sin
registro parcial, sin bloqueo huérfano.
{
"TransactItems": [
{
"Put": {
"TableName": "accounts",
"Item": {
"PK": {"S": "ACCT#a1f9c3"},
"SK": {"S": "PROFILE"},
"email": {"S": "ada@lovelace.io"},
"username": {"S": "ada"}
},
"ConditionExpression": "attribute_not_exists(PK)"
}
},
{
"Put": {
"TableName": "accounts",
"Item": {
"PK": {"S": "UNIQ#EMAIL#ada@lovelace.io"},
"SK": {"S": "LOCK"}
},
"ConditionExpression": "attribute_not_exists(PK)"
}
},
{
"Put": {
"TableName": "accounts",
"Item": {
"PK": {"S": "UNIQ#HANDLE#ada"},
"SK": {"S": "LOCK"}
},
"ConditionExpression": "attribute_not_exists(PK)"
}
}
]
}La condición es todo el mecanismo. Sin attribute_not_exists, un segundo registro con el
mismo email sobrescribe en silencio el primer bloqueo. Con ella, el put se niega, la
transacción se cancela y tu app muestra "email ya en uso".
Lee el fallo, no lo adivines
Cuando la transacción se cancela, DynamoDB devuelve un array CancellationReasons
posicionalmente — una entrada por item, en orden de petición. Un ConditionalCheckFailed
en la posición 1 significa que el email está ocupado; la posición 2 significa que el
username lo está. Mapea la posición de vuelta a un error preciso a nivel de campo en
lugar de un genérico "registro fallido".
Inspecciona los bloqueos en DynoTable
Los items marcadores son invisibles en la UI de tu app — son fontanería. Cuando un registro falla misteriosamente, necesitas ver si el bloqueo realmente existe.
Abre la tabla en DynoTable y haz un Query del prefijo UNIQ#. La cuenta y sus dos items
de bloqueo están juntos, así que un registro atascado (un bloqueo dejado atrás por un
borrado chapucero) es obvio de un vistazo.

Mantén los bloqueos honestos al cambiar y borrar
Los bloqueos no son de una sola escritura. Reflejan el valor en vivo, así que el ciclo de vida tiene que mantenerlos sincronizados — cada operación que toca un atributo protegido es también una transacción.
- Cambiar email. Una transacción: pon el nuevo bloqueo
UNIQ#EMAIL#…conattribute_not_exists, borra el bloqueo antiguo, actualiza la cuenta. Misma garantía de todo o nada. - Borrar cuenta. Borra el item de cuenta y ambos items de bloqueo en una sola transacción, o dejarás varado un bloqueo que bloquea el valor para siempre.
- Reintenta de forma segura. Pasa un
ClientRequestTokenpara que una transacción reenviada (tras un corte de red) sea idempotente en lugar de una doble escritura.
La trampa es tratar el bloqueo como fire-and-forget. Un bloqueo creado en el registro pero nunca borrado al eliminar la cuenta es un valor que nadie podrá reutilizar — y no aparecerá hasta que un usuario real no pueda reclamar su propio handle antiguo.
Próximos pasos
Los marcadores de unicidad son un patrón de tabla única, así que se sitúan de forma natural
junto a tus otros items — lee single-table design para la
disposición de claves, y Query vs Scan para que nunca recurras a
un Scan para comprobar un bloqueo. El patrón se presentó por primera vez en la sesión de
AWS re:Invent / AWS Summit 2018 DAT374 — DynamoDB Transactions.
Redacta los puts protegidos por condición con el DynamoDB Expression Builder, luego prueba DynoTable para inspeccionar los items de bloqueo contra tu propia tabla.


