Las arquitecturas modernas de Salesforce están cada vez más impulsadas por el procesamiento asíncrono; no como una comodidad, sino como un requisito estratégico para la escala. En los últimos años, hemos visto a más y más empresas lidiar con volúmenes de datos crecientes, integraciones complejas que implican múltiples puntos de contacto y el auge de sistemas autónomos que funcionan 24/7/365. Todas estas cosas empujan a los arquitectos hacia el diseño de sistemas que son asíncronos en primer lugar.

El procesamiento asíncrono en Salesforce a menudo significa diseñar alrededor de límites reguladores y complejidad. Esos límites actúan como barandillas y restricciones arquitectónicas que ayudan a producir sistemas escalables y seguros en masa. Aunque ningún límite de plataforma sirve directamente para gestionar la complejidad, los patrones de diseño pueden ayudar a mitigar el riesgo en ese frente. Internamente, Salesforce a menudo amplía los límites de la plataforma para probar nuevas funciones y automatizar procesos comerciales complejos. Construimos un marco de trabajo de procesamiento asíncrono basado en pasos para ejecutar trabajos asíncronos con un número arbitrario de pasos. Cada paso puede ejecutarse, reintentarse y reiniciarse de forma independiente con controles de gobernanza compartidos y visibilidad operativa completa a través del registro centralizado. Este documento describe sus componentes arquitectónicos clave: Apex y Finalizadores en cola, Flujo programado, Cursores Apex, Acciones invocables e integraciones con Slack. Juntos, estos componentes proporcionan una arquitectura modular, ampliable y observable adecuada para necesidades empresariales en evolución.

  • Las arquitecturas modernas de Salesforce deben adoptar un enfoque asíncrono primero para alcanzar la escala, la resiliencia y la transparencia operativa.
  • Dividir el trabajo complejo en pasos ejecutables de forma independiente permite un rendimiento predecible, reintentos más seguros, puntos de control, reversión y evolución modular sin volver a diseñar flujos de trabajo principales.
  • El marco de trabajo proporciona una alternativa ampliable a trabajos por lotes monolíticos y antiguos, llamadas asíncronas encadenadas y flujos profundamente anidados, y está creado para cargas de trabajo de gran volumen que deben ampliarse horizontalmente dentro de Salesforce sin orquestación fuera de la plataforma.
  • La ejecución determinista y observable garantiza el seguimiento del progreso, la supervisión de SLA, el diagnóstico de fallos y la transparencia a nivel de auditoría a través del registro y la gobernanza centralizados.
  • Diseñado para el rigor de nivel empresarial, incluyendo la gobernanza unificada, el cumplimiento y el control de estado distribuido entre procesos comerciales de larga ejecución.

Antes de revisar los requisitos, estos son algunos aspectos que se deben y no se deben tener en cuenta para cuándo utilizar un marco de trabajo como este. Sobre todo, considere qué sistema es la única fuente de verdad. Si su organización de Salesforce depende mínimamente de datos externos pero necesita ampliarse de cientos a millones de registros, considere un marco de trabajo asíncrono basado en pasos.

utilice este marco si:

  • La mayoría (o toda) la información sobre la que actuar ya existe en su CRM.
  • El coste inicial o continuo de mantener un trabajo de carga de transformación de extracción (ETL) para armonizar datos externos es demasiado alto.
  • Debe aplazar el procesamiento de un gran número de registros de Salesforce en una programación establecida.
  • Puede desglosar el procesamiento en pasos discretos. Por ejemplo, puede crear un conjunto de registros jerárquico o basado en árboles, especialmente si los aficionados al volumen de datos están fuera de la jerarquía o el árbol.

No utilice este marco de trabajo si:

  • La creación o actualización de registros requiere un nuevo cálculo inmediato.
  • La integración es un reto porque los sistemas externos alojan datos principales para actualizaciones de registros. (Considere enviar datos actualizados a Salesforce con la API masiva.)

Con esas prácticas en mente, revisemos nuestros requisitos y comencemos a construir.

Considere la declaración de problema:

Dado un trabajo que necesita ejecutarse diariamente, compruebe si ciertos registros cumplen criterios preestablecidos para su posterior procesamiento. Si lo hacen, inicie esos trabajos de procesamiento. Procesar registros podría significar extraer datos de múltiples sistemas externos para realizar cálculos. Los pasos en trabajos deben notificar a las personas a través de Slack que los registros procesados están listos para su revisión. Los pasos también deben distribuir notificaciones a gestores y superiores en la jerarquía de funciones basándose en un retraso configurable después de la primera ronda de notificaciones.

