5. Gestión de datos en microservicios

Los microservicios se apoyan en una autonomía plena que incluye la propiedad de los datos. Esta independencia habilita desplegar y evolucionar cada servicio sin coordinar esquemas compartidos, pero introduce retos para mantener la consistencia y garantizar visibilidad de la información en toda la plataforma. En este capítulo exploramos cómo definir bases independientes, enfrentar la consistencia distribuida y aplicar patrones como Saga, CQRS y Event Sourcing para coordinar cambios.

5.1 Bases de datos independientes por servicio

Separar los datos por servicio evita cuellos de botella en el modelo relacional tradicional y reduce el riesgo de que un cambio estructural afecte a toda la solución. Cada equipo elige la tecnología que mejor se adapte a su subdominio, por ejemplo PostgreSQL para transacciones financieras o MongoDB para catálogos flexibles. Esta diversidad exige disciplina en los contratos de intercambio y en la observabilidad para detectar divergencias.

El principio rector es que el servicio es responsable de leer y escribir su propia base. Si otro servicio requiere datos, debe hacerlo a través de una API o mediante eventos publicados. Compartir tablas produce acoplamiento directo y complica el versionado, lo que va en contra de la independencia buscada.

5.1.1 Estrategias para elegir el almacén

Para seleccionar la base adecuada conviene analizar el tipo de carga (lecturas vs escrituras), el volumen, la necesidad de transacciones distribuidas y el modelo de consistencia. Servicios orientados a consultas intensivas pueden optar por almacenes documentales, mientras que las finanzas tienden a motores relacionales con integridad referencial. La decisión incluye el costo de operación, soporte de replicación y las herramientas de monitoreo disponibles.

5.1.2 Observabilidad y contratos de datos

Una base independiente no implica opacidad. Se recomienda exponer contratos de API estables, documentar métricas de acceso y utilizar esquemas versionados para los eventos publicados. Además, la observabilidad debe cubrir slow queries, tamaño de las tablas y tendencias de crecimiento para permitir que el servicio escale de forma preventiva.

5.2 Desafíos de consistencia de datos

En una plataforma distribuida se renuncia a la consistencia inmediata global. Cada servicio es consistente dentro de su contexto, pero el sistema completo solo alcanza consistencia eventual. Esto genera escenarios donde los usuarios ven información desfasada o donde una transacción parcial debe revertirse con pasos compensatorios.

Para controlar estas brechas se definen tiempos de expiración de caché, se comunica la frescura disponible de los datos al usuario y se monitorean métricas de lag entre eventos. Los equipos también deben acordar semánticas idempotentes en los comandos de escritura para evitar duplicados ante reintentos.

5.2.1 Ejemplo de consistencia eventual

Imaginemos un servicio de pedidos que confirma compras y un servicio de facturación que emite comprobantes. El pedido se marca como confirmado inmediatamente, pero la factura puede demorar unos segundos. Se informa al usuario que la factura estará disponible cuando llegue la notificación por correo, minimizando confusiones.

5.3 Patrones de integración

Para coordinar cambios entre servicios se adoptan patrones diseñados para manejar la inconsistencia temporal y las fallas parciales.

5.3.1 Saga: coordinación de transacciones distribuidas

El patrón Saga divide una transacción de negocio en pasos locales con operaciones compensatorias. Un orquestador o una coreografía de eventos decide el siguiente paso según el resultado anterior. En caso de error, se ejecutan las compensaciones para revertir el estado.

Una implementación frecuente utiliza un servicio orquestador que invoca cada paso y escucha el resultado mediante eventos. Este enfoque facilita el monitoreo centralizado, aunque puede introducir un punto único de fallo. Como alternativa, la coreografía deja que cada servicio reaccione a eventos y dispare el siguiente paso, lo que distribuye la responsabilidad.

5.3.1.1 Ejemplo de orquestador Saga

El siguiente fragmento muestra un orquestador que coordina la reserva de inventario y la aprobación de pagos. Se emplean comandos idempotentes y eventos de respuesta para continuar el flujo.

@Service
public class CheckoutSaga {

    private final InventoryClient inventoryClient;
    private final PaymentClient paymentClient;
    private final ApplicationEventPublisher eventPublisher;

    public CheckoutSaga(InventoryClient inventoryClient,
                        PaymentClient paymentClient,
                        ApplicationEventPublisher eventPublisher) {
        this.inventoryClient = inventoryClient;
        this.paymentClient = paymentClient;
        this.eventPublisher = eventPublisher;
    }

    public void startCheckout(CheckoutCommand command) {
        inventoryClient.reserve(command.sku(), command.quantity(), command.traceId());
    }

