Leverage Platform Cache to Reduce Transaction Time and Increase Customer Satisfaction

Mike Faust
AppExchange and the Salesforce Ecosystem
5 min readFeb 10, 2020

Performance is an important factor in any application. Ensuring your app performs at the top of its game in all scenarios and situations is critical for customer satisfaction and long-term adoption. This is especially true for ISVs as they create applications that must work well in an org with multiple apps created by both other ISVs and the customers themselves.

Have you built a really cool application that works great in a controlled test environment, but falls short when installed in a large, enterprise customer’s org? Perhaps transactions run longer than expected? Time to consider platform cache.

What is Platform Cache?

Platform cache increases the speed at which the data is accessed in a transaction. It further improves performance by distributing cache space to particular namespaces so that cache capacity can’t be stolen by other applications or operations. ISVs can ensure that only Apex executing from their namespace can access the cached value. The best type of data to cache is reused throughout a session, not frequently changing, and expensive to retrieve.

There are two types of platform cache:

  • Session cache stores data to be reused by an individual user’s session. This data persists for the life of the user’s session (max 8 hours).
  • Org cache stores data that can be reused by any user in the org. Org cache expires when it’s specified time-to-live is reached (max 48 hours).

Bonus: It was announced at Dreamforce ’19 that every ISV application will have access to free platform cache in their package’s namespace. Stay tuned for further updates.

Let’s Walk Through an Example

There may be times where you need to use the Apex Schema describe methods to dynamically determine accessibility of objects and fields to query and then display said fields on a screen. This metadata changes only when an admin goes into setup and is not likely to change often.

The following example loops through the metadata of ten objects, reads its associated field metadata, and uses that metadata for additional processing.

/* In this instance, a wrapper class is used to contain the data that will be used for future processing*/
List<WrapperClass> wrapperList = new List<WrapperClass>();
List<String> objectNames = new List<String>{'Account', 'Contact','Opportunity', 'Task', 'Case', 'Product2', 'OpportunityLineItem','Lead','CaseTeamMember','User'};
List<Schema.DescribeSObjectResult> objDescribeList = Schema.describeSObjects(objectNames);
for (Schema.DescribeSObjectResult r : objDescribeList) {
WrapperClass wc = new WrapperClass();
List<WrapperClass.FieldData> fieldDataList = new
List<WrapperClass.FieldData>();
for(Schema.SObjectField fieldType : r.fields.getMap().values()){
Schema.DescribeFieldResult fieldDescribe =
fieldType.getDescribe();
WrapperClass.FieldData fd = new WrapperClass.FieldData();
fd.fieldName = fieldDescribe.getName();
fd.fieldLabel = fieldDescribe.getLabel();
fd.fieldType = fieldDescribe.getType().name();
fieldDataList.add(fd);
}
wc.objectName = r.getName();
wc.objectLabel = r.getLabel();
wc.fieldMetadata = JSON.serialize(fieldDataList);
wrapperList.add(wc);
}

Demonstrated in the above example, schema describes can be expensive to execute in every transaction. Meaning, the more objects you describe and the more fields you process, the longer the CPU time. In this example, it takes around 680 milliseconds to execute for 10 objects. For 20 objects, it takes around 950 milliseconds. Imagine the performance impact if you were executing this code against hundreds of objects! (Spoiler Alert: All 730+ objects in a Dev scratch org took around 13,000 milliseconds).

The Solution? Platform Cache.

This type of data is a prime example of when to use platform cache because it’s not dynamic and not frequently changing. To improve performance, we can restructure the code to first check the org cache for the data. We only have to execute the above code when there’s a cache miss (hypothetically, the first transaction of the day).

It is a best practice to avoid calling the contains(key) method followed by the get(key) method. Since we intended to use the key value in the code below, we get the cache and if the result is null, we then perform the necessary operation to populate the variable and cache.

Below, the code snippet is refactored to only execute when the data is not found in cache.

/*Key used to store cache value. Format in namespace.partition.key*/
String cacheKey = 'cachepoc1.default.ConfigList';
List<WrapperClass> wrapperList = new List<WrapperClass>();
List<String> objectNames = new List<String>{'Account', 'Contact','Opportunity', 'Task', 'Case', 'Product2', 'OpportunityLineItem','Lead','CaseTeamMember','User'};
/*pull from cache */
wrapperList = (List<WrapperClass>)Cache.Org.get(cacheKey);
/*If data doesn't exist in cache, build wrapper class and place into cache*/
If (wrapperList == null) {
wrapperList = new List<WrapperClass>();
List<Schema.DescribeSobjectResult> objDescribeList =
Schema.describeSObjects(objectNames);
for (Schema.DescribeSObjectResult r : objDescribeList) {
WrapperClass wc = new WrapperClass();
List<WrapperClass.FieldData> fieldDataList = new
List<WrapperClass.FieldData>();
for(Schema.SObjectField fieldType : r.fields.getMap().values()){
Schema.DescribeFieldResult fieldDescribe =
fieldType.getDescribe();
WrapperClass.FieldData fd = new WrapperClass.FieldData();
fd.fieldName = fieldDescribe.getName();
fd.fieldLabel = fieldDescribe.getLabel();
fd.fieldType = fieldDescribe.getType().name();
fieldDataList.add(fd);
}
wc.objectName = r.getName();
wc.objectLabel = r.getLabel();
wc.fieldMetadata = JSON.serialize(fieldDataList);
wrapperList.add(wc);
}
Cache.Org.put(cacheKey, wrapperList);
}

NOTE: A single cache item has a limit of 100KB and will throw an Cache.ItemSizeLimitExceededException if exceeded, so you’ll need to be thoughtful on how you store data in cache. In this instance, we stored all the data in one cache item. See platform cache limits here. In your development process, you can use the cache diagnostics page in a non-packaging org to examine the cache usage and the compressed size of the cache items. To see this page, enable the user permission “Cache Diagnostics”.

Using org cache instead of session cache allows us to save precious processing time by using the same cached item across all users, not just for an individual user. By refactoring the code, we can now retrieve the data in 10 milliseconds versus the original 680 milliseconds. This reduction in CPU time is compounded across each time the code executes, thus helping you to avoid concurrent apex transaction limits.

If you want to adopt platform cache as an ISV, you should define your own namespace cache in your package and reference that in your code. This ensures that only your application is leveraging/using the namespaced cache.

Summary

Platform cache is a great way to reduce CPU time by storing frequently used data that changes infrequently. It’s also a way to ensure your app performs at its very best, which means higher customer satisfaction.

Where to Learn More

Platform Cache Overview
Platform Cache Best Practices
Platform Cache Basics Trailhead Module

--

--