Este problema implica varios pasos diferentes, algunos de los cuales pueden ocurrir independientemente entre sí. Hay muchas formas de dividir el trabajo. Esta es una agrupación:

  • El programador.
  • La interfaz de pasos e implementaciones concretas que procesan registros (independientemente del tipo de procesamiento).
  • El procesador que organiza los pasos.
  • El Apex Invocable llamado por el programador.
  • El elemento de notificación. Utilizamos el SDK Apex Slack.
    • Existe cierta complejidad oculta en la frase “retraso configurable”. Revisaremos esta complejidad más adelante en este artículo.

Este es un diagrama de opinión para el marco de trabajo integrado:

Diagrama arquitectónico que muestra Flow Scheduler, procesamiento asíncrono Apex y diferentes implementaciones de pasos posibles Ahora, desglose ese diagrama y comience a construir las piezas.

Flujo programado ofrece varias ventajas como mecanismo de programación:

  • Los flujos programados se pueden empaquetar e implementar como metadatos. Esto no es cierto para trabajos programados mediante Apex (o a través de la página Trabajos programados).
  • El elemento Espera es crítico para marcos de trabajo que requieren llamadas. Al utilizarlo en Flujo, las llamadas no son necesarias en la parte Invocable del marco de trabajo.
  • La granularidad de programación cumple los requisitos: el intervalo mínimo para Flujos programados es diario. Si necesita una frecuencia más alta (por ejemplo, cada hora), reconsidere Flujo programado para este requisito.

Otra consideración al configurar el Flujo programado es la puerta de entorno. Antes de invocar la acción Apex, agregue un elemento Decisión que evalúe la variable {!$Api.Enterprise_Server_URL_100}. Esto garantiza que el trabajo se ejecute solo en los entornos previstos, como UAT y Producción. Este patrón es importante porque los entornos sandbox se actualizan con frecuencia o se crean recientemente durante el SDLC, y sin una comprobación de entorno explícita, un Flujo programado podría ejecutarse de forma no intencionada en entornos donde el marco no está destinado a ejecutarse. El uso del operador contains en el elemento Decisión hace que la configuración sea resistente a futuras creaciones de sandbox o cambios de URL.

Finalmente, considere cómo el marco de trabajo debe capturar fallos. Agregue siempre una ruta de fallo cuando Flow llame a cualquier acción; por ejemplo, puede conectar fallos a la acción "Agregar entrada de registro" de Nebula Logger. Nebula Logger escribe registros en objetos personalizados, de modo que los clientes deben tener en cuenta que los datos de registro consumen almacenamiento de la organización; de forma predeterminada, los registros se almacenan durante 14 días en una organización y luego se limpian; este periodo de retención es configurable. Nebula Logger también utiliza Eventos de plataforma para publicar registros, de modo que las entradas de registro se guardan independientemente de la transacción de procesamiento de datos principal, lo que garantiza la captura de fallos incluso si la acción principal Flujo o Apex se revierte. Los clientes deben evaluar el volumen de registro esperado y los requisitos de retención cuando consideren la incorporación de un marco de trabajo de registro.

Este es el aspecto del flujo:

El flujo programado que contiene un elemento Decisión para si ejecutarse o no, un elemento Pausa para permitir llamadas y la acción Apex Invocable llamada junto con la ruta de fallo del Registrador de nebulosas

Pasemos a los primeros fragmentos de código Apex con el requisito de programación ahora satisfecho.

Definir una interfaz de Step:

Para este artículo, la interfaz de step se muestra como una clase externa para mayor claridad. El marco de trabajo en sí es flexible: los equipos pueden organizar la interfaz y sus implementaciones utilizando cualquier patrón de empaquetado Apex que prefieran, siempre que todas las clases Paso hagan referencia a la misma interfaz.

Existen algunas cosas a tener en cuenta acerca de los métodos definidos dentro de nuestra interfaz:

  • La execute, aunque sin argumentos en este momento, mejora cuando pasamos una clase de State (o interfaz) para orquestar datos entre pasos cuando el orden importa.
  • Los getName podrían devolver un valor de System.Type en vez de un String. El objetivo es proporcionar a la capa de orquestación una forma de registrar nombres de pasos sin exponer otras propiedades.

Esta es la primera implementación concreta que muestra cómo se ajustan estas piezas. Con una excepción más adelante, recomendamos utilizar Apex en cola para implementar el procesamiento asíncrono en Apex; Apex por lotes es normalmente innecesario (y se desaconsejan los métodos de @future). Apex en cola se inicia rápidamente y, con Cursores Apex, tiene muchas ventajas sobre Apex por lotes.

