Read more at jamessimone.net, or return to the homepage





Future Methods, Callouts & Callbacks

Today we're going to talk about "future" methods - Salesforce's way of handling asynchronous code. For many, your introduction to future methods happens the first time that you need to integrate with an external API. Salesforce prevents you from performing any kind of synchronous API calls following DML operations; this is the "invisible hand" of SFDC guiding you to do things the way that they want you to — in other words, if you have begin your main thread operation with some kind of inserting / updating / upserting / deleting of SObject records, the code performing the API call needs to be pushed to another thread.

For many, future methods represent a distinct challenge when it comes to writing clean Apex code. They only accept primitive types, or collections; this means that most future methods end up taking in an Id or a List<Id> to then perform their work. That leads to tight coupling between the way API calls are being made, and the work that needs to be done once the call has finished. How can we batten down the hatches, write clean code, and still do work asynchronously?

linkSeparating the Concerns in Async Apex Methods

At its heart, async work with API callouts can be broken down into three pieces:

Looking at the list like this, it's almost like there are 3 classes waiting to be born. The truly tightly coupled concepts, written another way:

This is the kind of test I'd like to write, showcasing the first two objects:

classes/HttpCallout_Tests.cls
1link@isTest

2linkprivate class HttpCallout_Tests {

3link @isTest

4link static void it_should_callout_successfully() {

5link Callout fakeCallout = new Callout('{parameter1: perhaps a serialized list or id!}',

6link new Url('https://api.com'), RestMethod.POST);

7link String jsonString = JSON.serialize(fakeCallout);

8link

9link Test.startTest();

10link HttpCallout.process(jsonString);

11link Test.stopTest();

12link

13link System.assertEquals(1, Limits.getCallouts());

14link }

15link}

linkSetting up Callouts and HTTP Class Wrappers

The most basic implementations:

1linkpublic class Callout {

2link private static final Integer DEFAULT_TIMEOUT = 10000;

3link

4link public Callout(String jsonString,

5link Url endpoint,

6link RestMethod method,

7link Integer millisecondTimeout) {

8link this.BodyString = jsonString;

9link this.Endpoint = endpoint.toExternalForm();

10link this.RestMethod = method.name();

11link this.Timeout = millisecondTimeout;

12link }

13link

14link public Callout(String jsonString, Url endpoint, RestMethod method) {

15link this(jsonString, endpoint, method, DEFAULT_TIMEOUT);

16link }

17link

18link //sometimes an api key is supplied as part of the URL ...

19link //because it's not always necessary, we make it a public member of the class

20link public String ApiKey { get; set; }

21link

22link public String BodyString { get; private set; }

23link public String Endpoint { get; private set; }

24link public String RestMethod { get; private set; }

25link public Integer Timeout { get; private set; }

26link}

27link

28linkpublic class HttpCallout {

29link @future(callout = true)

30link public static void process(String calloutString) {

31link Callout calloutObj = (Callout)JSON.deserialize(calloutString, Callout.class);

32link HttpRequest req = setupHttpRequest(calloutObj);

33link //this is the part you would try/catch

34link //in a more robuse implementation ...

35link new Http().send(req);

36link }

37link

38link private static HttpRequest setupHttpRequest(Callout callout) {

39link HttpRequest req = new HttpRequest();

40link req.setEndpoint(callout.Endpoint);

41link req.setMethod(callout.RestMethod);

42link req.setTimeout(callout.Timeout);

43link req.setHeader('Content-Type', 'application/json');

44link req.setBody(callout.BodyString);

45link if(String.isNotBlank(callout.ApiKey)) {

46link req.setHeader('x-api-key', callout.ApiKey);

47link }

48link return req;

49link }

50link}

So what do we achieve from this structure? We've successfully encapsulated the aspects surrounding an HTTP request. By itself, this isn't necessarily very impressive; in fact, the test doesn't even pass due to one of my favorite possible Apex exceptions:

System.TypeException: Methods defined as TestMethod do not support Web service callouts.

Sigh. Luckily you only need one boilerplate implementation of Salesforce's included HttpCalloutMock interface in order to proceed. Unfortunately, it's only within that HttpCalloutMock class that you can assert against Salesforce's Limits.getCallouts() method to verify that a callout is indeed happening:

1link//slightly re-working HttpCallout ...

2link@future(callout = true)

3linkpublic static void process(String calloutString) {

4link Callout calloutObj = (Callout)JSON.deserialize(calloutString, Callout.class);

5link HttpRequest req = setupHttpRequest(calloutObj);

6link makeRequest(req);

7link}

