Integración con otros patrones

Un sistema puede usar DFE sin caché, sin eventos y sin varios servicios. DFE organiza las responsabilidades del código; no decide por nosotros cómo desplegar el sistema ni cómo comunicarlo con otros.

Aquí muestro algunas decisiones que pueden aparecer en un proyecto y dónde encajan. Son ejemplos de aplicación, no requisitos para decir que un sistema usa DFE. Tampoco son patrones nuevos de DFE: contexto de ejecución, invalidación de caché y outbox resuelven problemas conocidos.

Contexto de ejecución

Si una operación necesita saber para qué tenant trabaja, podemos pasar esa información como argumento. No hace falta tener microservicios para necesitarla: un solo proceso puede atender a varios tenants.

En este ejemplo, la entrada en API obtiene el tenant a partir de la identidad autenticada y comprueba que la petición puede operar sobre él. También establece un identificador para seguir la ejecución. El contexto podría tener esta forma:

pub struct TenantContext {
    pub tenant_id: TenantId,
    pub tenant_slug: String,
    pub currency: String,
    pub language: String,
    pub timezone: String,
    pub request_id: Uuid,
}

El contexto contiene datos de la ejecución, no servicios ni un contenedor de dependencias. Los datos estables del tenant pueden reutilizarse; el identificador de la ejecución corresponde a esa petición, no al tenant compartido.

No hace falta pasar la estructura completa a todas las funciones. Si el flow solo necesita tenant_id, puede recibir ese dato. Si una regla usa la moneda, API le entrega también esa información. El dominio no necesita conocer HTTP ni el mecanismo de autenticación.

Tener un TenantContext no garantiza el aislamiento entre tenants. Las consultas y las escrituras deben respetar ese alcance, con filtros, restricciones o políticas de base de datos según el diseño. No basta con aceptar un identificador enviado por el cliente.

En DFE, API prepara el contexto, el flow recibe lo que necesita para aplicar las reglas y fx/db ejecuta las consultas o escrituras con ese alcance. En el carrito sencillo no hay tenants y no hace falta añadir este contexto.

Una caché después de la escritura

Supongamos que consultar el carrito es costoso y decidimos guardar una copia en caché. Esa decisión exige definir cuánto tiempo podemos mostrar un dato viejo y qué hacemos si la caché deja de responder. DFE no elige esa tolerancia por nosotros.

Una opción es invalidar la entrada después de confirmar la escritura:

api → flow consulta y aplica reglas
api → fx/db guarda el resultado
api → commit
api → fx/cache invalida la entrada

La coordinación queda en API y la llamada a la caché en fx/cache. El flow sigue devolviendo su resultado sin invalidar nada. Si el mismo guion se usa desde HTTP y gRPC, puede compartirse en api/actions/.

Esperar a que termine la invalidación antes de responder no garantiza coherencia en todo el sistema. Hay un intervalo entre el commit y la invalidación, y una lectura concurrente puede volver a guardar un dato viejo. Si cada instancia tiene su propia caché, invalidar la de este proceso tampoco limpia las demás.

Según el problema, podemos necesitar expiración, versiones de los datos o coordinación entre instancias. Si falla la invalidación después del commit, el cambio ya está guardado: registramos el fallo y aplicamos una política de recuperación, sin fingir que la escritura se revirtió. La respuesta debe distinguir un cambio persistido de una consecuencia pendiente para no provocar reintentos que dupliquen la operación.

Eventos y el intervalo después del commit

Un evento puede avisar a otros servicios de que el pedido cambió. Sus consumidores pueden actualizar proyecciones, refrescar cachés o iniciar otro trabajo. Usarlo es una decisión del sistema, no una condición de DFE.

Publicar directamente después del commit es sencillo, pero deja un problema: el proceso puede terminar cuando el pedido ya está guardado y el mensaje todavía no se ha publicado. Registrar el error ayuda a investigarlo, pero no garantiza que el aviso llegue.

Si perder ese aviso es aceptable y los consumidores pueden recuperar los datos de otra forma, una publicación directa puede bastar. Si la entrega es necesaria, hace falta un mecanismo que conserve el trabajo pendiente.

Cuando hace falta outbox

Con outbox, API guarda el cambio y un registro del evento pendiente en la misma transacción de base de datos. No publica al bus dentro de esa transacción:

api abre la transacción
 ├── flow consulta, aplica reglas y devuelve el resultado
 ├── api → fx/db guarda el resultado
 ├── api → fx/db guarda el evento pendiente en outbox
 └── api hace commit

publicador de outbox
 ├── consulta eventos pendientes mediante fx/db
 ├── publica mediante fx/events
 └── registra el envío mediante fx/db

El flow no escribe el pedido ni el registro de outbox. Puede devolver los hechos que resultan de la decisión de negocio; API coordina su persistencia. El publicador es otra entrada de ejecución, no una petición HTTP: su coordinación puede vivir en un módulo de API y usar las mismas funciones de fx/. No necesita un flow si solo entrega mensajes pendientes sin aplicar reglas de negocio.

Outbox evita separar el commit del cambio y el registro del aviso pendiente, siempre que ambos se guarden en la misma transacción. No garantiza por sí solo entrega exactamente una vez: el publicador puede enviar un mensaje y fallar antes de marcarlo como enviado. Al recuperarse, puede publicarlo otra vez.

Reintentos y mensajes repetidos

Si el transporte permite duplicados, el consumidor necesita reconocer mensajes ya procesados o aplicar una operación idempotente: repetirla no debe duplicar su resultado. Para un cobro, por ejemplo, no basta con repetir la llamada; conviene usar el identificador de idempotencia que admita el proveedor.

La política de reintentos debe distinguir un fallo transitorio de uno que requiere intervención. Podemos limitar intentos, espaciar los reintentos y conservar los mensajes que no se pudieron procesar para revisarlos. Los tiempos, el número de intentos y el mecanismo de confirmación dependen del sistema.

DFE permite ubicar la coordinación y los clientes externos, pero no sustituye ese diseño de entrega. Si el proveedor no permite saber si una acción se completó, cambiar de carpeta la llamada no elimina la incertidumbre.

HTTP y gRPC pueden compartir la operación

Un proyecto puede usar HTTP, gRPC, ambos o ninguno de los dos. No hay un transporte interno o público obligatorio en DFE.

Si las dos entradas exponen la misma operación, cada una convierte su petición a los datos que necesita el flow y traduce los errores a su protocolo. El flow no cambia para devolver un estado HTTP o un tonic::Status:

HTTP → adapta la petición ──┐
                           ├── mismo flow → resultado
gRPC → adapta la petición ──┘

La escritura y las acciones posteriores siguen coordinadas por API. Si las comparten las dos entradas, conviene extraer ese código dentro de API en lugar de copiarlo o moverlo al flow.

Estas integraciones pueden crecer sin convertirse en nuevas reglas de DFE. Primero se decide qué necesita el sistema; después se usa la separación de responsabilidades para que esa decisión pueda encontrarse y mantenerse.