Los cursores Apex ofrecen una alternativa moderna al modelo Apex tradicional por lotes. Al igual que el procesamiento por lotes, una implementación de Cursor puede obtener registros en fragmentos (hasta 2.000 por lote). Sin embargo, los Cursores permiten múltiples recuperaciones en una sola transacción, lo que permite un rendimiento significativamente superior para operaciones de gran volumen.

Al adoptar Cursores como parte de este marco de trabajo, los equipos deben tener en cuenta las limitaciones actuales de pruebas y capacidad de burla. El comportamiento del cursor en las pruebas puede diferir del comportamiento de producción, por lo que es importante diseñar estrategias de prueba que eviten depender de las internas del cursor y en su lugar validen la lógica de orquestación en los límites. A medida que evolucione la plataforma, estas áreas seguirán mejorando, pero la orientación principal sigue siendo: Los cursos proporcionan un mayor rendimiento y una reducción de los gastos generales de orquestación en comparación con Batch Apex para muchos casos de uso.

Para definir un límite claro entre el Cursor proporcionado por el sistema y su propio código, recomendamos crear una representación similar a Cursor al implementar la interfaz de Step. Considere este código:

Observe la clase de Cursor. Los cursores Apex son instancias de Database.Cursor, pero nuestra implementación de Cursor nos proporciona flexibilidad sobre las deficiencias de Cursores. Esta es la implementación:

Para el resto de este artículo, omitimos las declaraciones de sharing al hacer referencia a clases Apex. En la práctica, asegúrese de que las clases de nivel superior se utilizan explícitamente con o sin colaboración para ajustarse a su modelo de objeto y permisos.

Tenga en cuenta también que nuestra implementación de Cursor delega en el Database.Cursor de plataforma, con beneficios adicionales discutidos a continuación.

En primer lugar, estas son las pruebas correspondientes:

Al hacer que las Cursor sean virtuales, las implementaciones de CursorStep concretas pueden operar sin Database.Cursor cuando no necesitan iterar un conjunto de registros de gran tamaño, similar a devolver una System.Iterable<T> en vez de una Database.QueryLocator en Batch Apex. Este es un ejemplo:

Tenga en cuenta que como esta clase también es abstracta, deja la implementación concreta de innerExecute a subclases.

También hay una alternativa a la subclase interna CursorLike. Si sabe que las versiones concretas de un paso como este no superarán otros límites reguladores, puede devolver this.records desde CursorLike.fetch y sustituir el CursorStep.shouldRestart() principal para devolver falso. Eso le permite iterar sobre una lista limitada solo por el límite de acumulación de Apex de 12 MB por transacción asíncrona.

Nuestra implementación basada en cursor nos proporciona mucha flexibilidad al paginar grandes cantidades de datos. La interfaz de Step, por su parte, nos proporciona la flexibilidad de describir y encapsular pasos de todo tipo.

Considere un paso basado en flujo:

Como los flujos no pueden devolver parámetros de salida que se ajusten a un tipo definido por Apex, comprobamos un parámetro de salida de shouldRestart antes de utilizarlo.

Algunos pasos podrían estar marcados con funciones. Puede implementar lógica para decidir qué pasos incluir o utilizar un paso de no operación para una función desactivada. El patrón Objeto nulo es una forma común de reducir la complejidad dentro de la capa de orquestación:

Ahora tenemos bastantes elementos constructivos con los que trabajar. Echemos un vistazo a la capa de orquestación responsable de iterar sobre pasos.

El procesador es un punto de inflexión en la arquitectura. Debemos decidir quién define qué pasos se inicializan y dónde. Las opciones incluyen:

  • Haga que el procesador defina qué pasos asignar a la lógica comercial. Esta opción es sencilla, pero se amplía poco para facilitar la lectura.
  • Defina la asignación con Metadatos personalizados (CMDT). Los campos Relación de metadatos no admiten ApexClass, lo que acopla libremente la ortografía del nombre de clase en su configuración de proceso comercial. Puede reducir el riesgo de administrador convirtiendo el campo en una lista de selección y validando la existencia del tipo (Type.forName() o consultando ApexClass), pero como los registros CMDT no admiten desencadenadores, la validación se produce en tiempo de ejecución. Esta ruta es comprobable, pero los administradores aún pueden crear registros CMDT solo en producción: continúe con cuidado.
  • Defina la asignación con registros. Los no administradores pueden configurar pasos, pero las implementaciones se vuelven más difíciles y los entornos pueden desviarse. Proceda con precaución.

