Saltar a contenido

Capítulo 13 — Transacciones y Batch Writes en Cloud Firestore

Recursos visuales propuestos

Antes de desarrollar este capítulo conviene distinguir qué recursos realmente aportan claridad pedagógica. En operaciones atómicas, muchas comparaciones iniciales se entienden mejor con esquemas simples y ejemplos de negocio. En cambio, la concurrencia, los reintentos y la secuencia de confirmación requieren diagramas técnicos más precisos.

Imágenes didácticas

  1. Comparación Transaction vs Batch Write. Conviene como imagen didáctica porque el lector necesita una diferencia clara y rápida entre ambos mecanismos antes de entrar en detalles internos.[cite:1]
  2. Ejemplo de actualización simultánea. Se recomienda como imagen didáctica porque ayuda a visualizar el problema de dos usuarios intentando modificar el mismo dato al mismo tiempo.[cite:2]
  3. Flujo de un Batch Write. Funciona como imagen didáctica porque muestra varias escrituras agrupadas sin necesidad de representar todavía el control de concurrencia.[cite:1]
  4. Casos de uso típicos. También es mejor como imagen didáctica porque permite asociar cada mecanismo con escenarios reales del proyecto del libro: inscripción, cupos, asistencia, credenciales y estadísticas.

Diagramas SVG

  1. Secuencia completa de una transacción. Debe ser SVG porque requiere mostrar lectura, validación, conflicto, reintento y commit con precisión temporal.[cite:1][cite:2]
  2. Control de concurrencia. Conviene SVG porque la contención de datos y el aislamiento serializable son conceptos internos que se entienden mejor con una secuencia estructurada.[cite:2]
  3. Flujo interno entre cliente, Firestore y confirmación de escritura. Requiere SVG por su carácter arquitectónico y por involucrar varios actores.
  4. Comparación arquitectónica entre Transaction y Batch Write. Debe ser SVG porque no basta con una analogía visual: es necesario mostrar la diferencia entre leer-antes-de-escribir y escribir-sin-leer.[cite:1][cite:2]

Objetivos de aprendizaje

Al finalizar este capítulo, el lector será capaz de:

  • Comprender qué es una operación atómica y por qué es indispensable para mantener consistencia en aplicaciones concurrentes.[cite:1]
  • Explicar qué problemas de concurrencia resuelven las transacciones y por qué existen en Firestore.[cite:2]
  • Distinguir con claridad cuándo conviene usar runTransaction() y cuándo conviene usar writeBatch().[cite:1]
  • Implementar operaciones críticas con la sintaxis modular moderna del SDK web de Firebase.
  • Entender el ciclo interno de una transacción, sus reintentos automáticos y sus límites técnicos.[cite:1][cite:2]
  • Analizar el impacto de transacciones y lotes sobre rendimiento, costos y escalabilidad.[cite:1]
  • Aplicar ambos mecanismos a escenarios reales del proyecto transversal del libro: inscripción de alumnos, asignación de docentes, actualización de cupos, registro de asistencia y generación de documentos relacionados.

Introducción

En una aplicación real, guardar datos no siempre consiste en ejecutar un setDoc() o un updateDoc() aislado. Muchas operaciones de negocio requieren que varios cambios ocurran juntos o que un cambio dependa del estado actual de uno o varios documentos. En esos escenarios, la pregunta técnica ya no es únicamente “cómo escribir”, sino “cómo garantizar consistencia cuando varios usuarios actúan al mismo tiempo”.

La documentación oficial de Firestore define dos tipos de operaciones atómicas: transacciones, que combinan lecturas y escrituras sobre uno o más documentos, y batched writes, que agrupan varias escrituras sobre uno o más documentos.[cite:1] En ambos casos, la atomicidad significa que todas las operaciones se aplican o ninguna se aplica.[cite:1]

Sin embargo, ambos mecanismos resuelven problemas distintos. Las transacciones son útiles cuando el valor nuevo depende del valor actual leído durante la propia operación; además, si otro cliente modifica un documento leído por la transacción, Firestore reintenta la operación completa automáticamente.[cite:1][cite:2] Los Batch Writes, en cambio, no leen documentos, por lo que tienen menos causas de fallo por concurrencia y pueden ejecutarse incluso cuando el dispositivo está offline.[cite:1]