    @EventListener
    public void onInventoryReserved(InventoryReserved event) {
        paymentClient.authorize(event.checkoutId(), event.total(), event.traceId());
    }

    @EventListener
    public void onPaymentAuthorized(PaymentAuthorized event) {
        eventPublisher.publishEvent(new CheckoutCompleted(event.checkoutId(), event.traceId()));
    }

    @EventListener
    public void onInventoryRejected(InventoryRejected event) {
        eventPublisher.publishEvent(new CheckoutFailed(event.checkoutId(), "Sin stock", event.traceId()));
    }

    @EventListener
    public void onPaymentRejected(PaymentRejected event) {
        inventoryClient.release(event.checkoutId(), event.traceId());
        eventPublisher.publishEvent(new CheckoutFailed(event.checkoutId(), "Pago rechazado", event.traceId()));
    }
}

La orquestación facilita la trazabilidad porque concentra el estado global, pero requiere resguardar el orquestador con mecanismos de alta disponibilidad.

5.3.2 CQRS: separar comandos y consultas

El patrón CQRS divide el modelo de datos en dos vistas: una optimizada para comandos (escrituras) y otra para consultas (lecturas). Esto permite usar almacenes distintos y escalar la lectura mediante replicación o caché dedicada. Cada cambio en el modelo de comandos publica eventos que actualizan la vista de lectura.

Una aplicación de pedidos puede mantener un modelo transaccional relacional para escrituras y enviar eventos a una base orientada a documentos que alimenta el buscador de pedidos. De esta forma, la carga de consultas no degrada el rendimiento de las escrituras.

5.3.3 Event Sourcing: reconstruir estado desde eventos

Event Sourcing persiste cada mutación del dominio como un evento inmutable. El estado actual se calcula reproduciendo la secuencia de eventos. Esta técnica ofrece trazabilidad completa y facilita reacciones tardías, aunque exige diseñar correctamente la evolución de los eventos y gestionar el tamaño del log.

5.3.3.1 Ejemplo de Event Sourcing en Java

Este ejemplo ilustra un agregado de cuentas que reproduce eventos para calcular el balance y genera nuevos eventos al procesar comandos.

public class AccountAggregate {

    private final List<DomainEvent> changes = new ArrayList<>();
    private BigDecimal balance = BigDecimal.ZERO;
    private boolean closed = false;

    public void loadFromHistory(List<DomainEvent> history) {
        history.forEach(this::apply);
    }

    public void handle(DepositCommand command) {
        ensureOpen();
        applyChange(new MoneyDeposited(command.accountId(), command.amount()));
    }

    public void handle(WithdrawCommand command) {
        ensureOpen();
        if (balance.compareTo(command.amount()) < 0) {
            throw new IllegalStateException("Fondos insuficientes");
        }
        applyChange(new MoneyWithdrawn(command.accountId(), command.amount()));
    }

    private void applyChange(DomainEvent event) {
        apply(event);
        changes.add(event);
    }

    private void apply(DomainEvent event) {
        if (event instanceof MoneyDeposited deposited) {
            balance = balance.add(deposited.amount());
        } else if (event instanceof MoneyWithdrawn withdrawn) {
            balance = balance.subtract(withdrawn.amount());
        } else if (event instanceof AccountClosed) {
            closed = true;
        }
    }

    private void ensureOpen() {
        if (closed) {
            throw new IllegalStateException("Cuenta cerrada");
        }
    }

    public List<DomainEvent> getUncommittedChanges() {
        return Collections.unmodifiableList(changes);
    }
}

La replicación de eventos se realiza mediante colas o streams persistentes. Es importante definir políticas de snapshot para reconstruir el estado sin reproducir todos los eventos desde el inicio.

5.4 Replicación y sincronización eventual

La replicación permite que varios servicios consuman datos sin consultar directamente a la fuente cada vez. Un servicio propietario puede publicar eventos que otros almacenan en sus propias vistas materializadas. Tecnologías como Amazon DynamoDB ofrecen replicación multi-región administrada, mientras que motores relacionales suelen apoyarse en logical replication o en change data capture.

La sincronización eventual requiere monitorear el retraso entre la fuente y la réplica. Cuando se superan los umbrales definidos, se disparan alertas y se activa la degradación controlada, por ejemplo limitando operaciones que dependen de datos frescos. También es conveniente incorporar checksums periódicos para detectar discrepancias silenciosas.

En escenarios con escrituras concurrentes es necesario definir la estrategia de resolución de conflictos: primera escritura en ganar, ordenamiento por reloj lógico o mezcla de cambios. Las decisiones deben alinearse con la semántica del dominio y comunicarse claramente a los consumidores.