Reusable JSON-Driven Apex Execution Framework in Salesforce

Reusable JSON-Driven Apex Execution Framework in Salesforce  

The purpose of this guide is to create a reusable and configurable JSON-based Apex execution engine that: 

  • 📦 Stores business logic in JSON format inside Salesforce 
  • 🔁 Executes Apex methods dynamically based on metadata 
  • 🚀 Supports reuse across multiple orgs via packaging 

Step-by-Step Implementation 

Step 1: Structure Your Apex Classes 

We assume multiple service classes and methods need to be called in a dynamic flow. For example: 

  • Class AMethod A, Method B, Method C 
  • Class BMethod B, Method B 
  • Class CMethod C 

Use this pattern: 

  • Serialize results in each method: 

String jsonString = JSON.serialize(con); 

return jsonString; 

  • Use deserialization in the next step’s input-consuming methods. 

 

Step 2: Create the Dynamic Interface 

1. Define the Interface 

public interface StepInterface { 

    Object invokeDynamicMethod(String methodName, Map<String, Object> params); 

} 

2. Implement in a Service Class 

public class VortexifyServiceClass implements StepInterface { 

    public Object invokeDynamicMethod(String methodName, Map<String, Object> inputs) { 

        Map<String, Object> response = new Map<String, Object>(); 

 

        if (methodName == ‘insertContactIfBanking’) { 

            response = VortexifyServiceClassHelper.insertContactIfBanking( 

                (String)inputs.get(‘accountId’), 

                (String)inputs.get(‘firstName’), 

                (String)inputs.get(‘lastName’), 

                (String)inputs.get(’email’) 

            ); 

 

        } else if (methodName == ‘updateContactEmail’) { 

            response = VortexifyServiceClassHelper.updateContactEmail( 

                (String)inputs.get(‘accountId’), 

                (String)inputs.get(‘firstName’), 

                (String)inputs.get(‘lastName’), 

                (String)inputs.get(’email’), 

                (String)inputs.get(‘newEmail’) 

            ); 

 

        } else if (methodName == ‘updateAccountPhoneIfHot’) { 

            response = VortexifyServiceClassHelper.updateAccountPhoneIfHot( 

                (String)inputs.get(‘accountId’), 

                (String)inputs.get(‘phone’) 

            ); 

        } 

 

        return response; 

    } 

} 

 

Step 3: Create the JSONFlowExecutor Class 

This is the core engine that dynamically reads JSON config and executes steps. 

public class JSONFlowExecutor { 

 

    @TestVisible private static String TEST_JSON_OVERRIDE; 

 

    public static void runFlow(String metadataName) { 

        if (String.isNotBlank(TEST_JSON_OVERRIDE)) { 

            runFlowFromJson(TEST_JSON_OVERRIDE); 

            return; 

        } 

 

        Framework_Config__mdt meta = [ 

            SELECT Config_Name__c, Config_JSON__c 

            FROM Framework_Config__mdt 

            WHERE DeveloperName = :metadataName 

            LIMIT 1 

        ]; 

 

        runFlowFromJson(meta.Config_JSON__c); 

    } 

 

    public static void runFlowFromJson(String configJson) { 

        Map<String, Object> rootMap = (Map<String, Object>) JSON.deserializeUntyped(configJson); 

        Map<String, Object> globalInputs = (Map<String, Object>) rootMap.get(‘globalInputs’); 

        List<Object> sequenceList = (List<Object>) rootMap.get(‘sequence’); 

 

        Map<String, Object> stepResults = new Map<String, Object>(); 

 

        for (Object stepKey : sequenceList) { 

            String stepName = (String) stepKey; 

            Map<String, Object> stepDefinition = (Map<String, Object>) rootMap.get(stepName); 

 

            String className = (String) stepDefinition.get(‘className’); 

            String action = (String) stepDefinition.get(‘action’); 

            List<Object> inputPath = (List<Object>) stepDefinition.get(‘inputpath’); 

 

            Map<String, Object> inputs = new Map<String, Object>(); 

            for (Object inputObj : inputPath) { 

                Map<String, Object> input = (Map<String, Object>) inputObj; 

                String key = (String) input.get(‘key’); 

 

                if (input.containsKey(‘path’)) { 

                    inputs.put(key, resolvePath((String) input.get(‘path’), rootMap, stepResults, globalInputs)); 

                } else if (input.containsKey(‘valueFrom’)) { 

                    inputs.put(key, stepResults.get((String) input.get(‘valueFrom’))); 

                } else { 

                    inputs.put(key, input.get(‘value’)); 

                } 

            } 

 

            Type t = Type.forName(className); 

            if (t == null) throw new FlowExecutionException(‘Invalid class: ‘ + className); 

 

            Object instance = t.newInstance(); 

            if (!(instance instanceof StepInterface)) { 

                throw new FlowExecutionException(className + ‘ must implement StepInterface’); 

            } 

 

            Object result = ((StepInterface) instance).invokeDynamicMethod(action, inputs); 

            stepResults.put(stepName, result); 

        } 

    } 

 