Esta diferencia parece simple, pero tiene consecuencias enormes en arquitectura. Elegir mal entre transacción y lote puede producir inconsistencias, reintentos innecesarios, más costo, peor latencia o reglas de negocio frágiles. En este capítulo se estudiarán ambos mecanismos con profundidad, usando exclusivamente el SDK modular moderno y aplicando todos los ejemplos al proyecto oficial del libro.

Desarrollo completo

¿Qué es una operación atómica?

Una operación atómica es una operación que se ejecuta como una unidad indivisible: o todo se confirma correctamente, o nada se aplica. La documentación oficial lo expresa con claridad: en un conjunto de operaciones atómicas de Firestore, todas las operaciones tienen éxito o ninguna se aplica.[cite:1]

Esta propiedad es crucial en dominios donde un dato no puede cambiar de forma aislada sin comprometer el significado del resto. Si un alumno se inscribe a un grupo, no basta con crear un documento de inscripción; también puede ser necesario descontar un cupo disponible, registrar al usuario en una lista resumida o actualizar estadísticas. Si solo una parte se guarda y otra falla, la base queda incoherente.

La atomicidad, por tanto, no es un lujo. Es un mecanismo para proteger la integridad del modelo de negocio.

¿Por qué existen las transacciones?

Las transacciones existen porque muchas decisiones de escritura dependen del estado actual de los datos. La documentación oficial indica que son útiles cuando se desea actualizar un valor en función de su valor actual o del valor de otro campo.[cite:1]

Supongamos que el sistema del libro administra cupos en grupos académicos. Si dos estudiantes intentan ocupar el último lugar casi al mismo tiempo, una escritura ingenua podría permitir ambas inscripciones si cada cliente lee primero cuposDisponibles = 1 y luego actualiza sin coordinación. Aquí aparece la necesidad de una operación que lea y escriba bajo garantías de consistencia.

La transacción responde precisamente a esa necesidad.

Problemas de concurrencia

La concurrencia aparece cuando varias operaciones intentan interactuar con el mismo dato o con datos relacionados en intervalos de tiempo superpuestos. La documentación de serializabilidad lo denomina data contention: dos o más operaciones compiten por controlar el mismo documento.[cite:2]

En aplicaciones educativas como la del libro, la concurrencia es normal:

  • varios alumnos pueden intentar inscribirse al mismo grupo;
  • dos docentes podrían editar asistencia casi al mismo tiempo;
  • un administrador y un proceso automático podrían actualizar estadísticas simultáneamente;
  • una tarea puede cambiar de estado mientras otro usuario lee y decide sobre ella.

Sin mecanismos atómicos, estas situaciones generan resultados inconsistentes.

Lecturas inconsistentes

Una lectura inconsistente ocurre cuando una operación toma decisiones sobre datos que ya no reflejan el estado válido en el instante real de commit. Firestore aborda este problema mediante aislamiento serializable en transacciones.[cite:2]

La documentación oficial afirma que Firestore garantiza serializable isolation y que las transacciones están serializadas e aisladas por tiempo de commit.[cite:2] En otras palabras, aunque múltiples operaciones se ejecuten en paralelo, el sistema garantiza un resultado equivalente a que se hubieran ejecutado en serie.

Este principio es fundamental: una transacción no debe basarse en lecturas “sucias” o en estados intermedios de otras operaciones concurrentes.

Escrituras simultáneas

Las escrituras simultáneas son uno de los escenarios más delicados. Si dos clientes actualizan el mismo documento sin coordinación, el último en escribir puede sobrescribir al primero, o ambos pueden tomar decisiones basadas en un estado antiguo.

La documentación de contención explica que, para que una transacción tenga éxito, los documentos recuperados por sus operaciones de lectura deben permanecer sin cambios por operaciones externas durante la ejecución.[cite:2] Si otro proceso cambia cualquiera de esos documentos, Firestore detecta la contención y la transacción se reintenta automáticamente.[cite:1][cite:2]

Esa es la base técnica que protege la consistencia frente a escrituras simultáneas.

¿Qué es una transacción en Firestore?

Según la documentación oficial, una transacción en Firestore es un conjunto de operaciones de lectura y escritura sobre uno o más documentos.[cite:1] La transacción puede incluir cualquier número de lecturas get() seguido de cualquier número de escrituras como set(), update() o delete(), pero las lecturas deben ocurrir antes de las escrituras.[cite:1]

