Un comercial está al teléfono, prometiendo una fecha de entrega. El número que ve en pantalla viene de Salesforce. Si ese número lo refrescó por última vez un batch que corrió a las dos de la madrugada, la promesa está construida sobre un dato que ya puede tener entre doce y veinticuatro horas de retraso. Nadie se da cuenta hasta que el cliente vuelve a llamar preguntando por qué el pedido que ayer estaba "confirmado" no aparece por ningún lado. Ese es el fallo que empuja a los equipos del batch a los eventos — no una diapositiva sobre transformación digital, sino una promesa concreta rota delante de un cliente.
Por qué eventos y no batch
La integración por batch funciona bien durante mucho tiempo. Extraes el delta cada noche, haces upsert, sigues adelante. Es sencilla de construir, sencilla de razonar, y para la mayoría del dato de back-office a nadie le importa si tiene seis horas. El problema empieza en el momento en que el negocio comienza a hacer compromisos en tiempo real sobre ese dato — una fecha de entrega, una cantidad disponible para vender, un agente de soporte citando el saldo de una cuenta. Ahí la obsolescencia deja de ser un error de redondeo: es una mentira que el sistema cuenta en nombre del negocio.
El giro hacia event-driven es un cambio de quién inicia la conversación. En un mundo batch, Salesforce (o la capa de integración) le pregunta al sistema origen: "¿qué ha cambiado desde la última vez?". En un mundo event-driven, el sistema origen no espera a que le pregunten — en el momento en que un hecho pasa a ser cierto, lo publica. El trabajo de la integración cambia de hacer polling a reaccionar.
Dos propiedades de esa reacción importan más que ninguna otra cosa en el diseño que viene después. Primero, el orden: si dos eventos de la misma entidad llegan seguidos, hay que procesarlos en el orden en que ocurrieron, o la lógica de "gana el último" aguas abajo los aplicará encantada al revés. Una garantía FIFO — eventos de la misma entidad procesados en el orden de publicación — elimina de raíz toda una categoría de bugs del tipo "por qué se ha revertido este registro". Segundo, la semántica de entrega: la mayoría de brokers garantizan entrega at-least-once, no exactamente una vez. Es una decisión de diseño deliberada por parte del broker, y significa que el mismo evento puede, y eventualmente va a, entregarse dos veces. Si el consumidor no está construido para manejar eso con elegancia, no tienes una integración — tienes un generador de números aleatorios. Más adelante, cómo sobrevivir a eso.
Tres capas, un trabajo cada una
API-led connectivity te da un vocabulario para repartir el trabajo — Experience, Process, System — pero los nombres por sí solos no dicen mucho hasta que has visto qué se niega a hacer cada capa. En un flujo event-driven, las tres capas se mapean con limpieza sobre el camino que recorre un evento desde el broker hasta Salesforce.
Experience se sitúa lo más cerca posible del broker. Su único trabajo es coger el payload en bruto — el formato de envelope, las cabeceras y la codificación que use el broker en cuestión — y convertirlo en la forma que el resto del sistema ha acordado internamente. Conoce las manías del broker para que nada más tenga que conocerlas.
Process es la única capa que sabe qué significa un evento para el negocio. Decide si ese evento importa ahora mismo, qué hay que enriquecer antes de que sea utilizable, y en qué orden tienen que ocurrir las llamadas aguas abajo. Orquesta; no habla directamente ni con Salesforce ni con ninguna API externa.
System es donde ocurren las conversaciones de protocolo reales. Habla el idioma del conector de Salesforce y el de la API de enriquecimiento — nombres de campo, tipos de objeto, autenticación, paginación. No aplica ninguna lógica de negocio; simplemente ejecuta lo que Process le ha pedido, con fidelidad y de forma idempotente.
La simplicidad es rechazar trabajo
Escribir qué hace cada capa es la mitad fácil del ejercicio. La mitad que de verdad importa — la que determina si esta arquitectura sigue siendo coherente dentro de dos años — es escribir qué no puede hacer cada capa.
- Experience se niega a conocer reglas de negocio. No puede decidir si un evento merece una acción. Si una decisión de filtrado se cuela en esta capa, acabas de hacer invisible una regla de negocio para cualquiera que no esté leyendo el código del adaptador del broker.
- Process se niega a conocer nombres de campo de Salesforce. Trabaja enteramente en el modelo de dominio — cliente, pedido, entitlement — nunca en nombres de API. En el momento en que un nombre de campo de un objeto de Salesforce aparece en un script de orquestación, Process se ha acoplado a System en silencio.
- System se niega a transformar. Si un payload necesita reformatearse antes de escribirse, ese reformateo ocurre aguas arriba, en Process. El DataWeave de System debería leerse casi como un mapeo directo de campos — porque si no es así, está cargando con lógica que pertenece a otro sitio.
Esta disciplina se paga sola exactamente en los momentos que antes salían caros. Cuando un admin de Salesforce renombra un campo, cambia una capa. Cuando el equipo del broker cambia el formato del envelope — añade una cabecera, renombra una clave de metadata —, cambia una capa. Nada más en el flujo necesita enterarse de que ha pasado ninguna de las dos cosas.
Una capa que hace dos cosas acabará cambiando por dos razones. Ese es todo el argumento.
Cinco patrones que sobreviven al cambio
Más allá del propio reparto en capas, hay un puñado de patrones concretos que marcan la diferencia entre una integración que envejece bien y una que acaba reescrita en silencio dieciocho meses después.
Contratos publicados antes que código
Escribe la especificación OpenAPI o RAML de la interfaz de cada capa y publícala — en Anypoint Exchange o donde tu equipo las guarde — antes de que la implementación esté terminada. Quien consume esa interfaz construye contra el contrato mientras la implementación de debajo todavía se está moviendo. La especificación se convierte en el artefacto de negociación entre equipos, en lugar de un hilo de Slack que nadie encuentra seis meses después.
Propiedades por entorno con secretos cifrados
Propiedades seguras, cifradas en reposo, con la clave de descifrado inyectada por entorno en tiempo de ejecución. Ninguna credencial — API key, secreto de connected app, contraseña del broker — vive nunca en el repositorio en texto legible. Promocionar un flow de un entorno al siguiente cambia un fichero de propiedades, no una línea de código de la aplicación, que es exactamente el límite que quieres entre "configuración" y "lógica".
Upsert idempotente por external ID
Este es el patrón individual más importante en cuanto aceptas la entrega at-least-once como un hecho de la vida y no como un caso raro. Hacer upsert de un registro de Salesforce usando como clave un external ID estable significa que reproducir el mismo evento dos veces produce la misma fila, no dos filas ni un dolor de cabeza de detección de duplicados. Es lo que convierte "el broker ha reentregado este mensaje" en un no-evento en lugar de un incidente. Toda escritura de la capa System que toque Salesforce debería construirse así por defecto, no como un parche añadido después del primer ticket de registro duplicado.
Tolerancia a fallos por registro
En cualquier batch — incluido el batch de reconciliación del que hablamos más abajo — un registro malformado nunca debe poder tumbar los otros nueve mil buenos. La implementación ingenua procesa el batch como una sola transacción y muere en la primera fila mala. El patrón que sobrevive a producción aísla el fallo de cada registro, lo registra con contexto suficiente (el ID de origen, el payload, el error) para poder reprocesarlo después, y deja que el batch siga avanzando. Un registro fallido se convierte en una entrada de cola, no en una caída del servicio.
Logging categorizado
Cada línea de log lleva una categoría y un ID de correlación que sigue a un único evento de negocio a través de las tres capas. "Qué le ha pasado a este pedido concreto" debería poder responderse con una consulta de log filtrada por ese ID de correlación, no con una tarde entera haciendo grep en tres logs de runtime distintos tratando de alinear timestamps. La observabilidad diseñada dentro del flujo desde el día uno cuesta casi nada; la observabilidad añadida a parches después del primer incidente en producción cuesta una reescritura.
Las decisiones difíciles
Todo lo anterior es casi un problema resuelto — trillado, poco controvertido. Las decisiones de abajo no lo son. Cada una tiene un coste real en los dos lados, y la respuesta "correcta" depende de detalles que ningún playbook genérico te puede dar.
Conector vs DataWeave escrito a mano
El conector de Salesforce gestiona autenticación, comportamiento de retry, batching y deriva de versión de API por ti, gratis, siempre que te quedes dentro de lo que fue diseñado para hacer. El valor por defecto siempre debería ser: usa el conector. Recurres a DataWeave escrito a mano contra la API REST o Bulk solo cuando el mapeo es genuinamente no trivial, o cuando el comportamiento incorporado del conector esconde algo que necesitas controlar específicamente — una política de retry que no encaja con tu volumen, una estrategia de batching que no encaja con la forma de tu dato. El modo de fallo aparece en ambas direcciones: pelearte horas con el conector para que haga algo para lo que nunca fue construido, o escribir a mano trescientas líneas de DataWeave para resolver un problema que el conector ya resolvía correctamente.
Dónde se sitúa el límite del cifrado
Cifrar a nivel de propiedad mantiene el secreto fuera del repositorio por completo — pero el proceso en tiempo de ejecución sigue teniendo la clave de descifrado en memoria, y ese es un límite en el que tienes que confiar. Cifrar a nivel de payload — campos individuales dentro del mensaje — cierra ese hueco, pero pagas el coste de ese cifrado en cada salto por el que pasa el payload, y depurar se vuelve notablemente más difícil cuando no puedes leer una línea de log sin un paso de descifrado. La regla general que aguanta: cifra las credenciales en reposo como propiedades seguras, siempre. Cifra campos del payload solo cuando una normativa concreta lo exija de verdad para ese dato — no como postura por defecto "por si acaso".
Tolerar un 404 de la API de enriquecimiento
Aquí está el escenario: Salesforce conoce una entidad — un cliente, una cuenta — de la que la API externa de enriquecimiento nunca ha oído hablar. Es un estado completamente normal, no un fallo de calidad de dato. Trátalo como un error y paras el flujo por algo que ocurre de forma rutinaria. Trátalo como silencio — te tragas el 404 y sigues — y has perdido la señal de que ese registro necesita atención. La respuesta que de verdad funciona es una tercera opción: seguir adelante con el registro sin enriquecer, marcarlo como tal, y dejar que vuelva a salir a la superficie en el siguiente pase de reconciliación. Por eso, precisamente, en la arquitectura de referencia de arriba la llamada de enriquecimiento se dibuja como best-effort y no como una dependencia dura en el camino crítico.
Por qué sigues necesitando el batch
Los eventos en tiempo real son el camino rápido, no el camino completo. Un job de reconciliación nocturno sigue siendo necesario por razones que no tienen nada que ver con lo bien construido que esté el pipeline de eventos:
- Los eventos se pierden. Las ventanas de retención del broker expiran, un consumidor está caído durante un despliegue, un mensaje acaba aparcado en una DLQ y ese día nadie la está vigilando.
- El enriquecimiento best-effort a veces falla y se queda fallado. El registro del escenario del 404 de arriba necesita una segunda oportunidad para recoger el enriquecimiento que no consiguió la primera vez.
- El sistema origen se corrige fuera de banda. Alguien arregla un registro directamente en una base de datos origen, o mediante un proceso que no emite ningún evento. El stream de eventos nunca ve ese cambio; el batch es lo único que lo va a ver.
Plantéalo así: el stream de eventos te da velocidad, el batch te da corrección. Cualquiera que te diga que los eventos en tiempo real eliminan la necesidad de reconciliar no ha llevado uno de estos sistemas en producción durante un año entero — el batch es lo que atrapa en silencio todo lo que el camino rápido nunca iba a ver.
Cuándo esto es sobreingeniería
Nada de esto es gratis. Este patrón cuesta tres desplegables independientes que construir, probar y operar, un broker que correr y monitorizar, una disciplina de publicación de contratos que el equipo tiene que mantener de verdad, y una configuración de observabilidad que alguien tiene que sostener. No pagues esa factura si:
- El volumen es bajo y una obsolescencia horaria — o incluso diaria — es realmente aceptable para el negocio. Un único job programado es algo mucho más pequeño de mantener.
- Hay exactamente un origen y un destino con un mapeo estable y sencillo. El reparto en capas existe para absorber cambio que llega desde varias direcciones a la vez; con un origen y un destino, todavía no te está comprando nada.
- El equipo no tiene ningún apetito real por operar un broker. Un patrón que nadie quiere operar a las tres de la madrugada es un patrón que se abandonará a las tres de la madrugada.
Se recupera la inversión cuando hay varios orígenes alimentando un único destino, cuando el negocio está haciendo compromisos en vivo sobre ese dato, o cuando el roadmap ya tiene más sistemas en cola para sumarse al flujo. En esos casos el reparto en capas no es ceremonia — es lo único que se interpone entre tú y una reescritura dentro de dieciocho meses.
Si estás valorando si tu propia integración necesita esta forma o algo más sencillo, encantado de reservar una conversación y pensarlo juntos.