Arquitectura de Datos
Ownership de los datos, cómo se modelan y cómo se persisten — y cuándo Event Sourcing vale la pena.
Las decisiones de datos suelen ser de las más difíciles de cambiar posteriormente. Hay tres preguntas distintas: ¿quién es dueño del dato?, ¿cómo se modela? y ¿cómo se persiste?
Ownership de los datos
Dos estrategias fundamentales: base de datos compartida y base de datos por servicio.
Base de datos compartida
flowchart LR
Pay["Payments"] --> PG[("PostgreSQL")]
Ord["Orders"] --> PG
Inv["Inventory"] --> PGVentajas: transacciones simples, joins, integridad relacional, menor complejidad.
Problema: una modificación del schema puede afectar a múltiples consumidores. Payments no puede cambiar una columna sin coordinar con Orders e Inventory; la “base compartida” se convierte en un contrato implícito entre equipos.
flowchart TD S["Shared Schema"] --> A["Service A"] S --> B["Service B"] S --> C["Service C"]
Base de datos por servicio
flowchart LR
PS["Payment Service"] --> PDB[("Payment DB")]
OS["Order Service"] --> ODB[("Order DB")]
IS["Inventory Service"] --> IDB[("Inventory DB")]Cada servicio tiene ownership de sus datos. Eso da autonomía: puede cambiar su schema, su motor y su ciclo de despliegue sin coordinar con los demás. El costo es que una transacción que antes era un BEGIN/COMMIT ahora cruza bases distintas:
BEGIN
Update Payment
Update Order
COMMIT
cuando cada operación ocurre en una base de datos distinta. Ahora necesitamos eventos, sagas, compensaciones, idempotencia y consistencia eventual.
Tipos de base de datos y modelado
No conviene empezar por:
¿SQL o NoSQL?
El proceso debería ser:
flowchart TD D["Domain"] --> M["Data Model"] --> AP["Access Patterns"] --> CR["Consistency Requirements"] --> W["Workload"] --> S["Scale"] --> T["Storage Technology"]
SQL / Relacional
Modelo: tablas, filas, columnas, relaciones, restricciones, transacciones.
Ejemplo:
payments: id, order_id, amount, currency, status, created_at
payment_transactions: id, payment_id, provider, provider_transaction_id, status, created_at
Fortalezas: relaciones, restricciones, transacciones, joins, tooling maduro y sólido soporte para OLTP. Bases de datos conocidas: PostgreSQL, MySQL, SQL Server, Oracle.
Normalización
La normalización busca reducir redundancia y anomalías de actualización. En lugar de repetir información (Payment, OrderId, CustomerName, CustomerEmail), podemos mantener relaciones claras:
flowchart LR P["Payment"] --> O["Order"] --> C["Customer"]
Pero la normalización no debe convertirse en una religión.
Desnormalización
A veces duplicar información es una decisión correcta para mejorar el rendimiento de lectura, simplificar las consultas y escalar mejor. Por ejemplo, un Order Read Model con order_id, customer_name, payment_status, total, shipping_status puede estar optimizado para lectura aunque los datos estén normalizados en el sistema transaccional.
NoSQL
“NoSQL” agrupa modelos diferentes:
| Categoría | Ejemplo |
|---|---|
| Document | MongoDB |
| Key-Value | Redis |
| Wide-Column | Cassandra |
| Graph | Neo4j |
Document
{
"id": "123",
"status": "captured",
"amount": 100,
"currency": "ARS"
}
Puede ser conveniente cuando los documentos se leen/escriben como unidades y el acceso no necesita joins complejos.
Key-Value
payment:123 → { ... }
Excelente para caché, sesiones, búsquedas por clave y datos temporales. Redis es un ejemplo clásico.
Wide-Column
Está orientado a cargas de trabajo de enorme escala y patrones de acceso específicos. Cassandra es una tecnología conocida en esta categoría. Acá el modelo de datos debería diseñarse en torno a las consultas que realmente se van a ejecutar.
Graph
Los datos se modelan como nodos, aristas y propiedades. Es especialmente útil para representar relaciones complejas. Neo4j es una implementación conocida.
SQL vs. NoSQL
La discusión correcta no es “SQL vs. NoSQL”, sino: ¿qué modelo?, ¿qué consultas?, ¿qué consistencia?, ¿qué carga de trabajo?, ¿qué escala?, ¿qué restricciones operativas?
Un sistema también puede utilizar varios tipos:
flowchart TD
App["Application"] --> PG[("PostgreSQL<br/>source of truth")]
App --> R[("Redis<br/>cache")]
App --> ES[("Elasticsearch<br/>search")]Esto se denomina persistencia políglota. La ventaja es la especialización; el costo, la sincronización y la complejidad operativa.
Event Sourcing
En un modelo tradicional almacenamos el estado actual (Payment.status = CAPTURED). Con Event Sourcing, los eventos son la fuente de verdad:
flowchart LR C["PaymentCreated"] --> A["PaymentAuthorized"] --> Cap["PaymentCaptured"]
El estado actual se puede reconstruir:
flowchart LR ES["Event Store"] --> R["Replay"] --> CS["Current State"]
Esto permite tener el historial completo de los cambios.
Ventajas
- registro de auditoría natural;
- replay;
- múltiples proyecciones;
- análisis temporal;
- reconstrucción del estado.
Costos
- evolución del schema;
- versionado de eventos;
- snapshots;
- complejidad del replay;
- concurrencia;
- debugging;
- complejidad de migración.
Además, Event Sourcing ≠ Event-Driven Architecture. Se pueden combinar, pero son decisiones independientes.
Cuándo usar Event Sourcing
Especialmente interesante cuando el historial de cambios es parte del dominio, la auditoría es fundamental, necesitamos reconstruir estados, existen múltiples modelos de lectura o el dominio se expresa naturalmente como una secuencia de hechos. Payments y sistemas financieros son casos donde el concepto puede ser atractivo, aunque eso no significa que todos necesiten Event Sourcing.
Cuándo evitarlo
Para un CRUD convencional donde solo importa el estado actual (Product: name, price, description), probablemente agrega más complejidad de la que elimina.
Herramientas
PostgreSQL, MySQL, MongoDB, Redis, Cassandra, Neo4j, Elasticsearch, Kafka, Debezium. Kafka, por ejemplo, puede utilizarse para transportar eventos, pero Kafka no convierte automáticamente una aplicación en un sistema con Event Sourcing.