En el SDK modular web se implementa con runTransaction().

import { doc, runTransaction } from 'firebase/firestore';

async function incrementarCuposDisponibles(grupoId) {
  const grupoRef = doc(db, 'grupos', grupoId);

  await runTransaction(db, async (transaction) => {
    const grupoSnap = await transaction.get(grupoRef);

    if (!grupoSnap.exists()) {
      throw new Error('El grupo no existe');
    }

    const cupos = grupoSnap.data().cuposDisponibles ?? 0;
    transaction.update(grupoRef, {
      cuposDisponibles: cupos + 1
    });
  });
}

Este patrón es apropiado porque el valor nuevo depende del valor actual.

Funcionamiento interno

El comportamiento interno de las transacciones en Firestore está documentado en dos piezas clave. Primero, la guía de transacciones dice que si la transacción lee documentos y otro cliente modifica alguno de ellos, Firestore vuelve a ejecutar toda la transacción.[cite:1] Segundo, la guía de serializabilidad aclara que los SDKs web y móviles usan control de concurrencia optimista, independientemente de la configuración del modo de concurrencia del servidor.[cite:2]

Eso significa que, en aplicaciones web como las que desarrolla este libro:

  1. La transacción registra qué documentos leyó.
  2. Calcula sus escrituras candidatas.
  3. Intenta confirmar solo si las versiones leídas siguen siendo válidas.[cite:2]
  4. Si detecta cambios externos, la transacción se reintenta automáticamente.[cite:1][cite:2]

A diferencia de algunas bibliotecas de servidor, el SDK web no usa bloqueos pesimistas; emula concurrencia optimista con precondiciones sobre versiones de documentos.[cite:2]

Ciclo completo de una transacción

El ciclo de una transacción puede resumirse así:

  1. Inicio de la función transaccional.
  2. Lectura de uno o más documentos mediante transaction.get().[cite:1]
  3. Cálculo de decisiones en memoria.
  4. Declaración de escrituras set, update o delete.
  5. Validación de que los documentos leídos no cambiaron externamente.[cite:2]
  6. Commit final de todas las escrituras o reintento automático.

La documentación también subraya que las escrituras nunca se aplican parcialmente y que todas se ejecutan al final de una transacción exitosa.[cite:1] Por tanto, una transacción no “va guardando” cosas a mitad del proceso.

Reintentos automáticos

Uno de los rasgos más importantes de una transacción es que la función puede ejecutarse más de una vez. La guía oficial lo advierte explícitamente: una función que llama una transacción puede ejecutarse varias veces si una edición concurrente afecta un documento leído por la transacción.[cite:1]

La guía de contención añade que, tras un número finito de reintentos, si no se logra un resultado limpio, la operación falla con un error como ABORTED: Too much contention on these documents. Please try again.[cite:2]

Esto tiene consecuencias directas sobre cómo escribir código transaccional:

  • la función debe ser idempotente en sus efectos externos;
  • no debe modificar estado visual o estado global de la aplicación dentro de la función transaccional;
  • no debe enviar correos, disparar animaciones ni registrar eventos duplicables dentro de la transacción.

Lectura y escritura dentro de una transacción

La documentación oficial impone una regla clara: las lecturas deben ejecutarse antes que las escrituras.[cite:1] Además, la guía de aislamiento aclara que las consultas y lecturas dentro de una transacción no ven los resultados de escrituras previas de esa misma transacción; siempre observan la versión del documento correspondiente al tiempo de commit, antes de aplicar las escrituras.[cite:2]

Esto sorprende a muchos desarrolladores al principio. Si dentro de una transacción se modifica un documento y luego se intenta leerlo otra vez esperando ver el valor actualizado, esa expectativa es incorrecta. El modelo de aislamiento evita ese comportamiento.

Casos de uso de transacciones

Las transacciones convienen cuando el valor final depende del estado actual leído dentro de la propia operación. Casos típicos del proyecto transversal:

  • Inscripción de alumnos: leer cupos disponibles de un grupo y decrementar solo si aún hay espacio.
  • Asignación de docentes: verificar que una clase no tenga docente asignado antes de registrar uno nuevo.
  • Registro de asistencia consolidada: leer un documento de resumen y actualizar sus contadores con base en el estado actual.
  • Generación de credenciales con contador secuencial controlado: leer último folio emitido y reservar el siguiente.
  • Actualización de estadísticas dependientes del valor previo: por ejemplo, total de tareas entregadas o alumnos activos.

