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 A → Method A, Method B, Method C
- Class B → Method B, Method B
- Class C → Method 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
- Go to Setup → Custom Metadata Types
- Click New Custom Metadata Type
- Label: Framework Config
- Object Name: Framework_Config
- Visibility: Public
- 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