8link

9link@testVisible

10linkprivate static HttpResponse makeRequest(HttpRequest req) {

11link return new Http().send(req);

12link}

13link

14link//in HttpCallout_Tests ...

15link@isTest

16linkstatic void it_should_properly_stub_response() {

17link Test.setMock(HttpCalloutMock.class, new MockHttpResponse(200, 'Success', '{}'));

18link HttpResponse res = HttpCallout.makeRequest(new HttpRequest());

19link System.assertEquals(200, res.getStatusCode());

20link}

21link

22link@isTest

23linkstatic void it_should_callout_successfully() {

24link Callout fakeCallout = new Callout('{parameter1: perhaps a serialized list or id!}',

25link new Url('https://api.com'), RestMethod.POST);

26link String jsonString = JSON.serialize(fakeCallout);

27link

28link Test.setMock(HttpCalloutMock.class, new MockHttpResponse(200, 'Success', '{}'));

29link

30link Test.startTest();

31link HttpCallout.process(jsonString);

32link Test.stopTest();

33link

34link System.assert(true, 'should make it here!');

35link}

36link

37linkpublic class MockHttpResponse implements HttpCalloutMock {

38link private final Integer code;

39link private final String status;

40link private final String body;

41link

42link public MockHttpResponse(Integer code, String status, String body) {

43link this.code = code;

44link this.status = status;

45link this.body = body;

46link }

47link

48link public HTTPResponse respond(HTTPRequest req) {

49link System.assertEquals(1, Limits.getCallouts());

50link HttpResponse res = new HttpResponse();

51link

52link if(this.body != null) {

53link res.setBody(this.body);

54link }

55link res.setStatusCode(this.code);

56link res.setStatus(this.status);

57link

58link return res;

59link }

60link}

Encapsulating the details behind an HTTP request inside of the Callout object isn't, by itself, that impressive. You're saving on lines of code in setting up your HttpRequest objects that would otherwise have to be performed each time you wanted to make a callout. You're also reducing mental overhead by properly separating concerns; the HttpCallout object shown is very simple, but when you want to, say, add a try/catch block to your wrapper, you now know that there's only one single place in the system where it's necessary to try/catch for HttpRequests.

The real potential behind encapsulating your request information into the Callout object is what comes next — typically, the purpose of a callout is not only to send information to other systems. Sometimes that's the case, and those cases are lucky — no further processing is required! But most of the time, you're going to be getting information back from having made a callout in order to perform additional processing. This can typically lead to a lot of if/then statements, or perhaps a big switch statement following the HTTP section in your class.

In order to prevent our HttpCallout from growing in size beyond its concerns as the means of making HTTP requests, let's borrow some terminology from languages where methods, or functions, themselves are first-class citizens. There is no System.Type for the methods within your classes themselves; no way to represent them for internal or external purposes. You can't pass a function to your Callout object ... but you can pass a class!

linkIntroducing the Callback Object

When making use of future methods, it used to be that options were a lot more limited in terms of what kind of processesing could occur. Because operating in read/write mode was limited to async methods, and because one future method couldn't call another future method, there wasn't much you could do (beyond helper classes designed to setup HttpRequests) to safely encapsulate HTTP-related logic. Furthermore, future methods must return void, effectively limiting that separation of concerns even more.

Thankfully, a few years ago, Salesforce introduced the System.Queuable interface — a means of performing async work that could be called from within a future method. Defining a simple callback class becomes drop-dead simple, as a result:

1linkpublic abstract class Callback implements System.Queueable {

2link private HttpResponse res;

3link

4link public abstract void execute();

5link

6link public void callback() {

7link System.enqueueJob(this);

8link }

9link

10link public void callback(HttpResponse res) {

11link this.res = res;

12link this.callback();

13link }

14link}

15link

16link//and the basic test:

17link@isTest

18linkprivate class Callback_Tests {

19link @isTest

20link static void it_should_callback() {

21link CallbackMock mock = new CallbackMock(null);

22link

23link //have to wrap in start/stop test again to force async execution

24link Test.startTest();

25link mock.callback();

26link Test.stopTest();

27link

28link System.assertEquals(true, WasCalled);

29link }

30link

31link static boolean WasCalled = false;

32link private virtual class CallbackMock extends Callback {

33link

34link public override void execute(System.QueueableContext context) {

35link WasCalled = true;

36link }

37link }

38link}

Right. We're nearly there. Let's define the Callback member on our Callout object:

classes/Callout.cls
1linkpublic class Callout {

2link private static final Integer DEFAULT_TIMEOUT = 10000;

3link

4link public Callout(String jsonString,

5link Url endpoint,

6link RestMethod method,

7link Integer millisecondTimeout,

8link Callback callback) {

9link this.BodyString = jsonString;

10link this.Callback = callback;

11link this.Endpoint = endpoint.toExternalForm();

12link this.RestMethod = method.name();

13link this.Timeout = millisecondTimeout;

14link }

15link

16link public Callout(String jsonString, Url endpoint, RestMethod method, Callback callback) {

17link this(jsonString, endpoint, method, DEFAULT_TIMEOUT, callback);

18link }

19link //...

20link public Callback Callback { get; private set; }

21link}

And then in our wrapper object:

classes/HttpCallout.cls
1linkpublic class HttpCallout {

2link @future(callout = true)

3link public static void process(String calloutString) {

4link Callout calloutObj = (Callout)JSON.deserialize(calloutString, Callout.class);

5link HttpRequest req = setupHttpRequest(calloutObj);

6link HttpResponse res = makeRequest(req);

7link calloutObj.Callback.callback(res);

8link }

9link//...

10link}

To make things bullet-proof safe, we can even utilize the polymorphic empty object pattern for the Callback object once it's been received by the Callout:

classes/Callout.cls
1linkpublic Callout(String jsonString,

2link Url endpoint,

3link RestMethod method,

4link Integer millisecondTimeout,

5link Callback callback) {

6link this.BodyString = jsonString;

7link this.Callback = callback != null ? callback : new EmptyCallback();

8link this.Endpoint = endpoint.toExternalForm();

9link this.RestMethod = method.name();

10link this.Timeout = millisecondTimeout;

11link}

12link

13linkpublic Callout (String jsonString, Url endpoint, RestMethod method) {

14link this(jsonString, endpoint, method, null);

15link}

16link//...

17linkprivate virtual class EmptyCallback extends Callback {

18link public override void execute(System.QueueableContext context) {}

19link}

Let's verify that a non-null callback can receive the HttpResponse and do something with it (the first test we wrote in HttpCallout_Tests covers the null callback case):

1link//in HttpCallout_Tests ...

2link@isTest

3linkstatic void it_should_callout_and_callback() {

4link HttpCallback mockCallback = new HttpCallback();

5link Callout fakeCallout = new Callout(

6link '{parameter1: perhaps a serialized list or id!}',

7link new Url('https://api.com'),

8link RestMethod.POST,

9link mockCallback

10link );

11link String jsonString = Json.serialize(fakeCallout);

12link

13link Id fakeAccountId = TestingUtils.generateId(Account.SObjectType);

14link Test.setMock(HttpCalloutMock.class,

15link new MockHttpResponse(200, 'Success', '{ \""AccountId\"" : "'+ fakeAccountId +'" } ')

16link );

17link

18link Test.startTest();

19link HttpCallout.process(jsonString);

20link Test.stopTest();

21link

22link System.assertEquals(fakeAccountId, mockCallback.acc.Id);

23link}

24link//...

25linkprivate class HttpCallback extends Callback {

26link public Account acc { get; private set; }

27link

28link public override void execute(System.QueueableContext context) {

29link MockApiResponse mockRes =

30link Json.deserialize(this.res.getBody(), MockApiResponse.class);

31link this.acc = new Account(Id = mockRes.AccountId);

32link //do other work and perform DML ...

33link }

34link}

35link

36linkprivate class MockApiResponse {

37link public Id AccountId { get; set; }

38link}

Here I ran into my first snafu, as the test returns System.SerializationException: Not Serializable: System.HttpResponse. Circular references in JSON are fun! OK, let's try something a little simpler:

1linkpublic abstract class Callback implements System.Queueable {

2link protected string responseBody;

3link //...

4link public void callback(HttpResponse res) {

5link this.responseBody = res.getBody();

6link this.callback();

7link }

8link}

9link

10link//and in HttpCallout_Tests's HttpCallback object:

11linkpublic override void execute(System.QueueableContext context) {

12link MockApiResponse mockRes = (MockApiResponse)

13link Json.deserialize(this.responseBody, MockApiResponse.class);

14link this.acc = new Account(Id = mockRes.AccountId);

15link}

This is where things started to get really weird. The test was still failing; the Account and its Id didn't seem to be getting set. I backed off from what I was doing and asserted that there had been a Queueable job added to the queue. That failed. Peering at the log, though, things seemed to differ:

How is Limits.getQueueableJobs() failing?

