Data Architecture
Quién es dueño del dato, cómo se modela y cómo se persiste: base de datos compartida vs. base de datos por servicio, SQL vs. NoSQL según el patrón de acceso, persistencia políglota y cuándo Event Sourcing aporta valor real.
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?
Data Ownership
Dos estrategias fundamentales: Shared Database y Database per Service.
Shared Database
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.
flowchart TD S["Shared Schema"] --> A["Service A"] S --> B["Service B"] S --> C["Service C"]
Database per Service
flowchart LR
PS["Payment Service"] --> PDB[("Payment DB")]
OS["Order Service"] --> ODB[("Order DB")]
IS["Inventory Service"] --> IDB[("Inventory DB")]Cada servicio es dueño de sus datos. Esto mejora la autonomía, pero vuelve mucho más difícil hacer:
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.
Database Types / Data Modelling
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: tables, rows, columns, relationships, constraints, transactions.
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 cache, sessions, lookups y datos temporales. Redis es un ejemplo clásico.
Wide-Column
Está orientado a workloads de enorme escala y patrones de acceso específicos. Cassandra es una tecnología conocida en esta categoría. Aquí el data model debería diseñarse en torno a los queries 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 esquema;
- 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.