Limitaciones de las transacciones

La documentación oficial enumera varios límites y causas de fallo relevantes:

  • las lecturas deben ir antes de las escrituras;[cite:1]
  • la transacción falla offline;[cite:1]
  • la función puede reintentarse varias veces;[cite:1][cite:2]
  • falla si supera el tamaño máximo de solicitud de 10 MiB;[cite:1]
  • falla si supera el lock deadline de 20 segundos;[cite:1]
  • falla si excede 270 segundos o 60 segundos de inactividad.[cite:1]

Estas limitaciones muestran que una transacción no es un contenedor para lógica pesada, lenta o muy extensa. Debe ser corta, precisa y enfocada.

¿Qué es un Batch Write?

Un Batch Write es un conjunto de operaciones de escritura sobre uno o más documentos sin lecturas previas. La documentación oficial lo define como un set de operaciones de escritura —set(), update() o delete()— que se ejecutan como un solo batch atómico.[cite:1]

En el SDK modular moderno se implementa con writeBatch().

import { writeBatch, doc } from 'firebase/firestore';

async function crearDocumentosRelacionados() {
  const batch = writeBatch(db);

  const cursoRef = doc(db, 'cursos', 'curso_001');
  const grupoRef = doc(db, 'grupos', 'grupo_001');
  const docenteRef = doc(db, 'usuarios', 'docente_001');

  batch.set(cursoRef, { titulo: 'Matemáticas I', activo: true });
  batch.update(grupoRef, { cursoId: 'curso_001' });
  batch.update(docenteRef, { cursoActualId: 'curso_001' });

  await batch.commit();
}

Aquí no se necesita leer nada para decidir los valores. Solo se quiere asegurar que todas las escrituras ocurran juntas.

Funcionamiento interno del Batch Write

El funcionamiento interno del lote es más simple que el de una transacción. Firestore agrupa las escrituras declaradas y las confirma como una única unidad atómica.[cite:1] No necesita verificar que documentos leídos sigan intactos, porque no hubo lecturas dentro de la operación.

La documentación destaca justamente esta diferencia: a diferencia de las transacciones, los batched writes no necesitan garantizar que los documentos leídos permanezcan sin modificar, lo que produce menos casos de fallo. Tampoco están sujetos a reintentos ni a errores por demasiados reintentos.[cite:1]

Además, los batch writes pueden ejecutarse incluso cuando el dispositivo del usuario está offline.[cite:1] Esto los vuelve especialmente útiles en determinadas experiencias móviles o web con persistencia local.

Diferencias entre Transaction y Batch

La diferencia central es esta:

  • Transaction: lee primero y luego escribe, tomando decisiones en función del estado actual.[cite:1]
  • Batch Write: escribe múltiples cambios sin leer dentro de la operación.[cite:1]

La segunda gran diferencia es el comportamiento frente a conflictos:

  • la transacción puede reintentarse automáticamente si hay contención sobre documentos leídos;[cite:1][cite:2]
  • el batch write no se reintenta por contención de lecturas, porque no depende de lecturas previas.[cite:1]

La tercera diferencia es el entorno:

  • la transacción falla cuando el cliente está offline;[cite:1]
  • el batch write puede ejecutarse offline.[cite:1]

Ventajas de Batch Writes

Los lotes tienen varias ventajas muy concretas:

  • permiten confirmar varias escrituras como una sola unidad atómica;[cite:1]
  • son conceptualmente más simples que una transacción;
  • tienen menos causas de fallo relacionadas con concurrencia de lectura;[cite:1]
  • funcionan incluso offline en SDKs cliente;[cite:1]
  • son ideales cuando la lógica de negocio ya conoce los valores a escribir.

En el proyecto del libro esto resulta perfecto para operaciones de creación coordinada de documentos relacionados.

Casos de uso de Batch Writes