I did a little reading on the subject and didn't come up with anything. I started debugging, and quickly ran into an issue that superceded what I had been looking into; namely, that the HttpCallback mock that I was initializing in my test wasn't getting called at all when the test was being run. At this point, my writing was interrupted by a quick flight, and during my time in the air I finally realized the obvious (which may have already occurred to you) — namely that the serialization process for the Callback was losing the crucial pointer to the actual instance of the HttpCallback. When de-serialized, the only encoding that remained was for the dumbed-down abstract version of the Callback class. Bummer. There was no way to pass a specific instance for the Callback to the Callout object, after all. Or was there?

Undaunted, and with plenty of wifi-free time on my hands, I thought about my options. The entire point in writing this article was to explore how developers could move towards a more polymorphic approach to HTTP requests and their subsequent follow-up work. If the developer and team was confident that the Callout and HttpCallout objects were doing their part, testing could occur in isolation for concrete Callback implementations; reactions to different kinds of HTTP requests could be entirely decoupled from the fetching process. Not getting the callback idea to work would have been a big loss.

linkCallbacks, Redux

It was time to re-engineer the Callback object. It still needed to abstract out the Queueable Apex implementation, but it also needed to play nice with serialization, and couldn't remain on the Callout object as a result. I went back to the drawing board:

1linkpublic virtual class Callback implements System.Queueable {

2link private Type callbackType;

3link protected string responseBody;

4link

5link protected Callback() {}

6link

7link public Callback(Type callbackType) {

8link this.callbackType = callbackType;

9link }

10link

11link public void callback() {

12link System.enqueueJob(this);

13link }

14link

15link public void callback(HttpResponse res) {

16link this.responseBody = res.getBody();

17link this.callback();

18link }

19link

20link public virtual void execute(System.QueueableContext context) {

21link if(this.callbackType == null) {

22link this.callbackType = EmptyCallback.class;

23link }

24link ((Callback) this.callbackType.newInstance())

25link .load(responseBody)

26link .execute(context);

27link }

28link

29link protected Callback load(String responseBody) {

30link this.responseBody = responseBody;

31link return this;

32link }

33link

34link private class EmptyCallback extends Callback {

35link public override void execute(System.QueueableContext context) {

36link //do something like debug here

37link //or just do nothing, like the name suggests!

38link }

39link }

40link}

41link

42link//and in the test ...

43link@isTest

44linkprivate class Callback_Tests {

45link @isTest

46link static void it_should_callback() {

47link Callback mock = new Callback(CallbackMock.class);

48link

49link Test.startTest();

50link mock.callback();

51link Test.stopTest();

52link

53link System.assertEquals(true, MockWasCalled);

54link }

55link

56link static boolean MockWasCalled = false;

57link public virtual class CallbackMock extends Callback {

58link public override void execute(System.QueueableContext context) {

59link MockWasCalled = true;

60link }

61link }

62link}

Now the empty callback itself is encapsulated within the Callback object — which actually works much more nicely than the Callout being concerned with whether or not to add an empty callback. The addition of the load method is how we'll pass the results of the HttpRequests into callbacks which are concerned with responding to them; this is to get around the fact that the newInstance() Type method accepts no other arguments. While I haven't shown the code for it here, the astute reader might also note that zero-argument constructors play very nicely with the Factory Pattern, due to the fact that the Factory can be initialized within any object's constructor and still be over-ridden in tests.

I'll also just explicitly mention that the callback() method without arguments is meant for further re-use within your codebase. The Queuable interface is ideal for use cases where you don't want to perform Batch Apex (which, while powerful, is slow — and requires way more boilerplate) but do need to push things async due to performing DML, and you'd like easy access to recursion. The CallbackMock just shown gives you an idea of how little code you need to write in order to start moving — all you need to do if you're not concerned with HTTP is extend the Callback class and override the execute method. If you need recursion, you can just call callback() at the end of your execute method to get things re-queued up.

Of course, our HTTP consumers still need some code tweaks:

classes/Callout.cls
1linkpublic Callout(

2link String jsonString,

3link Url endpoint, RestMethod method,

4link Integer millisecondTimeout,

5link Type callbackType) {

6link this.BodyString = jsonString;

7link //Type.forName throws for nulls, alas

8link this.CallbackName = callbackType == null ? '' : callbackType.getName();

9link this.Endpoint = endpoint.toExternalForm();

10link this.RestMethod = method.name();

11link this.Timeout = millisecondTimeout;

12link}

13link

14linkpublic Callout(String jsonString, Url endpoint, RestMethod method, Type callbackType) {

15link this(jsonString, endpoint, method, DEFAULT_TIMEOUT, callbackType);

16link}