Existe una cita famosa de Clean Code sobre cómo gestionar esta complejidad en particular:

La solución a este problema es enterrar la declaración switch [para crear objetos] en el sótano de una fábrica abstracta, y nunca dejar que nadie la vea.

Con eso en mente, y debido a que nuestro número actual de pasos está bien definido y es poco probable que crezca demasiado, está bien que el procesador de pasos también sea la fábrica de pasos. Esto puede utilizar un número para dirigir la declaración de cambio:

Y luego para nuestro StepProcessor:

Los métodos de fábrica mostrados, como addTypeOneSteps(), pueden delegar preocupaciones como el marcado de funciones; cleanSteps() realiza una comprobación puntual en los pasos recopilados para asegurarse de que no hay ningún paso “vacío” antes de ser realmente asíncrono. Podría tener este aspecto:

No hemos debatido la gestión de errores desde que mencionamos el Registrador de nebulosas en la sección Flujo programado. Eso se debe a que System.Finalizer nos permite realizar un registro de cobertura general para todas las condiciones de error sin agregar una gestión de errores específica en cada paso. Cada Step se centra en la ejecución, mientras registramos y volvemos a lanzar cualquier ruta insatisfecha de modo que aflore en pruebas de unidad. Esto admite una iteración segura y alertas a nivel de producción (utilizando el complemento Slack Logger para Nebula para todos los registros WARN y ERROR).

Una nota acerca del registro de errores: pasar la instancia del paso a mensajes de registro asume un nivel de Trust en lo que se vuelve visible en los registros. El toString() predeterminado para clases Apex incluye todas las propiedades estáticas y a nivel de instancia en el mensaje. Eso puede ser deseable, o puede filtrar información confidencial. Aunque el registro y la seguridad no son el enfoque aquí, tenga en cuenta que para algunos sistemas, la adhesión a una interfaz como Step también puede implicar forzar una sustitución para toString().

Este método impone a cada creador de objetos la responsabilidad de decidir qué se puede imprimir, lo que puede ser deseable.

En los niveles de registro: en el nivel de StepProcessor, utilizamos INFO, el nivel de no error más alto. A medida que se vuelva más granular en la aplicación, los niveles de registro deben disminuir en consecuencia. Los pasos individuales podrían utilizar DEBUG para información de alto nivel, con FINE, FINER y FINEST reservados para resultados cada vez más detallados. El registro es tanto un arte como una ciencia, pero seguir estos principios ayuda a mantener los registros coherentes y útiles.

Antes de continuar, reflexionemos brevemente sobre la decisión de que nuestro procesador de pasos aloje la lógica para la que se utilizan los pasos. En una base de código grande, considere hacer que los StepProcessor sean virtuales o abstractos y haga que las subclases identifiquen pasos específicos para establecer una separación apropiada de las preocupaciones.

El programador eventualmente invoca Apex. Con el resto de la configuración completa, la sección Apex invocable puede decidir qué pasos se deben ejecutar y pasar el List<StepType> al procesador:

Esta es una parte sencilla de la ecuación: utilizar registros, datos o lógica para determinar qué tipos de pasos ejecutar. La Acción invocable es sencilla porque encapsulamos la complejidad en otros lugares. También nos hemos protegido contra excepciones inesperadas y hemos hecho que cada pieza sea fácil de probar de forma aislada.

El Apex Slack SDK está fuera del ámbito de este artículo, pero un posible inconveniente de los requisitos merece una revisión: notificar a las personas en la jerarquía de funciones basándose en un retraso configurable. Sobre el papel, esto es sencillo y podría (correctamente) considerar System.enqueueJob(this) en el StepProcessor. Con System.AsyncOptions, nuestra inclinación inicial era utilizar la sobrecarga de enqueueJob para satisfacer este requisito.

Por ahora, sin embargo, el retraso máximo a través de System.AsyncOptions.MinimumQueueableDelayInMinutes es de 10 minutos. Como el requisito es de 120 minutos, quedan algunas opciones. Un enfoque ingenuo podría tener este aspecto:

En la práctica, el retraso se pasaría a esta clase porque el retraso está dirigido por la configuración.

No recomendamos este enfoque a menos que esté seguro de que solo habrá un tipo de notificación demorada. Se quema en 11 trabajos asíncronos adicionales antes de comenzar (o más, si aumenta el retraso). Ese costo podría estar bien para un trabajo, no para muchos. También necesitaría agregar un método a la interfaz de Step de modo que cada paso pueda indicar al procesador cuánto tiempo esperar antes de reiniciar, lo que agrega ruido.