Casos típicos donde conviene lote y no transacción:

  • crear una tarea y, al mismo tiempo, crear documentos derivados de visibilidad para varios grupos;
  • registrar asistencia de varios alumnos a partir de un formulario ya resuelto en cliente;
  • generar credenciales y documentos secundarios cuando no hace falta consultar el estado previo;
  • actualizar indicadores redundantes ya calculados fuera de la operación;
  • crear múltiples documentos relacionados en una misma acción administrativa.

La regla práctica es simple: si no necesitas leer para decidir, el lote suele ser mejor.

Límites de operaciones

La documentación oficial no solo explica el propósito, sino también restricciones importantes. Para transacciones, ya se vio el límite de 10 MiB por solicitud, el plazo de 20 segundos de lock y el tiempo máximo de 270 segundos con 60 segundos de inactividad.[cite:1]

Para batched writes, la documentación advierte que un lote con cientos de documentos puede requerir muchas actualizaciones de índices y superar el límite de tamaño de transacción; en esos casos recomienda reducir el número de documentos por lote o considerar Bulk Writer o escrituras paralelas individuales para volúmenes masivos.[cite:1]

Esto es muy importante: que un batch write sea atómico no significa que sea adecuado para cualquier tamaño.

Manejo de errores

En transacciones, el manejo de errores debe considerar tres categorías:

  1. errores lógicos del negocio, como “el grupo ya no tiene cupos”;
  2. errores de contención y reintentos agotados, como ABORTED;[cite:2]
  3. límites técnicos o timeouts.[cite:1]

En lotes, los errores suelen relacionarse más con:

  • documentos inexistentes en update();
  • reglas de seguridad;
  • tamaño o cantidad excesiva del lote;
  • problemas de conectividad o autorización.

Un principio clave: el error debe comunicarse en términos de negocio al usuario y en términos técnicos al sistema de observabilidad.

Optimización

Optimizar operaciones atómicas significa reducir el número de documentos afectados, acortar la lógica crítica y elegir el mecanismo correcto.

Recomendaciones derivadas de la documentación:

  • en transacciones, leer solo los documentos indispensables y terminar rápido;[cite:1][cite:2]
  • no incluir trabajo pesado o llamadas externas dentro de la función transaccional;
  • en lotes, no agrupar cientos de documentos si el fanout de índices será excesivo;[cite:1]
  • usar FieldValue.increment() o transformaciones equivalentes cuando la lógica no requiera una transacción completa, como sugiere la documentación en sus ejemplos.[cite:1]

Impacto sobre costos

La documentación de precios explica que Firestore cobra por lecturas, escrituras y eliminaciones.[cite:3] Por eso, el costo de una operación atómica depende de cuántos documentos lee y cuántos escribe.

Implicaciones prácticas:

  • una transacción suele costar más que un batch equivalente porque incluye lecturas además de escrituras;
  • si hay contención y se producen reintentos, pueden generarse más lecturas, más latencia y más consumo operativo;[cite:1][cite:2]
  • cada operación de un batch cuenta por separado hacia el uso de Firestore, aunque el commit sea atómico.[cite:1]

La atomicidad no reduce mágicamente facturación. Solo garantiza consistencia.

Rendimiento

Desde la perspectiva de rendimiento:

  • las transacciones son más costosas cognitivamente y técnicamente porque coordinan lecturas, validaciones y posibles reintentos;[cite:1][cite:2]
  • los batch writes suelen ser más directos cuando los valores ya se conocen;
  • ambos mecanismos pueden degradarse si afectan demasiados documentos indexados, porque las escrituras activan trabajo de indexación.[cite:1]

En aplicaciones profesionales, el rendimiento de una operación atómica depende tanto del mecanismo elegido como del diseño de documentos e índices.

Comparaciones técnicas

Transaction vs Batch Write

Criterio Transaction Batch Write
Incluye lecturas Sí [cite:1] No [cite:1]
Incluye escrituras Sí [cite:1] Sí [cite:1]
Atomicidad Sí [cite:1] Sí [cite:1]
Reintentos automáticos Sí, ante contención [cite:1][cite:2] No por contención de lectura [cite:1]
Funciona offline No [cite:1] Sí [cite:1]
Mejor uso Decisiones basadas en estado actual Varias escrituras conocidas de antemano
Riesgo principal Contención y reintentos Sobrecargar el lote con demasiadas operaciones

Consistencia vs simplicidad

