Saltar al contenido
Ver todas las notas
arquitecturabases-de-datosfirestorepostgresql

Persistencia políglota: documento para identity, SQL para dinero

Por qué en el mismo sistema uso Firestore o MongoDB para perfiles y usuarios, y PostgreSQL para todo lo que toca dinero. Qué se gana, qué se paga, y cuándo esta decisión es sobreingeniería.

· 12 min de lectura

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.

FirestoreMongoDB
OperaciónNinguna. Es totalmente gestionadoAtlas gestionado, o lo operas tú
ConsultasLimitadas: sin JOIN, agregaciones básicasPipeline de agregación muy rico
SeguridadReglas declarativas en el borde, antes de tu códigoEn tu aplicación
Tiempo realNativo, con listenersChange streams, más trabajo
PortabilidadAtado a Google CloudCorre en cualquier sitio
CostoPor lectura, escritura y almacenamientoPor 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:

  1. Hay módulos con patrones de acceso claramente opuestos, no solo distintos.
  2. El módulo documental gana algo concreto: reglas en el borde, tiempo real, o un esquema que de verdad cambia seguido.
  3. 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.

¿Tienes un sistema con este tipo de problemas?

Cuéntame en qué estás y te digo cómo lo abordaría yo.

Hablemos de tu sistema