Read more at, or return to the homepage

Idiomatic Salesforce Apex

We often hear the word "idiomatic" applied to programming languages to express the language-specific way of accomplishing routinely encountered problems. In this post, we dive into how to write idiomatic Salesforce Apex to make the most of each line of code as I refactoring some existing vendor code into an easier to understand format.

linkThe Problem

Following up on the Apex Logging Service post, I was looking to push exceptions generated in Apex to Rollbar. Organizations, if they're using plenty of services beyond Salesforce, will undoubtedly be using a centralized logging solution. I've seen many ELK-stack approaches out there, but Rollbar (and competitors like Sumo Logic) is commonly seen on the web development side as an easy vendor to opt into, without having to worry about hosting or dashboard building. Once a company's using a vendor for one side of their logging approach, you can best believe that getting any other service's exceptions into said solution is going to be desirable — centralize where you view exceptions being generated, and you increase the visibility of all code-related issues.

At the time, I naively assumed that because Rollbar had an Apex installed package, my conscience could rest easy going with the provided solution. I was wrong. Upon installing the Rollbar package into a sandbox, I realized quickly that there were going to be a few issues (I want to use this as an opportunity to say that I love Rollbar. I think they're a great company that provides a great service. I use their code as an opportunity to make a point in this post; I'm not trying to trash it):