Escenario Mejor opción Motivo
Descontar último cupo disponible Transaction Depende del valor actual del grupo
Crear tarea y documentos auxiliares Batch Write Solo requiere escribir múltiples documentos
Recalcular contador leyendo estado previo Transaction La decisión depende de una lectura consistente
Registrar 20 asistencias ya validadas Batch Write Todas las escrituras ya están decididas

Ejemplos paso a paso

Ejemplo 1: inscripción de alumnos con control de cupo usando transacción

import { doc, runTransaction, serverTimestamp } from 'firebase/firestore';

async function inscribirAlumnoEnGrupo({ alumnoId, grupoId }) {
  const grupoRef = doc(db, 'grupos', grupoId);
  const inscripcionRef = doc(db, 'grupos', grupoId, 'inscripciones', alumnoId);

  await runTransaction(db, async (transaction) => {
    const grupoSnap = await transaction.get(grupoRef);

    if (!grupoSnap.exists()) {
      throw new Error('El grupo no existe');
    }

    const data = grupoSnap.data();
    const cuposDisponibles = data.cuposDisponibles ?? 0;

    if (cuposDisponibles <= 0) {
      throw new Error('No hay cupos disponibles');
    }

    transaction.set(inscripcionRef, {
      alumnoId,
      grupoId,
      createdAt: serverTimestamp(),
      estado: 'activa'
    });

    transaction.update(grupoRef, {
      cuposDisponibles: cuposDisponibles - 1
    });
  });
}

Por qué es transacción: la decisión depende de leer cuposDisponibles y confirmar que nadie lo cambió antes del commit.[cite:1][cite:2]

Ejemplo 2: asignación de docente a una clase

import { doc, runTransaction, serverTimestamp } from 'firebase/firestore';

async function asignarDocenteAClase({ claseId, docenteId }) {
  const claseRef = doc(db, 'clases', claseId);
  const docenteRef = doc(db, 'usuarios', docenteId);

  await runTransaction(db, async (transaction) => {
    const [claseSnap, docenteSnap] = await Promise.all([
      transaction.get(claseRef),
      transaction.get(docenteRef)
    ]);

    if (!claseSnap.exists()) throw new Error('La clase no existe');
    if (!docenteSnap.exists()) throw new Error('El docente no existe');

    if (claseSnap.data().docenteId) {
      throw new Error('La clase ya tiene docente asignado');
    }

    transaction.update(claseRef, {
      docenteId,
      asignadoAt: serverTimestamp()
    });

    transaction.update(docenteRef, {
      claseActualId: claseId,
      updatedAt: serverTimestamp()
    });
  });
}

Por qué es transacción: se necesita garantizar que la clase siga sin docente en el momento real de commit.

Ejemplo 3: generación de credenciales con Batch Write

import { writeBatch, doc, serverTimestamp } from 'firebase/firestore';

async function generarCredencialesAlumno({ alumnoId, institucionId }) {
  const batch = writeBatch(db);

  const credencialRef = doc(db, 'credenciales', alumnoId);
  const archivoRef = doc(db, 'archivos', `credencial_${alumnoId}`);
  const notificacionRef = doc(db, 'usuarios', alumnoId, 'notificaciones', `cred_${Date.now()}`);

  batch.set(credencialRef, {
    alumnoId,
    institucionId,
    estado: 'generada',
    createdAt: serverTimestamp()
  });

  batch.set(archivoRef, {
    ownerId: alumnoId,
    tipo: 'credencial_pdf',
    createdAt: serverTimestamp()
  });

  batch.set(notificacionRef, {
    tipo: 'credencial_generada',
    leida: false,
    createdAt: serverTimestamp()
  });

  await batch.commit();
}

Por qué es batch: todos los datos a escribir ya están decididos. No se necesita leer nada.

Ejemplo 4: registro de asistencia grupal con Batch Write

import { writeBatch, doc, serverTimestamp } from 'firebase/firestore';

async function registrarAsistencia({ claseId, asistencias }) {
  const batch = writeBatch(db);

  for (const item of asistencias) {
    const asistenciaRef = doc(db, 'clases', claseId, 'asistencias', item.alumnoId);

    batch.set(asistenciaRef, {
      alumnoId: item.alumnoId,
      presente: item.presente,
      updatedAt: serverTimestamp()
    });
  }

  const claseRef = doc(db, 'clases', claseId);
  batch.update(claseRef, {
    asistenciaCapturada: true,
    asistenciaUpdatedAt: serverTimestamp()
  });

  await batch.commit();
}

