6

I have a requirement in which, for each time an account is inserted, I have to make a callout to an external system sending some data of the record and then the API returns a unique Id that I have to assign to a field in Account, and then update it. (basically the account is saved in the external system, syncing the accounts in the org and in the external system)

So the flow would be: Account inserted => trigger after insert => callout => update on account.

This callout also has to be made if certain fields are updated on account, so the trigger would also be defined for after update, and if the criteria is matched, the callout is also made. This flow would be Account updated => if criteria matched => callout

I have to design the code so as to handle many records inserted at once. I know triggers require callouts to be made asynchronously. This is where I'm blocked as to what approach is best.

The limitations I have to consider is the max 100 callouts per transaction and the fact I cannot call future methods from a batch class so that won't be possible.

I already have a class with methods that receive an account as parameter and makes the callout:

public static Account altaCF(Account cuenta){
    String body = JSON.serialize(new Consumidor(cuenta));
    TopGroup_Integration__c cred = TopGroup_Integration__c.getOrgDefaults();
    HttpResponse response = doRequest(body, cred.Endpoint_CF__c, cred.Api_Username__c, cred.Api_Token__c);
    handleResponse(response, cuenta);
    return cuenta;
}

doRequest method:

public static HttpResponse doRequest(String requestBody, String endpoint, String username, String token) {
    HttpRequest request = new HttpRequest();

    request.setEndpoint(endpoint);
    request.setMethod('POST');
    request.setHeader('Content-Type', 'application/json');
    request.setHeader('authorization-type', 'Basic');
    request.setHeader('api-username', API_USERNAME);
    request.setHeader('api-token', API_TOKEN);
    request.setBody(requestBody);

    HttpResponse response = new Http().send(request);
    return response;
}

This is the handleResponse method:

public static void handleResponse(HttpResponse res, Account cuenta){
    if(res.getStatusCode() == 200){
        SuccessResponse responseBody = (SuccessResponse) JSON.deserialize(JSON.serialize(res.getBody()), SuccessResponse.class);
        cuenta.ID_Top_Group__c = responseBody.data.numeroCliente;
        cuenta.TG_Service_Status__c = responseBody.statusCode;
        cuenta.TG_Service_Message__c = responseBody.data.message;
    }else{
        FailResponse responseBody = (FailResponse) JSON.deserialize(JSON.serialize(res.getBody()), FailResponse.class);
        cuenta.TG_Service_Status__c = responseBody.status;
        cuenta.TG_Service_Message__c = generateServiceErrorMessage(responseBody);
    }
}

My idea was to call this the altaCF method from my Trigger Handler class, for each record inserted.

Now what I have to figure out is: 1)the best approach to make the logic asynchronous (don't know if a future method or batch or queuable is best. 2)how to handle large number of records inserted at once.

I know triggers execute in batches of 200, and since the limit of callouts per transaction is 100 I have to consider that. The ideal scenario would be to handle this in a batch class but I don't know if that is enough to meet the "asynchronous callout" requirement of triggers.

Any help would be appreciated!

6
  • You're not going to delete this question, are you? Remember that questions (and answers) are as much for you as they are for other people with similar issues coming here from, say, a google search.
    – Derek F
    Commented Oct 4, 2023 at 16:19
  • architect.salesforce.com/decision-guides/trigger-automation talks about this. Other decision guides on architect.salesforce.com are also a good read.
    – identigral
    Commented Oct 4, 2023 at 16:29
  • My point is that I've made a mental note that you tend to delete questions quite frequently, including ones where other people (myself included) have made contributions (comments and/or answers).
    – Derek F
    Commented Oct 4, 2023 at 16:37
  • @DerekF I only delete questions if I seriously believe the way it is written is not helpful for myself or others. I'm trully sorry if you have been affected by that. Given that you have taken mental note of my questions and how I post, feel free to not contribute if you believe you are wasting your time. Commented Oct 4, 2023 at 16:43
  • 1
    In addition to Phil's excellent answer and related links; if you have Dan Appleman's book "Advanced Apex" he addresses this issue with a scalable pattern in the chapter "Going Async"
    – cropredy
    Commented Oct 4, 2023 at 17:06

2 Answers 2

10

I wrote an article about this sort of requirement for ApexHours, which you'll find here.

The key points are:

  • Futures are a real problem to be called from a trigger because you cannot invoke/enqueue a future if the trigger is called from an async context and triggers are called with chunks of at most 200 records even in sync processing, so this isn't scalable.
  • Batches are a problem from a trigger because you can only queue 105 batches at any given time on an org and triggers are called with chunks, so this isn't scalable.
  • Queueables are generally a problem from a trigger because you can only enqueue 1 queueable if the trigger is called from an async context and triggers are called with chunks of at most 200 records, so this isn't scalable.
    • Note: not covered in the article, because this is totally new, but you might be able to engineer a solution on queueable if you use the new queueable "deduplication" mechanism, so the system will try to minimize duplications. The queueable needs to be self-chaining when it knows there are callouts still to be performed (you must track this state in the database somehow). However, unfortunately, this doesn't guarantee single threading; I got this from the PM at Salesforce:

Note that the deduplication signature doesn't guarantee exactly once semantics (i.e. a uniqueness constraint). It vastly reduces the possibility of duplicate messages occurring, but should be considered accurately as have at-least-once semantics.

I provided a github repo that demonstrates how to capture the need for a callout in a "command" object, and how these can be processed asynchronously (via a combination of Platform Events and Queueables). The queueable is still required at the moment because Platform Event trigger subscribers cannot yet make callouts, despite being effectively async. Please upvote this idea to simplify how this can work.

1
  • Thank you for this! It seems that this requirement is quite common and there are many limitations for it. I will check out your article and decide on an approach with the tools at my disposal. Commented Oct 4, 2023 at 16:45
1

Use a trigger on Account to publish a Platform Event, e.g. Account_Update__e.

Use an after insert trigger on Account_Update__e to call a @future(callout=true) method to call your API.

If possible, consider updating your API endpoint to handle many Accounts in a single call to avoid the daily future invocation limit. If necessary, create a PlatformEventSubscriberConfig for your platform event trigger and adjust the batchSize to control the number of Account updates are processed in a batch.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .