Lección 3 de 912 min
CQRS, Event Sourcing, BFF y micro-frontends
Separar lecturas y escrituras, guardar hechos en vez de estados y adaptar el backend y el frontend a cada equipo.
Tu tienda online recibe cien lecturas del catálogo por cada compra. La pantalla de inicio de la app móvil hace siete peticiones para mostrarse. Y cinco equipos se pisan en el mismo proyecto de Angular. Son tres problemas distintos con algo en común: un único modelo o una única pieza intenta servir a todos por igual.
Los patrones de esta lección separan lo que antes iba junto: las lecturas de las escrituras (CQRS), el estado de su historia (Event Sourcing), el backend de cada tipo de cliente (BFF) y el frontend de cada equipo (micro-frontends).
Separar lecturas, escrituras e historia
CQRS
Un modelo para cambiar datos (comandos) y otro distinto para leerlos (consultas).
- Separa:
- Comandos y consultas
CQRS (Command Query Responsibility Segregation) divide la aplicación en dos lados. El de escritura recibe comandos («ConfirmarPedido») y aplica las reglas de negocio sobre un modelo normalizado. El de lectura responde consultas con modelos hechos a medida de cada pantalla, ya calculados y listos para mostrar.
Cada vez que algo cambia en el lado de escritura, se actualizan las vistas de lectura, a menudo de forma asíncrona mediante eventos. Así cada lado escala y se optimiza por separado: las lecturas pueden ir a réplicas, a una caché o a un buscador.
Úsalo cuando
Cuando las lecturas y las escrituras son muy distintas en volumen o en forma: paneles con datos agregados, catálogos que se leen mucho más de lo que se modifican, búsquedas complejas.
Ten en cuenta
Si las vistas se actualizan de forma asíncrona, hay consistencia eventual: el usuario puede no ver su cambio durante un instante, y la interfaz debe tenerlo en cuenta. En un CRUD sencillo, CQRS duplica el trabajo sin beneficio.
// Lado de escritura: un comando con reglas de negocio
export async function confirmarPedido(cmd: { pedidoId: string }) {
const pedido = await repositorio.buscar(cmd.pedidoId);
pedido.confirmar(); // valida stock, estado, etc.
await repositorio.guardar(pedido);
await eventos.publicar("PedidoConfirmado", { pedidoId: pedido.id });
}
// Lado de lectura: una vista ya preparada para la pantalla
export async function resumenDePedidos(clienteId: string) {
return db.query("SELECT * FROM vista_resumen_pedidos WHERE cliente_id = $1", [clienteId]);
}Event Sourcing
Guardar la lista de hechos que han ocurrido, en lugar de solo el estado actual.
- Los eventos:
- Solo se añaden
Normalmente, una base de datos guarda el estado actual: «saldo: 120 €». Con Event Sourcing guardas la secuencia de eventos que llevaron hasta ahí: «Abierta», «Ingresados 200 €», «Retirados 80 €». Los eventos solo se añaden, nunca se modifican ni se borran, y el estado actual se obtiene reproduciéndolos en orden.
Ganas un historial completo y fiable: puedes auditar cualquier cambio, reconstruir el estado de cualquier fecha pasada y crear vistas nuevas reproduciendo eventos antiguos. Para no recorrer miles de eventos en cada lectura se usan snapshots (fotos del estado cada cierto número de eventos) y proyecciones, que suelen ser el lado de lectura de CQRS.
Úsalo cuando
En dominios donde la historia importa tanto como el estado: contabilidad, banca, seguros, logística o cualquier sistema que exija auditoría.
Ten en cuenta
Cambiar el formato de los eventos ya guardados es delicado, porque están ahí para siempre: hay que versionarlos. Una consulta como «clientes con saldo negativo» necesita una proyección. Y borrar datos personales choca con el «nunca se borra»; una solución habitual es cifrarlos y destruir la clave.
type Evento =
| { tipo: "Ingresado"; importe: number }
| { tipo: "Retirado"; importe: number };
// El estado no se guarda: se calcula a partir de los hechos
function saldo(eventos: Evento[]) {
return eventos.reduce(
(total, e) => (e.tipo === "Ingresado" ? total + e.importe : total - e.importe),
0,
);
}
const historial: Evento[] = [
{ tipo: "Ingresado", importe: 200 },
{ tipo: "Retirado", importe: 80 },
];
saldo(historial); // 120, y sabes exactamente por quéCQRS no exige Event Sourcing
Se presentan juntos porque encajan bien (los eventos alimentan las vistas de lectura), pero son independientes. Puedes aplicar CQRS con una base de datos normal y una réplica de lectura. Y casi nunca necesitas Event Sourcing en todo el sistema: úsalo solo donde la historia aporte valor.
Un backend por cliente, un frontend por equipo
Backend for Frontend (BFF)
Un backend pequeño hecho a medida de cada frontend: web, móvil, televisión…
- Lo mantiene:
- El equipo del frontend
Una API general sirve a todos los clientes por igual, y eso no contenta a ninguno: la app móvil recibe campos que no usa y necesita siete peticiones para pintar una pantalla. Un BFF es una capa de backend dedicada a un frontend concreto: agrega llamadas a los servicios internos y recorta y da forma a los datos tal como esa interfaz los necesita.
Suele mantenerlo el mismo equipo que hace el frontend, así que evoluciona al ritmo de la interfaz. También es un buen sitio para guardar los tokens en el servidor en lugar de en el navegador, algo que verás en el módulo de autenticación.
Úsalo cuando
Cuando tienes clientes con necesidades muy distintas (web, móvil, otros dispositivos), muchas llamadas por pantalla o redes lentas en las que cada viaje cuenta.
Ten en cuenta
Un BFF por cliente multiplica el código: si los BFF empiezan a duplicar lógica de negocio, esa lógica debe bajar a los servicios. Un BFF traduce y agrega; no decide reglas de negocio.
// Una sola llamada desde la app en lugar de tres
app.get("/movil/inicio", async (req, res) => {
const [usuario, pedidos, ofertas] = await Promise.all([
servicios.usuarios.obtener(req.usuarioId),
servicios.pedidos.ultimos(req.usuarioId, 3),
servicios.ofertas.destacadas(),
]);
// Solo los campos que pinta la pantalla del móvil
res.json({
saludo: `Hola, ${usuario.nombre}`,
pedidos: pedidos.map((p) => ({ id: p.id, estado: p.estado })),
oferta: ofertas[0]?.titulo,
});
});Micro-frontends
Dividir un frontend en partes que construyen y despliegan equipos distintos.
- Integración:
- Module Federation, web components
Los micro-frontends llevan la idea de los microservicios a la interfaz. Cada equipo es dueño de una parte de la aplicación (el catálogo, el carrito, la cuenta de usuario) de principio a fin, y la despliega cuando quiere. Una aplicación contenedora (shell) carga esas partes y las compone en la página.
Hay varias formas de integrarlas. Module Federation permite que una aplicación cargue en tiempo de ejecución módulos de JavaScript publicados por otra; en Angular se usa a menudo Native Federation, que aplica la misma idea apoyándose en estándares del navegador (módulos ES e import maps). Otra opción son los web components: cada equipo publica elementos personalizados, como <carrito-compra>, que funcionan con cualquier framework.
Los costes son reales: más JavaScript si cada parte trae su propio framework o versiones distintas de una dependencia, riesgo de una experiencia visual incoherente y más complejidad en el build, el enrutado y las pruebas de extremo a extremo.
Úsalo cuando
Con varios equipos que trabajan en la misma aplicación y necesitan desplegar sin coordinarse, o para migrar poco a poco una aplicación antigua a un framework nuevo.
Ten en cuenta
Con un solo equipo, o con pocos, casi siempre es mala idea: un monorepo bien organizado da casi todo el beneficio con muchos menos problemas. Comparte un sistema de diseño para que las partes no parezcan de webs distintas.
// El equipo del carrito publica un elemento personalizado
class CarritoCompra extends HTMLElement {
connectedCallback() {
const unidades = this.getAttribute("unidades") ?? "0";
this.innerHTML = `<button>Carrito (${unidades})</button>`;
}
}
customElements.define("carrito-compra", CarritoCompra);
// La aplicación contenedora solo necesita cargar su script:
// <script type="module" src="https://carrito.ejemplo.com/carrito.js"></script>
// <carrito-compra unidades="3"></carrito-compra>Nota
BFF y micro-frontends combinan bien: cada equipo de frontend puede tener su propio BFF y así controla de punta a punta su parte del producto, desde los datos hasta los píxeles.
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
El panel de ventas calcula totales por región con consultas enormes que bloquean las tablas y ralentizan la confirmación de pedidos.
Para recordar
- CQRS separa el modelo que cambia los datos del que los lee para optimizar cada uno; casi siempre trae consistencia eventual.
- Event Sourcing guarda hechos inmutables y calcula el estado a partir de ellos: historia completa a cambio de más complejidad.
- CQRS y Event Sourcing combinan bien, pero son independientes, y ninguno es para todo el sistema.
- Un BFF adapta los datos a un frontend concreto; no debe acumular lógica de negocio.
- Los micro-frontends resuelven un problema de organización entre equipos: con un solo equipo, sobran.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.En CQRS, ¿qué es un comando?
2.Con CQRS y vistas que se actualizan de forma asíncrona, un usuario cambia su nombre y, al recargar, aún ve el antiguo durante un segundo. ¿Qué ocurre?
3.En un sistema con Event Sourcing, el cliente pide corregir un ingreso que se registró por error. ¿Qué haces?
4.¿Qué no debería hacer un BFF?
5.¿Cuál es un coste típico de los micro-frontends?