Por qué es batch: la asistencia ya fue decidida por el docente en la UI; solo hace falta persistir múltiples documentos de forma atómica.

Ejemplo 5: actualización de estadísticas con transacción

import { doc, runTransaction } from 'firebase/firestore';

async function incrementarTareasEntregadas(cursoId) {
  const statsRef = doc(db, 'estadisticasCursos', cursoId);

  await runTransaction(db, async (transaction) => {
    const statsSnap = await transaction.get(statsRef);

    if (!statsSnap.exists()) {
      transaction.set(statsRef, { tareasEntregadas: 1 });
      return;
    }

    const total = statsSnap.data().tareasEntregadas ?? 0;
    transaction.update(statsRef, {
      tareasEntregadas: total + 1
    });
  });
}

Por qué es transacción: el nuevo total depende del valor actual almacenado.

Casos prácticos

Caso 1: inscripción al último cupo

Dos alumnos intentan inscribirse al último lugar de un grupo. Sin transacción, ambos podrían leer el mismo valor y ambos podrían creer que aún hay espacio. Con transacción, Firestore detecta si uno de los documentos leídos cambió y reintenta; al final, solo una operación debería consolidarse correctamente.[cite:1][cite:2]

Caso 2: creación coordinada de documentos relacionados

Cuando un administrador crea una nueva clase y al mismo tiempo quiere crear su documento principal, un documento de configuración inicial y una notificación para el docente asignado, el Batch Write es ideal. No hace falta leer nada; solo se necesita que todos esos documentos aparezcan juntos o no aparezca ninguno.[cite:1]

Caso 3: actualización de cupos y lista de inscritos

Si la operación depende de verificar el número actual de cupos y del hecho de que el alumno todavía no esté inscrito, la transacción vuelve a ser la mejor opción. Aquí el estado previo sí importa.

Caso 4: propagación de cambios secundarios

Si una acción ya validada debe reflejarse en varios documentos espejo o resúmenes predecibles, un batch write simplifica la operación y reduce problemas de reintento.

Caso 5: operaciones masivas de escritura

La documentación advierte que lotes con cientos de documentos pueden disparar muchas actualizaciones de índices y superar límites; en ese caso conviene dividir en varios lotes o considerar otras estrategias de escritura masiva.[cite:1] Esto es importante, por ejemplo, al generar miles de notificaciones o registros derivados.

Buenas prácticas

  • Usar transacciones solo cuando la lógica depende del valor actual leído dentro de la operación.[cite:1]
  • Usar Batch Writes cuando todos los valores ya están decididos y solo se necesita atomicidad de múltiples escrituras.[cite:1]
  • Mantener las transacciones pequeñas, rápidas y enfocadas.[cite:1][cite:2]
  • No modificar estado de UI ni variables globales dentro de la función transaccional, porque puede ejecutarse varias veces.[cite:1]
  • No meter llamadas HTTP, envío de correos o efectos colaterales dentro de la transacción.
  • Diseñar mensajes de error de negocio distintos de los errores técnicos de contención.
  • Limitar el tamaño de lotes para evitar exceso de trabajo de índices y problemas de rendimiento.[cite:1]
  • Evaluar si increment o transformaciones atómicas simples resuelven el caso sin necesidad de una transacción completa.[cite:1]
  • Validar consistencia también con Security Rules cuando corresponda; la documentación muestra getAfter() como herramienta para comprobar el estado final antes del commit.[cite:1]

Errores comunes

  • Usar transacción para operaciones que no requieren lectura previa.
  • Usar batch write cuando en realidad la decisión depende del estado actual del documento.
  • Cambiar estado visual dentro de la función de transacción y provocar duplicaciones cuando se reintenta.[cite:1]
  • Leer después de escribir dentro de la misma transacción, contradiciendo la regla del modelo.[cite:1][cite:2]
  • Ignorar que las transacciones fallan offline.[cite:1]
  • Crear lotes demasiado grandes y culpar a Firestore cuando el problema es el diseño del proceso.[cite:1]
  • No contemplar contención alta sobre el mismo documento en operaciones populares.[cite:2]
  • Confundir atomicidad con costo bajo; cada lectura y escritura sigue facturando normalmente.[cite:1][cite:3]