Eso nos deja con dos posibilidades interesantes:

  • Puede dividir el paso retrasado en su marco de trabajo existente si ya tiene un trabajo de sondeo programado en un intervalo apropiado. También debe estar de acuerdo con que el retraso especificado llegue hasta 15 minutos más tarde (15 minutos es el intervalo de actualización mínimo para una expresión CRON programada por Apex). Esto coincide aproximadamente con el ejemplo Apex invocable; la programación se realiza mediante Apex en su lugar. En otras palabras, puede reutilizar la misma arquitectura basada en Step para procesar registros basándose en una marca de tiempo “Comenzar después” y decidir qué pasos utilizar basándose en una lista de selección o asignación de lista de selección múltiple a los valores de número de StepType mostrados anteriormente.
  • De manera alternativa, si está cómodo definiendo una clase de Apex externa adicional, vuelva a Batch Apex (a diferencia de Apex en cola, que admite clases internas, las clases de Apex por lotes deben ser clases externas) utilizando System.scheduleBatch().

Considere el ejemplo de Apex por lotes. Aunque generalmente recomendamos Apex en cola para flexibilidad y control, este es un caso donde Apex por lotes aún reina:

Y luego, en la StepProcessor, imagine que el método de addTypeOneSteps() mostrado anteriormente se actualiza con este paso demorado:

Aunque normalmente no recomendamos tanto salto de aro, este retraso de paso se convierte en otro elemento constructivo reutilizable. Hasta que se permiten retrasos más largos en Apex en cola, también representa la forma más sencilla de producir este efecto (sin un mecanismo de sondeo, como se ha comentado).

Utilizamos diseño orientado a objetos para cumplir los requisitos y creamos un sistema que se ampliará mientras equilibra el coste a largo plazo de construcción y mantenimiento. Aunque la declaración de pasos y la creación de instancias pueden superar en última instancia su lugar en StepProcessor, hay poca deuda técnica adicional aquí. Con FlowStep, los administradores y desarrolladores pueden decidir juntos cuándo tienen más sentido las soluciones sin código o procódigo.

Utilizando la interfaz de System.Finalizer en el marco de trabajo Queueable de Apex, junto con Nebula Logger, hemos creado un sistema sólido y comprobable que nos alerta de fallos imprevistos incluso si pasos futuros carecen de un registro explícito. Para nosotros, este sistema está felizmente superando números y reduciendo costes y complejidad. También nos ha dado perspectivas valiosas sobre el comportamiento de los Cursores Apex bajo cargas de trabajo reales, ayudándonos a refinar nuestro enfoque mientras mejoramos la función en sí.

Al descomponer cargas de trabajo complejas y de gran volumen en pasos de ejecución modulares, el marco de trabajo Marco de procesamiento asíncrono basado en pasos transforma las restricciones de plataforma en ventajas de ingeniería, permitiendo el rendimiento, la observabilidad y la regulación predecibles a escala empresarial. Los pasos pueden configurarlos administradores y desarrolladores, y en cualquier caso, los autores de los pasos pueden centrarse de forma segura en observar los límites reguladores de plataforma básicos (como filas DML y filas de consulta recuperadas) sin tener que preocuparse de cómo ampliar cada paso.

Para poner en marcha y adoptar este patrón en implementaciones empresariales, los arquitectos deben:

  • Evalúe automatizaciones existentes para identificar áreas donde la orquestación asíncrona puede ayudar a mejorar el rendimiento y mejorar la observabilidad.
  • Divida procesos grandes en pasos discretos ejecutables de forma independiente con objetivos de procesamiento claros y puntos de autor discretos (como Flujo o Apex).
  • Defina y agrupe tipos de pasos para acelerar la reutilización y estandarización de pasos entre unidades comerciales.
  • Pilote el enfoque con nuevos procesos o automatizaciones existentes. ¡Te sorprenderá descubrir cuántos casos periféricos encuentras de forma gratuita en cuestión de pasos, cuidando tu registro integrado y observabilidad!

James Simone es ingeniero de software principal en Salesforce y tiene más de una década de experiencia trabajando en la plataforma. Fue cliente de Salesforce (y propietario de productos) antes de pasar al desarrollo, y ha estado escribiendo inmersiones técnicas profundas sobre Salesforce desde 2019 dentro de The Joys Of Apex. Anteriormente ha publicado artículos en el blog Desarrollador de Salesforce y también en el blog Ingeniería de Salesforce.