That being said ... no need to reinvent the wheel, I thought. You can't see namespaced code in your org unless it's a global class (and even then you can't see the full object) ... but that wasn't the case for this code, so I dove right in.

I had already seen via the documentation on Rollbar's Apex documentation page that they were employing the Singleton pattern for accessing the logger, so that was the first thing I chose to look at:

1linkpublic with sharing class Rollbar {


3link public static Rollbar instance() {

4link if (Rollbar.instance == null) {

5link Rollbar.instance = new Rollbar();

6link }


8link return Rollbar.instance;

9link }


11link public static Rollbar init()

12link {

13link return Rollbar.init(

14link RollbarSettings__c.getInstance().AccessToken__c,

15link UserInfo.getOrganizationName()

16link );

17link }


19link public static Rollbar init(String accessToken, String environment) {

20link return Rollbar.init(new Config(accessToken, environment));

21link }


23link public static Rollbar init(Config config) {

24link Rollbar instance = instance();

25link instance.config = config;

26link instance.notifier = new Notifier(instance.config);

27link instance.initialized = true;

28link return instance;

29link }


31link public static HttpResponse log(String level, String message) {

32link Rollbar instance = initializedInstance();

33link return instance.notifier.log(level, message);

34link }


36link public static HttpResponse log(Exception exc) {

37link Rollbar instance = initializedInstance();

38link return instance.notifier.log(exc);

39link }


41link public static HttpResponse log(Exception exc, Map<String, Object> custom) {

42link Rollbar instance = initializedInstance();

43link return instance.notifier.log(exc, custom);

44link }


46link public static HttpResponse log(ExceptionData exData) {

47link Rollbar instance = initializedInstance();

48link return instance.notifier.log(exData);

49link }


51link private static Rollbar initializedInstance()

52link {

53link Rollbar instance = Rollbar.instance();

54link if (!instance.initialized) {

55link Rollbar.init();

56link }


58link return instance;

59link }


61link private Rollbar() {

62link }


64link private static Rollbar instance = null;

65link private Config config = null;

66link private Notifier notifier = null;

67link private Boolean initialized = false;


linkRefactoring Into Idiomatic Apex

Let's just leave aside all those public init methods and hone in here ...

So we've got ~ 70 lines of code mostly dealing with the problem of initializing a singleton instance / logging with strings that are actually constants.

First let's review the idomatic way to initialize Singletons in Apex (if you're interested in more information on the singleton shown below, please read Building A Better Singleton to learn more!):

1linkpublic class MyClass {

2link private MyClass() {

3link //prevent public initialization

4link //you can still use dependency injection within your constructor though

5link }


7link //Singleton method

8link private static final MyClass Instance = new MyClass();


10link //expose public static methods for using MyClass

11link public static void sayHi() {

12link Instance.say('hi');

13link }


15link private void say(String sayString) {

16link System.debug(sayString);

17link }


With that in mind, let's streamline this implementation:

1linkprivate final Http http;

2linkprivate final RollbarDataBuilder dataBuilder;


4linkprivate static final String API_URI = '';


6linkpublic enum Level { Critical, Debug, Error, Info, Warning }


8linkprivate Rollbar() {

9link this.dataBuilder = new RollbarDataBuilder();

10link this.http = new Http();



13linkpublic static final Rollbar Instance = new Rollbar();


15linkpublic static HttpResponse log(Level level, String message) {

16link Message payload =, message);

17link return send(payload);



20linkpublic static HttpResponse log(Exception ex) {

21link Message payload =;

22link return send(payload);



25linkprivate static HttpResponse send(Message payload) {

26link HttpRequest request = new HttpRequest();

27link request.setEndpoint(API_URI);

28link request.setMethod(;

29link //Rollbar only wants non-null properties sent over

30link //The second argument suppresses null values

31link request.setBody(Json.serialize(payload, true));


33link HttpResponse res = Instance.http.send(request);

34link if(res.getStatusCode() != 200) {

35link throw new CalloutException(

36link 'Rollbar callout failed with response: ' + res.getBody()

37link );

38link }

39link return res;


So what we have gained? We killed off the Notifier class (which wasn't shown, but also was just wrapping calls to the DataBuilder) in favor of the Rollbar object encapsulating the full callout. We got rid of a bunch of boilerplate related to initialization. We traded string constants for a Rollbar.Level enum which can easily be understood as the severity of the log being sent.

The astute reader might note that I'm not following the pattern I prescribed in Future Methods, Callouts & Callbacks. You'd be exactly right in saying that, but I was recently reminded of this age-old adage while reading something that came up on the React subreddit:

Duplication is far cheaper than the wrong abstraction

The Callout & Callback pattern that I documented previously is great when you have many consumers whose post-callout behavior is tightly coupled to interactions with the database or further processing is necessary. In this case, however, it would be a mistake to try to shoehorn this specific logging implementation into an abstraction meant for post-processing ... especially because having the Http object as a member of the Rollbar class will prove helpful for fully testing everything out.

But I'm getting ahead of myself. What does this DataBuilder object look like?

1linkpublic with sharing class DataBuilder {

2link public DataBuilder(Config config) {

3link this.config = config;

4link }


6link public Map<String, Object> buildPayload(String level, String message)

7link {

8link return buildPayloadStructure(level, buildMessageBody(message), null);

9link }


11link public Map<String, Object> buildPayload(Exception exc)

12link {

13link return buildPayloadStructure('error', buildExceptionBody(exc), null);

14link }


16link public Map<String, Object> buildPayload(Exception exc, Map<String, Object> custom)

17link {

18link return buildPayloadStructure('error', buildExceptionBody(exc), custom);

19link }


21link public Map<String, Object> buildPayload(ExceptionData exData)

22link {

23link Map<String, Object> custom = new Map<String, Object>();

24link custom.put('context', exData.context());


26link return buildPayloadStructure('error', buildTraceBody(exData), custom);

27link }


29link private Map<String, Object> buildExceptionBody(Exception exc)

30link {

31link if (exc.getCause() == null) {

32link return buildTraceBody(exc);

33link } else {

34link return buildTraceChainBody(exc);

35link }

36link }


38link private Map<String, Object> buildTraceChainBody(Exception exc)

39link {

40link Map<String, Object> outterExTrace = (Map<String, Object>)

41link this.buildTraceBody(exc).get('trace');

42link Map<String, Object> innerExTrace = (Map<String, Object>)

43link this.buildTraceBody(exc.getCause()).get('trace');


45link List<Map<String, Object>> traceChainList = new List<Map<String, Object>>();

46link traceChainList.add(outterExTrace);

47link traceChainList.add(innerExTrace);



50link Map<String, Object> body = new Map<String, Object>();

51link body.put('trace_chain', traceChainList);


53link return body;

54link }


56link private Map<String, Object> buildPayloadStructure(

57link String level,

58link Map<String, Object> body,

59link Map<String, Object> custom

60link ) {

61link Map<String, Object> data = this.buildDataStructure(

62link level,

63link this.config.environment(),

64link body,

65link custom

66link );


68link Map<String, Object> structure = new Map<String, Object>();

69link structure.put('access_token', this.config.accessToken());

70link structure.put('data', data);

71link return structure;

72link }


74link private Map<String, Object> buildDataStructure(

75link String level,

76link String environment,

77link Map<String, Object> body,

78link Map<String, Object> custom

79link ) {


81link Map<String, Object> notifierMap = new Map<String, Object>();

82link notifierMap.put('name', Notifier.NAME);

83link notifierMap.put('version', Notifier.VERSION);


85link Map<String, Object> structure = new Map<String, Object>();

86link structure.put('notifier', notifierMap);

87link structure.put('level', level);

88link structure.put('environment', environment);

89link structure.put('framework', 'apex');

90link structure.put('body', body);

91link structure.put('custom', custom);


93link return structure;

94link }


96link private Map<String, Object> buildMessageBody(String message)

97link {

98link Map<String, Object> messageMap = new Map<String, Object>();

99link messageMap.put('body', message);


101link Map<String, Object> body = new Map<String, Object>();

102link body.put('message', messageMap);


104link return body;

105link }


107link private Map<String, Object> buildTraceBody(ExceptionData exData)

108link {

109link List<Map<String, Object>> framesList = new List<Map<String, Object>>();


111link Map<String, Object> frameMap = new Map<String, Object>();

112link frameMap.put('filename', exData.fileName());

113link frameMap.put('class_name', exData.className());

114link frameMap.put('method', exData.fileName());

115link frameMap.put('lineno', exData.line());

116link frameMap.put('colno', exData.column());


118link framesList.add(frameMap);


120link Map<String, Object> excMap = new Map<String, Object>();

121link excMap.put('class', exData.className());

122link excMap.put('message', exData.message());


124link return buildTraceStructure(excMap, framesList);

125link }


127link private Map<String, Object> buildTraceBody(Exception exc)

128link {

129link List<Map<String, Object>> framesList = new List<Map<String, Object>>();


131link String[] frames = exc.getStackTraceString().split('\n');

132link for (String frameStr : frames) {

133link if (frameStr == '()') {

134link continue;

135link } else if (frameStr.toLowerCase() == 'caused by') {

136link break;

137link }


139link Map<String, Object> frameMap = new Map<String, Object>();

140link frameMap.put('filename', frameStr);


142link String className = frameStr.split(':')[0];

143link String methodName = '';

144link if (className != 'AnonymousBlock') {

145link className = className.split('\\.')[1];

146link methodName = frameStr.split(':')[0].split('\\.')[2];

147link }


149link frameMap.put('class_name', className);

150link frameMap.put('method', methodName);


152link Pattern linePattern = Pattern.compile('line (\\d+)');

153link Matcher lineMatcher = linePattern.matcher(frameStr);

154link lineMatcher.find();

155link frameMap.put('lineno', Integer.valueOf(;


157link Pattern colPattern = Pattern.compile('column (\\d+)');

158link Matcher colMatcher = colPattern.matcher(frameStr);

159link colMatcher.find();

160link frameMap.put('colno', Integer.valueOf(;


162link framesList.add(frameMap);

163link }


165link Map<String, Object> excMap = new Map<String, Object>();

166link excMap.put('class', exc.getTypeName());

167link excMap.put('message', exc.getMessage());


169link return buildTraceStructure(excMap, framesList);

170link }


172link private Map<String, Object> buildTraceStructure(

173link Map<String, Object> exceptionMap,

174link List<Map<String, Object>> framesList

175link ) {

176link Map<String, Object> body = new Map<String, Object>();


178link Map<String, Object> traceMap = new Map<String, Object>();


180link traceMap.put('exception', exceptionMap);

181link traceMap.put('frames', framesList);


183link body.put('trace', traceMap);


185link return body;

186link }


188link private Config config;


Eep. Hopefully you just scrolled through that at a rapid staccato. We're using a strongly-typed language — let's try to take advantage of that to cut down on some of the string maps here. We can also make use of the same Rollbar.Level enum introduced above, making it easier to differentiate between method arguments. Unfortunately, not everything will be able to be strongly typed — if you'll notice, the JSON object that's being built here makes use of two properties, exception and class, both of which are reserved keywords in Apex.

Again, since the JSON object that's being built is singularly constrained to use with Rollbar, I'm less concerned with the tight coupling between this DataBuilder object and the Rollbar class, and more concerned with how I'd like to refer to things within a "Rollbar namespace" — in other words, let's put those strongly typed classes into the Rollbar class, and rename the "DataBuilder" so that it's the "RollbarDataBuilder". That's idomatic Apex — typings to help us and our tests verify that things are being shaped correctly, with descriptive names to assist:

1linkpublic class Message {

2link public String access_token { get; set; }

3link public Data data { get; set; }



6linkpublic class Data {

7link public Data() {

8link notifier = new Notifier();

9link }

10link public Notifier notifier { get; set; }

11link public String level { get; set; }

12link public String environment { get; set; }

13link public String framework { get { set; }

14link public MessageBody body { get; set; }



17linkpublic class MessageBody {

18link //trace can't be strongly typed because

19link //it has a property named "exception"

20link //which is a reserved word in Apex

21link //the "exception" property also has "class" and "message"

22link //strings, and "class" is another reserved word

23link public Map<String, Object> trace { get; set; }

24link public List<Map<String, Object>> trace_chain { get; set; }

25link public InnerMessage message { get; set; }



28linkpublic class InnerMessage {

29link public String body { get; set; }



32linkpublic class Notifier {

33link public String name { get; set;}

34link public String version { get; set; }



37linkpublic class ExceptionFrame {

38link public String filename { get; set; }

39link public String class_name { get; set; }

40link public String method { get; set; }

41link public Integer lineno { get; set; }

42link public Integer colno { get; set; }


So we get a bunch of POJOs, which thanks to IDE intellisense are going to prove enormously helpful in making clear the shape of the log objects being constructed.

And the much-reduced RollbarDataBuilder:

1linkpublic class RollbarDataBuilder {

2link public static final String FRAMEWORK = 'apex';

3link public static final String NAME = 'rollbar-sf-apex';

4link public static final String VERSION = '1.0.0';


6link public Rollbar.Message build(Rollbar.Level level, String message) {

7link return buildPayloadStructure(level, buildMessageBody(message));

8link }


10link public Rollbar.Message build(Exception ex) {

11link return buildPayloadStructure(Rollbar.Level.Error, buildExceptionBody(ex));

12link }


14link private Rollbar.Message buildPayloadStructure(

15link Rollbar.Level level,

16link Rollbar.MessageBody body) {

17link Rollbar.Message message = new Rollbar.Message();

18link //wherever you store secrets, be it a custom setting, object, or metadata

19link message.access_token = someValue;

20link = this.buildDataStructure(level, body);

21link return message;

22link }


24link private Rollbar.Data buildDataStructure(Rollbar.Level level,

25link Rollbar.MessageBody body) {

26link Rollbar.Data data = new Rollbar.Data();

27link data.body = body;

28link //I didn't like using the Org name, preferring the granularity of the Org URL.

29link // You could realistically put any kind of domain-recognizable identifier here.

30link data.environment = Url.getSalesforceBaseUrl().toExternalForm();

31link data.framework = FRAMEWORK;

32link data.level =;

33link = NAME;

34link data.notifier.version = VERSION;

35link return data;

36link }


38link private Rollbar.MessageBody buildMessageBody(String messageBody) {

39link Rollbar.InnerMessage innerMessage = new Rollbar.InnerMessage();

40link innerMessage.body = messageBody;


42link Rollbar.MessageBody body = new Rollbar.MessageBody();

43link body.message = innerMessage;

44link return body;

45link }


47link private Rollbar.MessageBody buildExceptionBody(Exception ex) {

48link if (ex.getCause() == null) {

49link return buildTraceMessage(ex);

50link } else {

51link return buildTraceChainBody(ex);

52link }

53link }


55link private Rollbar.MessageBody buildTraceMessage(Exception ex) {

56link //note that while the typings have changed in this method

57link //the underlying logic I left alone.

58link List<Rollbar.ExceptionFrame> framesList = new List<Rollbar.ExceptionFrame>();


60link String[] frames = ex.getStackTraceString().split('\n');

61link for (String frameStr : frames) {

62link if (frameStr == '()') {

63link continue;

64link } else if (frameStr.toLowerCase() == 'caused by') {

65link break;

66link }


68link Rollbar.ExceptionFrame frame = new Rollbar.ExceptionFrame();

69link frame.filename = frameStr;


71link String className = frameStr.split(':')[0];

72link String methodName = '';

73link if (className != 'AnonymousBlock') {

74link className = className.split('\\.')[1];

75link methodName = frameStr.split(':')[0].split('\\.')[2];

76link }


78link frame.class_name = className;

79link frame.method = methodName;


81link Pattern linePattern = Pattern.compile('line (\\d+)');

82link Matcher lineMatcher = linePattern.matcher(frameStr);

83link lineMatcher.find();

84link frame.lineno = Integer.valueOf(;


86link Pattern colPattern = Pattern.compile('column (\\d+)');

87link Matcher colMatcher = colPattern.matcher(frameStr);

88link colMatcher.find();

89link frame.colno = Integer.valueOf(;


91link framesList.add(frame);

92link }


94link Map<String, Object> excMap = new Map<String, Object>();

95link excMap.put('class', exc.getTypeName());

96link excMap.put('message', exc.getMessage());


98link return buildTraceBody(excMap, framesList);

99link }


101link private Rollbar.MessageBody buildTraceBody(Map<String, Object> exceptionMap,

102link List<Rollbar.ExceptionFrame> framesList) {

103link Rollbar.MessageBody body = new Rollbar.MessageBody();

104link body.trace = new Map<String, Object>();

105link body.trace.put('exception', exceptionMap);

106link body.trace.put('frames', framesList);

107link return body;

108link }


110link private Rollbar.MessageBody buildTraceChainBody(Exception ex) {

111link Map<String, Object> outterExTrace = this.buildTraceMessage(ex).trace;

112link Map<String, Object> innerExTrace = this.buildTraceMessage(ex.getCause()).trace;


114link List<Map<String, Object>> traceChainList = new List<Map<String, Object>>();

115link traceChainList.add(outterExTrace);

116link traceChainList.add(innerExTrace);


118link Rollbar.MessageBody body = new Rollbar.MessageBody();

119link body.trace_chain = traceChainList;

120link return body;

121link }


Because the DataBuilder class came with tests, it's easy to verify that this refactor, which cut the lines of code in half, is still doing exactly what we'd like it to. The tests themselves cleaned up nicely, because there was a heck of a lot less casting to Map<String, Object> happening. I also changed some method names; particularly for public methods, it's often a waste of characters to describe the return type in the name of the method — intellisense will tell you the return type!

Lastly, I'll just touch on what I was talking about before — using the Http member object on the Rollbar class to confirm end-to-end testing. Yes, we use Test.setMock(System.HttpCalloutMock) to properly mock API responses in tests, but in order to verify that our data is built correctly prior to sending out, having access to the body of the HttpRequest is paramount. Again, I typically don't like to utilize this kind of pattern, but because logging as an activity is something that's going to happen many layers deep into your application, sometimes it's necessary (as opposed to passing an object back up the entire stack):

1linkprivate final HttpWrapper http;

2linkprivate final RollbarDataBuilder dataBuilder;


4linkprivate Rollbar() {

5link this.dataBuilder = new RollbarDataBuilder();

6link this.http = new HttpWrapper();





11link@testVisible private static HttpRequest testReq;

12linkprivate class HttpWrapper {

13link private final Http http;

14link public HttpWrapper() {

15link this.http = new Http();

16link }


18link public HttpResponse send(HttpRequest req) {

19link testReq = req;

20link return this.http.send(req);

21link }


This exhibits two other important idiomatic Apex patterns:

linkIdiomatic Apex Wrap-Up

In the end, it's a shame that some of the JSON object being sent to Rollbar made use of reserved words in Apex. This isn't something that they, as a vendor, could have anticipated; they have many SDKS across a variety of commonly used languages, and planning out their object structure with deference to one specific language is a little much to ask of anybody. Still, it made the object structure less clean than I would have liked. That the real world demands us to do things we'd prefer to avoid doing is another important lesson.

I realize I'm just brushing the surface of an extremely broad topic — "Idiomatic Apex" would be an enormous book, if printed, and Singleton initialization and strong typing is barely a start. Have some thoughts on other excellent idiomatic Apex examples? Feel free to reach out — perhaps your suggestions will inform more posts on this subject!

The original version of Idiomatic Salesforce Apex can be read on my blog.

The ProblemRefactoring Into Idiomatic ApexIdiomatic Apex Wrap-Up

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