    public static Object resolvePath(String path, Map<String, Object> rootMap, Map<String, Object> stepResults, Map<String, Object> globalInputs) { 

        if (String.isBlank(path)) return null; 

 

        List<String> segments = path.split(‘\\.’); 

        Object current; 

 

        if (segments[0] == ‘globalInputs’) { 

            current = globalInputs; 

            segments.remove(0); 

        } else if (stepResults.containsKey(segments[0])) { 

            current = stepResults.get(segments[0]); 

            segments.remove(0); 

        } else if (segments[0] == ‘_request’ && rootMap.containsKey(‘_request’)) { 

            current = rootMap.get(‘_request’); 

            segments.remove(0); 

        } else { 

            return null; 

        } 

 

        for (String segment : segments) { 

            if (current instanceof Map<String, Object>) { 

                current = ((Map<String, Object>) current).get(segment); 

            } else { 

                return null; 

            } 

        } 

 

        return current; 

    } 

 

    public class FlowExecutionException extends Exception {} 

} 

 

Step 4: Create Custom Metadata Type 

Metadata Name: Framework_Config__mdt 

  1. Go to Setup → Custom Metadata Types 
  1. Click New Custom Metadata Type 
  • Label: Framework Config 
  • Object Name: Framework_Config 
  • Visibility: Public 
  1. Click Save 

Add Fields: 

Field Label  Field Name  Type  Required 
Config Name  Config_Name  Text  ✅ Yes 
Config JSON  Config_JSON  Long Text Area  ✅ Yes 

 

Sample JSON Config 

Below is a sample dynamic JSON stored in the custom metadata: 

json 

CopyEdit 

{ 

  “configList”: [“WOD_WR_AP_QueryInventoryWithWR”], 

  “sequence”: [“insertContactIfBanking”, “updateAccountPhoneIfHot”], 

  “insertContactIfBanking”: { 

    “className”: “VortexifyServiceClass”, 

    “action”: “insertContactIfBanking”, 

    “inputpath”: [ 

      {“key”: “accountId”, “value”: “001gL000000mCrhQAE”}, 

      {“key”: “firstName”, “value”: “kajal”}, 

      {“key”: “lastName”, “value”: “yadav”}, 

      {“key”: “email”, “value”: “kajal.yadav@vortexifysync.com”} 

    ] 

  }, 

  “updateAccountPhoneIfHot”: { 

    “className”: “VortexifyServiceClass”, 

    “action”: “updateAccountPhoneIfHot”, 

    “inputpath”: [ 

      {“key”: “accountId”, “path”: “insertContactIfBanking.contactId”}, 

      {“key”: “phone”, “value”: “9810091942”} 

    ] 

  } 

} 

 

Step 5: Execute in Anonymous Apex 

apex 

CopyEdit 

JSONFlowExecutor.runFlow(‘DynamicConfig’); 

 

Benefits of This Architecture 

  • No code change needed to update flow logic
  • Reusable across any org
  • Supports dynamic method chaining
  • Data-driven, metadata-configurable
  • Easy to extend and unit test 

 

Leave a Comment

Your email address will not be published. Required fields are marked *