17link

18linkpublic Callout (String jsonString, Url endpoint, RestMethod method) {

19link this(jsonString, endpoint, method, null);

20link}

21link//... and the member within Callout is also defined:

22linkpublic String CallbackName { get; private set; }

Which makes the HttpCallout class look like ...

1linkpublic class HttpCallout {

2link @future(callout = true)

3link public static void process(String calloutString) {

4link Callout calloutObj = (Callout)JSON.deserialize(calloutString, Callout.class);

5link HttpRequest req = setupHttpRequest(calloutObj);

6link HttpResponse res = makeRequest(req);

7link Type callbackType = Type.forName(calloutObj.CallbackName);

8link new Callback(callbackType).callback(res);

9link }

10link//...

11link}

12link

13link//and in HttpCallout_Tests:

14link@isTest

15linkstatic void it_should_callout_and_callback() {

16link Type callbackType = HttpCallbackMock.class;

17link Callout fakeCallout = new Callout(

18link '{parameter1: perhaps a serialized list or id!}',

19link new Url('https://api.com'),

20link RestMethod.POST,

21link callbackType);

22link String jsonString = Json.serialize(fakeCallout);

23link

24link Id accountId = TestingUtils.generateId(Account.SObjectType);

25link Test.setMock(

26link HttpCalloutMock.class,

27link new MockHttpResponse(200, 'Success', '{ "AccountId" : "' + accountId + '"}')

28link );

29link

30link Test.startTest();

31link HttpCallout.process(jsonString);

32link Test.stopTest();

33link

34link System.assertEquals(accountId, mockId);

35link}

36link

37linkprivate static Id mockId;

38linkprivate class HttpCallbackMock extends Callback {

39link public override void execute(System.QueueableContext context) {

40link FakeApiResponse fakeResponse =

41link (FakeApiResponse)Json.deserialize(this.responseBody, FakeApiResponse.class);

42link mockId = fakeResponse.AccountId;

43link //do other work and perform DML ...

44link }

45link}

46link

47linkprivate class FakeApiResponse {

48link Id AccountId { get; set; }

49link}

If you've made it this far, none of this should come as a surprise. One thing that did come as a surprise to me — though it makes sense thinking about it now — is that the test classes extending Callback had to be public in order for them to be properly constructed by the newInstance Type method.


linkClosing Thoughts On Future Methods, Callouts & Callbacks

I have been wanting to incorporate this design into some of my orgs for a while, and while there were some hiccups along the way, I consider them valuable learning experiences.

The ability to decouple our HTTP-related code from how consumers needing to make API calls choose to respond to requests is extremely valuable; it lets us test our production-level code without having to keep re-testing the underlying basics (making the request, try/catching the response, etc ...). And while a future method can't call a future method, Queueable Apex can nearly endlessly be chained together (so long as your maximum enqueued jobs don't exceed 50, the Salesforce limit for Queueable Apex). This means your Callback objects can be encapsulating further callbacks in turn within their own execute methods ... a powerful combo, particularly if you need to interact with more than one API at a given time (you'll need to also implement Database.AllowsCallouts on the Callback object if you need to perform further API calls).

There are of course many different ways to implement future and Queueable methods within Apex; everybody has a different use case, as they say. Still, it's my hope that this article has gotten you thinking about how you might best create reusably async Apex within your own codebase(s). I've uploaded the example code shown here in various iterations on the Apex Mocks repo, in the hopes that it will prove useful.

Thanks for staying with me and stay tuned for more on The Joys of Apex!

The original version of Future Method, Callouts & Callbacks can be read on my blog.

Separating the Concerns in Async Apex MethodsSetting up Callouts and HTTP Class WrappersIntroducing the Callback ObjectCallbacks, ReduxClosing Thoughts On Future Methods, Callouts & Callbacks

Home Apex Logging Service Apex Object-Oriented Basics Batchable And Queueable Apex Building A Better Singleton Continuous Integration With SFDX Dependency Injection & Factory Pattern Enum Apex Class Gotchas Extendable Apis Future Methods, Callouts & Callbacks Idiomatic Salesforce Apex Introduction & Testing Philosophy Lazy Iterators Lightweight Trigger Handler LWC Composable Modal LWC Composable Pagination LWC Custom Lead Path Mocking DML React Versus Lightning Web Components Refactoring Tips & Tricks Repository Pattern setTimeout & Implementing Delays Sorting And Performance In Apex Test Driven Development Example Testing Custom Permissions Writing Performant Apex Tests



Read more tech articles