La pregunta "¿SQL o NoSQL?" está mal planteada. Presupone que un sistema tiene una base de datos, y los sistemas medianamente grandes tienen partes con necesidades opuestas.
En el ecosistema que mantengo conviven dos: una base de documentos para identidad y personas, y PostgreSQL para tesorería. No por gusto tecnológico: porque los patrones de acceso de cada módulo no se parecen en nada.
Dos módulos, dos problemas distintos#
Identity: forma variable, lectura constante#
Un perfil de persona cambia de forma según el caso. Unos tienen datos de contacto completos, otros solo un teléfono; unos pertenecen a tres grupos, otros a ninguno; el año que viene alguien pedirá un campo nuevo que hoy no existe.
Y el patrón de acceso es abrumadoramente leer por identificador: dame este usuario, dame los miembros de este grupo. Casi no hay consultas analíticas.
Una tabla relacional para esto acaba en una de dos: cincuenta columnas
mayormente nulas, o seis tablas con LEFT JOIN para reconstruir algo que
conceptualmente es un único objeto.
// Lo que quiero leer es esto, de una vez
{
id: 'abc',
name: 'María Restrepo',
contact: { phone: '+57...', email: 'maria@...' },
memberships: [{ groupId: 'g1', role: 'LEADER', since: '2024-03-01' }],
preferences: { locale: 'es', notifications: { email: true, push: false } },
}
En una base de documentos eso es el documento. Se lee en una operación y añadir un campo no requiere migración.
Treasury: dinero, y el dinero no admite "casi"#
Aquí todo lo anterior se invierte:
- Un movimiento contable que se aplique a medias corrompe los saldos.
- Un gasto que apunte a una categoría inexistente es un dato inválido, no un documento flexible.
- Las consultas son agregaciones: total por fondo, por periodo, por categoría, cruzando varias entidades.
- El saldo tiene que ser exacto siempre, no dentro de un rato.
Eso es exactamente lo que un motor relacional lleva cuarenta años resolviendo:
transacciones ACID, claves foráneas, restricciones y SUM ... GROUP BY que no
hay que inventar en la aplicación.
-- Esto en una base de documentos se convierte en código de aplicación,
-- y ese código es donde aparecen los descuadres
SELECT fund_id, SUM(amount) AS total
FROM expenses
WHERE status = 'APPROVED' AND occurred_at BETWEEN $1 AND $2
GROUP BY fund_id;
Forzar tesorería en una base de documentos significa reimplementar integridad referencial y agregación a mano. Se puede. Y cada error ahí es dinero mal contado.
Firestore o MongoDB: yo elijo Firestore, pero no siempre es lo correcto#
Para la parte documental, las dos opciones que considero de verdad son Firestore y MongoDB. Yo prefiero Firestore, y conviene explicar por qué y en qué casos esa preferencia sería un error.
| Firestore | MongoDB | |
|---|---|---|
| Operación | Ninguna. Es totalmente gestionado | Atlas gestionado, o lo operas tú |
| Consultas | Limitadas: sin JOIN, agregaciones básicas | Pipeline de agregación muy rico |
| Seguridad | Reglas declarativas en el borde, antes de tu código | En tu aplicación |
| Tiempo real | Nativo, con listeners | Change streams, más trabajo |
| Portabilidad | Atado a Google Cloud | Corre en cualquier sitio |
| Costo | Por lectura, escritura y almacenamiento | Por instancia o cluster |
Por qué elijo Firestore#
Las reglas de seguridad. Se declaran fuera de la aplicación y se aplican antes de que la petición llegue a mi código. En un sistema multi-tenant es la diferencia entre "confío en que ningún endpoint olvide filtrar por tenant" y "es imposible leer datos de otro tenant aunque el endpoint esté mal escrito".
Cero operación. No hay servidor que dimensionar, actualizar ni respaldar. En equipos pequeños eso no es comodidad: es la diferencia entre tener a alguien haciendo mantenimiento o construyendo producto.
El tiempo real sale gratis. Un listener y la interfaz se actualiza sola. Si el producto lo aprovecha, es una ventaja enorme; si no, no aporta nada.
Encaja con el resto. Si ya usas Firebase Auth y Storage, todo comparte identidad y reglas.
Cuándo elegiría MongoDB en su lugar#
Y aquí soy honesto, porque mucha gente prefiere Mongo con buenas razones:
- Consultas complejas sobre documentos. El pipeline de agregación de Mongo hace cosas que en Firestore acaban resolviéndose en la aplicación o duplicando datos. Si el módulo documental necesita analítica, Mongo gana sin discusión.
- Evitar el lock-in. Firestore solo existe en Google Cloud. Mongo corre en Atlas, en tu nube o en tu máquina. Si eso es un requisito —y en clientes corporativos a veces lo es—, la decisión está tomada.
- Ya tienes equipo de operaciones. Si hay quien opere una base, buena parte de la ventaja de Firestore desaparece y quedan sus límites de consulta.
- Costo con mucho volumen de lectura. Firestore cobra por operación. Un sistema que lee millones de documentos al día puede salir más caro que un cluster de tamaño fijo.
La regla que uso: ¿la parte documental necesita consultas ricas? Si sí, Mongo. Si son lecturas por identificador con seguridad estricta y tiempo real, Firestore.
Lo que se paga por tener dos#
Esta es la parte que no aparece en los artículos que recomiendan políglota.
No hay JOIN entre ellas. No puedes cruzar un gasto de PostgreSQL con el
usuario que lo aprobó en Firestore desde una consulta. O haces dos lecturas, o
denormalizas.
Yo denormalizo, con un snapshot mínimo:
// En el registro del gasto, dentro de PostgreSQL
{
approvedBy: {
personId: 'abc',
displayName: 'María Restrepo', // copia, a propósito
},
}
Guardar el nombre parece una herejía relacional. Es deliberado: el reporte de gastos del año pasado debe decir quién aprobó con el nombre que tenía entonces, no con el actual. La copia no es duplicación por pereza; es un dato histórico distinto.
No hay transacciones que crucen las dos. Si una operación toca identity y treasury, no existe un commit conjunto. Se resuelve con eventos de dominio y aceptando consistencia eventual: la fuente de verdad actualiza primero y emite un evento; el otro lado reacciona.
Eso implica que durante unos milisegundos —a veces segundos, si algo falla— los datos denormalizados están desfasados. Hay que decidir explícitamente si el negocio lo tolera. En un nombre mostrado en un reporte, sí. En un saldo, jamás: por eso el saldo vive entero en PostgreSQL.
Dos modelos mentales y dos respaldos. Cada persona que entra al equipo aprende dos formas de consultar y dos formas de migrar. Y una estrategia de recuperación que no contemple ambas no sirve de nada.
Cómo evito que el precio se dispare#
La clave es que el dominio no sepa qué hay debajo. Cada módulo declara un puerto y la infraestructura lo implementa:
// application/ports — el caso de uso solo conoce esto
export interface PersonRepository {
findById(id: string): Promise<Person | null>;
save(person: Person): Promise<void>;
}
Si mañana identity migra de Firestore a MongoDB, cambia una clase en
infrastructure/. Ni los casos de uso ni el dominio se enteran. No es una
promesa teórica: es la única razón por la que una decisión de persistencia deja
de ser irreversible.
Cuándo esto es sobreingeniería#
Casi siempre, al principio.
Si el sistema cabe en una base de datos, usa una. PostgreSQL con columnas
jsonb cubre sorprendentemente bien la parte documental, y una sola base
significa transacciones reales, un respaldo, un modelo mental y cero
sincronización.
La persistencia políglota empieza a compensar cuando:
- Hay módulos con patrones de acceso claramente opuestos, no solo distintos.
- El módulo documental gana algo concreto: reglas en el borde, tiempo real, o un esquema que de verdad cambia seguido.
- Existe quien mantenga las dos.
Si alguno de los tres falla, una sola base es la respuesta correcta — y cambiar después, si el dominio lo pide, es exactamente para lo que están los puertos.