Resumen

Cloud Firestore ofrece dos mecanismos principales para operaciones atómicas: transacciones y Batch Writes. Ambos garantizan que todas las operaciones se apliquen juntas o que no se aplique ninguna, pero resuelven problemas diferentes.[cite:1]

Las transacciones están diseñadas para escenarios donde el valor final depende del estado actual de uno o más documentos. Por eso combinan lecturas y escrituras, reintentan automáticamente cuando hay contención sobre documentos leídos y garantizan aislamiento serializable por tiempo de commit en Firestore.[cite:1][cite:2] Su potencia es alta, pero también lo son sus exigencias: deben ser rápidas, leer antes de escribir y tolerar reejecuciones.

Los Batch Writes, en cambio, son ideales cuando ya se conocen todos los valores a escribir y solo se necesita que varias escrituras se confirmen como una unidad atómica. No sufren reintentos por contención de lecturas, pueden ejecutarse offline en SDKs cliente y suelen ser una opción más simple para crear o actualizar varios documentos relacionados.[cite:1]

Elegir correctamente entre ambos mecanismos es una decisión arquitectónica clave. En sistemas con múltiples usuarios concurrentes, esa elección determina la consistencia de los datos, el costo de operación, la latencia percibida y la robustez general de la aplicación.

Conceptos clave

  • Operación atómica.[cite:1]
  • Transacción en Firestore.[cite:1]
  • Batch Write.[cite:1]
  • Contención de datos.[cite:2]
  • Concurrencia optimista en SDK web/móvil.[cite:2]
  • Aislamiento serializable.[cite:2]
  • Reintentos automáticos.[cite:1][cite:2]
  • runTransaction().
  • writeBatch().
  • Escrituras atómicas offline con batch.[cite:1]
  • Límite de 10 MiB por transacción.[cite:1]
  • Tiempo máximo de 270 segundos e inactividad de 60 segundos.[cite:1]
  • getAfter() en reglas de seguridad para validar estado posterior al commit.[cite:1]

Preguntas de repaso

  1. ¿Qué significa que una operación sea atómica en Firestore?[cite:1]
  2. ¿Por qué una transacción puede ejecutarse más de una vez?[cite:1][cite:2]
  3. ¿Qué problema de concurrencia resuelve una transacción que un Batch Write no resuelve por sí mismo?
  4. ¿Por qué las lecturas deben ocurrir antes de las escrituras dentro de una transacción?[cite:1]
  5. ¿Qué implica que los SDK web y móviles usen concurrencia optimista en transacciones?[cite:2]
  6. ¿Cuándo conviene usar Batch Write en lugar de Transaction?[cite:1]
  7. ¿Por qué un batch write puede funcionar offline y una transacción no?[cite:1]
  8. ¿Qué riesgos trae colocar efectos secundarios dentro de la función transaccional?[cite:1]
  9. ¿Cómo impactan lecturas, escrituras y reintentos sobre el costo total de una operación atómica?[cite:3]
  10. ¿Qué papel pueden cumplir las Security Rules con getAfter() en la validación de consistencia?[cite:1]

Ejercicios prácticos

  1. Implementa una transacción para inscribir a un alumno en un grupo solo si cuposDisponibles > 0.[cite:1][cite:2]
  2. Diseña un batch write que cree una tarea, una notificación al docente y un documento de auditoría en una sola operación atómica.[cite:1]
  3. Reescribe un contador manual usando runTransaction() y explica por qué una escritura simple podría producir inconsistencias bajo concurrencia.
  4. Diseña un caso donde un batch write sea incorrecto y una transacción sea obligatoria.
  5. Simula una condición de contención alta sobre el mismo documento y explica por qué puede aparecer el error ABORTED.[cite:2]
  6. Implementa un flujo de registro de asistencia para múltiples alumnos usando writeBatch() y justifica por qué no hace falta transacción.
  7. Analiza un caso del proyecto donde convendría usar FieldValue.increment() en vez de una transacción completa, siguiendo la sugerencia de la documentación oficial.[cite:1]
  8. Diseña una regla conceptual con getAfter() para exigir que una actualización de una clase modifique también un documento de estadísticas relacionado.[cite:1]

Bibliografía y referencias oficiales