Apex Rest API Architecture – Best Practices

What’s a Service/API

A service/API is a function that is well-defined, self-contained, and does not depend on the context or state of other services.

1. Abstract vs Concrete

When developing software we often use abstraction and polymorphism to get most of our applications. We want to reuse as much of the code as possible. Should we write our APIs that way too?The answer is NO. Concrete is better than abstract.

Can you guess why?Let me show you a few examples.

Let’s look at two API versions. Is it better to have an API that has one /customers or an API that has /accounts/contacts and, /users separately?Which one seems more descriptive to you as a developer? Which API would you rather use?I would always choose the second one.

2. URI Formatting

Treat the resource like a noun, and the HTTP method as a verb. If you do it like that, you’ll end up with something like this:

RESOURCE POST GET PUT DELETE
/accounts Creates a new account List accounts Replace accounts with new accounts (Bulk update) Delete all accounts
/accounts/0014P00002gqPpm Return error Show specific account If exist update account else error Delete specific account

This is a cleaner and more precise way to use the API. It is immediately clear to the end user, and there is a method to the madness.

Use a constants class to define all the endpoints and use {version} so it becomes a variable for each endpoint. So we can manage different versions of and endpoint in the same class.

public with sharing class Rest_Constants {

    public static final String API_VERSION = '{version}';
    public static final String API_VERSION_STR = '/' + API_VERSION;

    public static final String URI_ACCOUNTS =  API_VERSION_STR + '/accounts';
}

Return all endpoint declarations as part of /endpoints declaration
https://na132.salesforce.com/services/apexrest/v1/settings/endpoints

{
    "result": [
        {
            "path": {
                "operation": "HTTPGET",
                "uri": "/{version}/accounts"
            }
        }
    ],
    "timeStamp": "2020-05-06T16:49:00.671Z",
    "totalTime": 0.037,
    "userId": "0054P00000C8OBlQAN"
}

I prefer using plurals for resources names, read more here: https://stackoverflow.com/questions/6845772/rest-uri-convention-singular-or-plural-name-of-resource-while-creating-it

3. Error Handling

This is another important aspect of  API building. There are a few good ways to handle errors.
Create a static resource file with all the define errors of the app

[
	{
	 "id": "QUERY_OPERATION_FAILED",
	 "code": 991,
	 "clientTemplate": "Encountered an error",
	 "systemTemplate": "Query operation failed UserId: {0} | TraceId: {1} | TotalTime: {2} | Method: {3} | Path: {4} | Body: {5}"
	},
	{
	 "id": "SOBJECT_OPERATION_FAILED",
	 "code": 992,
	 "clientTemplate": "Encountered an error",
	 "systemTemplate": "SObject operation failed UserId: {0} | TraceId: {1} | TotalTime: {2} | Method: {3} | Path: {4} | Body: {5}"
	},
	{
	 "id": "JSON_SERIALIZATION_FAILED",
	 "code": 993,
	 "clientTemplate": "Encountered an error",
	 "systemTemplate": "Json serialization failed, check if Heroku is up or restart services UserId: {0} | TraceId: {1} | TotalTime: {2} | Method: {3} | Path: {4} | Body: {5}"
	},
	{
	 "id": "DATABASE_OPERATION_FAILED",
	 "code": 994,
	 "clientTemplate": "Encountered an error",
	 "systemTemplate": "Database operation failed UserId: {0} | TraceId: {1} | TotalTime: {2} | Method: {3} | Path: {4} | Body: {5}"
	},
	{
	 "id": "CALLOUT_FAILED",
	 "code": 995,
	 "clientTemplate": "Encountered an error",
	 "systemTemplate": "Callout to external service failed UserId: {0} | TraceId: {1} | TotalTime: {2} | Method: {3} | Path: {4} | Body: {5}"
	},
	{
	 "id": "SEARCH_FAILED",
	 "code": 996,
	 "clientTemplate": "Searching failed, try again",
	 "systemTemplate": "Searching Salesforce failed UserId: {0} | TraceId: {1} | TotalTime: {2} | Method: {3} | Path: {4} | Body: {5}"
	},
	{
	 "id": "REST_DISPATCHER_DEBUG",
	 "code": 997,
	 "clientTemplate": "Encountered an error",
	 "systemTemplate": "Debug UserId: {0} | TraceId: {1} | TotalTime: {2} | Method: {3} | Path: {4} | Body: {5}"
	},
	{
		"id": "SERIALIZATION_FAILED",
		"code": 998,
		"clientTemplate": "Encountered an error",
		"systemTemplate": "Serialization failed, error: {0}"
	},
	{
		"id": "REST_DISPATCHER_FAILED",
		"code": 999,
		"clientTemplate": "Encountered an error",
		"systemTemplate": "An error occurred UserId: {0} | TraceId: {1} | TotalTime: {2} | Method: {3} | Path: {4} | Body: {5}"
	}
]

Create a object representation of the error codes from the static resources

global with sharing class Rest_Error_ResponseCode {

  public String id { get; set; }
  public Integer code { get; set; }
  public String clientTemplate { private get; set { clientTemplate = value; } }
  public String systemTemplate { private get; set { systemTemplate = value; } }

  public String formattedClientMessage { get; set; }
  public String formattedSystemMessage { get; set; }

  global void formatMessages(List<String> args, Exception ex) {
    if (args != null && args.size() != 0) {
      formattedClientMessage = String.format(clientTemplate, args);
      if (systemTemplate != null) {
        formattedSystemMessage = String.format(systemTemplate, args);
      }
    } else {
      formattedClientMessage = clientTemplate;
      formattedSystemMessage = systemTemplate;
    }
    if (ex != null) {
      if (formattedSystemMessage == null) {
        formattedSystemMessage = formattedClientMessage;
      }
      formattedSystemMessage += ' -> Cause by: ' + ex.getTypeName().stripHtmlTags() + ' - ' + ex.getMessage().stripHtmlTags() + '. Cause trace: ' + ex.getStackTraceString().stripHtmlTags();
    }
  }

  global Rest_Error_ResponseCode copy() {
    Rest_Error_ResponseCode theNewObj = new Rest_Error_ResponseCode();
    theNewObj.id = this.id;
    theNewObj.code = this.code;
    theNewObj.clientTemplate = this.clientTemplate;
    theNewObj.systemTemplate = this.systemTemplate;
    return theNewObj;
  }

    global Rest_Error_ResponseCode(){}

  global override String toString() {
    return 'BrightPlan_ResponseCode(id=' + id + ';code=' + code + ';clientTemplate=' + clientTemplate + ';systemTemplate=' + systemTemplate + ';formattedClientMessage=' + formattedClientMessage + ';formattedSystemMessage=' + formattedSystemMessage + ')';
  }
}

Create a util to read the static resource file and create a map of error codes

public with sharing class Rest_Error_ResponseCodes_Util {

   private static Map<String, Rest_Error_ResponseCode> itsResponseCodes = new Map<String, Rest_Error_ResponseCode>();

   static {
    try {
      StaticResource theCodesSrc = [SELECT Id, Body FROM StaticResource WHERE Name = 'Rest_Error_ResponseCodes' LIMIT 1];
      if (theCodesSrc != null) {
            String theCodesJSON = theCodesSrc.Body.toString();
        List<Rest_Error_ResponseCode> theCodesList = (List<Rest_Error_ResponseCode>) JSON.deserialize(theCodesJSON, List<Rest_Error_ResponseCode>.class);
        for (Rest_Error_ResponseCode theCode: theCodesList) {
          itsResponseCodes.put(theCode.id, theCode);
        }
        System.debug('Loaded ' + itsResponseCodes.size() + ' response codes into static map');
      } else {
        System.debug(LoggingLevel.ERROR, 'Cannot query Error_ResponseCodes static resource from DB');
      }
    } catch (Exception ex) {
      System.debug(LoggingLevel.ERROR, 'ERROR loading response codes: ' + ex.getMessage());
    }

  }

  public static Rest_Error_ResponseCode getCode(String anID) {
    return getCode(anID, null, null);
  }

  public static Rest_Error_ResponseCode getCode(String anID, Exception anExp) {
    return getCode(anID, anExp, null);
  }

  public static Rest_Error_ResponseCode getCode(String anID, List<String> args) {
    return getCode(anID, null, args);
  }

  public static Rest_Error_ResponseCode getCode(String anID, Exception anExp, List<String> args) {
    Rest_Error_ResponseCode theCode = itsResponseCodes.get(anID);
    if (theCode == null) {
      theCode = itsResponseCodes.get('UNEXPECTED_RESPONSE_CODE').copy();
      List<String> theArgs = new List<String>();
      theArgs.add(anID);
      if (args != null) {
        theArgs.add(args + '');
      }
      theCode.formatMessages(theArgs, anExp);
      System.debug(LoggingLevel.ERROR, 'Unknown response code ' + anID + '. Returning UNEXPECTED_RESPONSE_CODE and ignoring args list ' + args);
    } else {
      theCode = theCode.copy();
      theCode.formatMessages(args, anExp);
    }
    return theCode;
  }
}

Errors returned will look like follows

{
    "responseCode": {
        "clientTemplate": "Incorrect operation, please try again",
        "code": 989,
        "formattedClientMessage": "Incorrect operation, please try again",
        "formattedSystemMessage": "Url does not exist or incorrect operation UserId: 0054P00000C8OBlQAN Type: HTTPGET URI: /v1/account Body: ",
        "id": "URL_INVALID",
        "systemTemplate": "Url does not exist or incorrect operation UserId: {0} Type: {1} URI: {2} Body {3}"
    },
    "timeStamp": "2020-05-07T17:21:47.278Z",
    "totalTime": 0.053,
    "userId": "0054P00000C8OBlQAN",
    "mapKeysFlag": false,
    "serializeNulls": false
}

4. API Versioning

Use the letter ‘v’ in the URL to denote the API version as below and handle the versions in the Rest_Dispatcher.Dispatchable

global with sharing class Rest_Accounts {

    global with sharing class Get implements Rest_Dispatcher.Dispatchable {
        private Rest_Request requestBody;

        global String getURIMapping(){
           return Rest_Constants.URI_ACCOUNTS;
        }

        global void setRequestBody(Rest_Request requestBody){
            this.requestBody = requestBody;
        }

        global Rest_Response execute(Map<String, String> parameters){
            Rest_Response restResponse = new Rest_Response();
            String version = parameters.get('version');
            
            if ('v1'.equals(version)){
                ...
            } else if ('v2'.equals(version)){
                ...
            } else if ('v3'.equals(version)){
                ...
            } 
            
            restResponse.setResult('');
            return restResponse;
        }

        global override String toString(){
          Rest_Endpoint.Path path = new Rest_Endpoint.Path();
          path.setPath(getURIMapping());
          Rest_Endpoint endpoint = new Rest_Endpoint(path);
          return JSON.serialize(endpoint);
        }
    }
}

5. Filtering

Filtering will be passed as key values where key to the Rest_Dispatcher.Dispatchable

GET /accounts?limit=50&offset=10

Execute method will receive filer key/value as parameters

 global Rest_Response execute(Map<String, String> parameters){
            Rest_Response restResponse = new Rest_Response();
            String limitVal = parameters.containsKey('limit') : parameters.get('limit') :null;
            String offsetVal = parameters.containsKey('offset') : parameters.get('offset') : null;
            List<Accounts> accoutsLst = new List<Account>();

            if (limitVal!=null && offsetVal!=null)
             accoutsLst = [Select Id, Name from Account limit :limit limitVal offset :offsetVal];
            else
             accoutsLst = [Select Id, Name from Account];
          
            restResponse.setResult(accoutsLst);
            return restResponse;
        }

6. Security

Security is one of the major concerns when compared to SOAP because still there are no standards such as ws-security defined for REST.

  • HTTPS is the default for all Apex REST API calls
  • Don’t forget to include timestamp in each and every API request and response. Make sure to log them all. In case of a dispute you can refer them.
    • Payload returned has timestamp

    Example of payload the contains timeStamp

    {
        "result": {
            "AccountId": "0014P00002hysSZQAY",
            "Id": "0054P00000C8OBlQAN"
        },
        "timeStamp": "2020-05-06T18:39:43.474Z",
        "totalTime": 0.034,
        "userId": "0054P00000C8OBlQAN",
        "mapKeysFlag": false,
        "serializeNulls": false
    }
    

    7. Analytics

    Log every API call request to build an analytical platform on top of that. Having analytics in your REST API will give you a good insight of what’s happening your API. To detect:
    1. Long running queries
    2. Duplicate requests by same user
    3. Errors by type

    public class Rest_Log {
    
      public static Rest_Log instance {get; private set;}
        
      static {
        instance = new Rest_Log();
      }
    
      private static final String PREFIX = 'Rest Log';
    
      public static HttpRequest setupLogEntries(){
        HttpResponse res=new HttpResponse();
        HttpRequest req = new HttpRequest();
        String logEntricUrl = 'https://webhook.logentries.com/noformat/logs/{logEntriesToken}';
        req.setEndpoint(logEntricUrl);
        req.setTimeout(60000);
        req.setMethod('POST');
        return req;
      }
    
      @future(callout=true)
      public static void putLogCentricLog(String responseCode) {
        try{
          HttpRequest req = setupLogEntries();
           if (responseCode!=null){
             req.setBody(PREFIX + ' : ' + responseCode);
           }
           Http http = new Http();
           http.send(req);
        } catch (System.CalloutException ex){
          System.debug('Callout failed ' + ex.getMessage());
        } catch (Exception ex){
          System.debug('Callout failed ' + ex.getMessage());
        }
      }
    }
    

    Example of the logging every API call to LogEntries (https://elements.heroku.com/addons/logentries)

    11 May 2020 10:20:05.167 Rest Log : Debug UserId: 0054P00000C8OBlQAN | TraceId: null | TotalTime: 0.042 | Method: HTTPGET | Path: /v1/accounts | Body:
    11 May 2020 10:20:10.934 Rest Log : Debug UserId: 0054P00000C8OBlQAN | TraceId: null | TotalTime: 0.007 | Method: HTTPGET | Path: /v1/settings/endpoints | Body:
    

    Example of a dashboard with widgets in LogEntries to monitor response times, errors and slow endpoints

    8. Documentation

    Proper Documentation is vital for the API. It doesn’t matter how great your API design is if the API consumers can’t consume it properly.

    Override toString for each endpoint

    global override String toString(){
              Rest_Endpoint.Path path = new Rest_Endpoint.Path();
              path.setPath(getURIMapping());
              path.setSummary('Get accounts for logged in user');
              path.setProduces(Rest_Endpoint.getModelByObject('Account'));
              Rest_Endpoint endpoint = new Rest_Endpoint(path);
              return JSON.serialize(endpoint);
            }
    

    Convert and return List of Rest Endpoints

     public static List<Rest_Endpoint> convertToEndpoint(Map<RequestType, List<Dispatchable>> dispatchableMap){
              List<Rest_Endpoint> restEndpoints = new List<Rest_Endpoint>();
              for (RequestType dpMap : dispatchableMap.keySet()){
                List<Dispatchable> dispatchableList = dispatchableMap.get(dpMap);
                for (Dispatchable dpList : dispatchableList){
                  Rest_Endpoint endpoint = (Rest_Endpoint)System.JSON.deserialize(dpList.toString(), Rest_Endpoint.class);
                  Rest_Endpoint.Path path = endpoint.getPath();
                  path.setOperation(String.valueOf(dpMap));
                  restEndpoints.add(endpoint);
                }
              }
              return restEndpoints;
            }
    

    Calling GET /settings/endpoints will return api documentation for all endpoints

    {
        "result": [
            {
                "path": {
                    "operation": "HTTPGET",
                    "produces": {
                        "Ownership_Check__c": "ownershipCheck",
                        "Total_Assets__c": "totalAssets",
                        "Number_of_Contacts__c": "numberofContacts",
                        "Match_Billing_Address__c": "matchBillingAddress",
                        "SLAExpirationDate__c": "sLAExpirationDate",
                        "SLASerialNumber__c": "sLASerialNumber",
                        "UpsellOpportunity__c": "upsellOpportunity",
                        "NumberofLocations__c": "numberofLocations",
                        "Active__c": "active",
                        "SLA__c": "sLA",
                        "CustomerPriority__c": "customerPriority",
                        "SicDesc": "sicDesc",
                        "AccountSource": "accountSource",
                        "Jigsaw": "jigsaw",
                        "IsCustomerPortal": "isCustomerPortal",
                        "IsPartner": "isPartner",
                        "Site": "site",
                        "Rating": "rating",
                        "Description": "description",
                        "TickerSymbol": "tickerSymbol",
                        "Ownership": "ownership",
                        "NumberOfEmployees": "numberOfEmployees",
                        "AnnualRevenue": "annualRevenue",
                        "Industry": "industry",
                        "Sic": "sic",
                        "Website": "website",
                        "AccountNumber": "accountNumber",
                        "Fax": "fax",
                        "Phone": "phone",
                        "ShippingLongitude": "shippingLongitude",
                        "ShippingLatitude": "shippingLatitude",
                        "ShippingCountry": "shippingCountry",
                        "ShippingPostalCode": "shippingPostalCode",
                        "ShippingState": "shippingState",
                        "ShippingCity": "shippingCity",
                        "ShippingStreet": "shippingStreet",
                        "BillingLongitude": "billingLongitude",
                        "BillingLatitude": "billingLatitude",
                        "BillingCountry": "billingCountry",
                        "BillingPostalCode": "billingPostalCode",
                        "BillingState": "billingState",
                        "BillingCity": "billingCity",
                        "BillingStreet": "billingStreet",
                        "ParentId": "parentId",
                        "Type": "type",
                        "Name": "name"
                    },
                    "summary": "Get account for logged in user",
                    "uri": "/{version}/accounts"
                }
            }
        ],
        "timeStamp": "2020-05-11T23:43:51.063Z",
        "totalTime": 0.202,
        "userId": "0054P00000C8OBlQAN",
        "mapKeysFlag": false,
        "serializeNulls": false
    }
    
  • Apex Coding Interview Challenge #3

    Coding question asked by Google

    Develop a Trigger on asset to count the amount of assets for a specific account. The value of the asset should increate on insert or undeleteand decrease on delete or reparent to a new account.

    Assumption: an asset cannot have a null AccountId, validation rule will fail and not save the record. Asset should always have a lookup to an account.

    Apex Asset Trigger

    trigger AssetTrigger on Asset (after insert, after update, after delete, after undelete) {
        
        Integer incDecVal = 1;
        if (Trigger.isDelete){
            incDecVal = -1;
        }
        
        AssetHelper.countAccountAssets(trigger.newMap, trigger.oldMap, incDecVal);
    }
    

    Apex asset helper class

    public with sharing class AssetHelper {
     
         public static void countAccountAssets(Map<Id, Asset> newAssets, Map<Id, Asset> oldAssets, Integer incrementDecrementVal){
             Set<Id> accountIds = new Set<Id>();
             
             if (newAssets!=null){
                for (Asset newAsset : newAssets.values())
                     accountIds.add(newAsset.AccountId);
                
             }
             
             if (oldAssets!=null){
                for (Asset oldAsset : oldAssets.values())
                   accountIds.add(oldAsset.AccountId);
             }
                 
             Map<Id, Account> accountMap = new Map<Id, Account>([Select Id, Total_Assets__c from Account where Id IN :accountIds]);
             
             Set<Account> updateAccountSet = new Set<Account>();
             
             if (newAssets!=null){
               for (Id newAssetId : newAssets.keySet()){
                 Asset newAsset = newAssets.get(newAssetId);
                 if (accountMap.containsKey(newAsset.AccountId)){
                     Account updateAccount = accountMap.get(newAsset.AccountId);
                     updateAccount.Total_Assets__c += incrementDecrementVal;
                     
                     if (updateAccount.Total_Assets__c < 0){
                      updateAccount.Total_Assets__c = 0;
                     }
                     
                     updateAccountSet.add(updateAccount);
                 }
               }
             }
             
        
             if (oldAssets!=null){
               for (Id oldAssetId : oldAssets.keySet()){
                 Asset oldAsset = oldAssets.get(oldAssetId);
                 
                 if (accountMap.containsKey(oldAsset.AccountId)){
                     Account updateAccount = accountMap.get(oldAsset.AccountId);
                     if (newAssets!=null && newAssets.containsKey(oldAssetId)){
                         Asset newAsset = newAssets.get(oldAssetId);
                         if (oldAsset.AccountId!=newAsset.AccountId){
                             updateAccount.Total_Assets__c -= incrementDecrementVal;
                         }
                     } else {
                         updateAccount.Total_Assets__c += incrementDecrementVal;
                     }
                     
                     if (updateAccount.Total_Assets__c < 0){
                      updateAccount.Total_Assets__c = 0;
                     }
                     
                     updateAccountSet.add(updateAccount);
                 }
               }
             } 
            
             Savepoint sp = Database.setSavePoint();
             try{
                 Database.update(new List<Account>(updateAccountSet));
             } catch(DMLException ex){
                 Database.rollback(sp);
             }
         }
    }
    

    Apex asset helper test class

    @isTest public class AssetHelperTest {
    
        @TestSetup static void setupAssetAccount(){
            Account setupAccount = new Account(Name='Test Account', Total_Assets__c=0);
            insert setupAccount;
            
            Asset setupAsset = new Asset(AccountId=setupAccount.Id, Name='Test Asset Setup');
            insert setupAsset;
        }
        
        @isTest static void testInsertAsset(){
            System.assertEquals([Select Total_Assets__c from Account][0].Total_Assets__c, 1);
        }
        
        @isTest static void testDeleteAsset(){
            System.assertEquals([Select Total_Assets__c from Account][0].Total_Assets__c, 1);
            Test.startTest();
                delete [Select Id from Asset][0];
            Test.stopTest();
            System.assertEquals([Select Total_Assets__c from Account][0].Total_Assets__c, 0);
        }
        
        @isTest static void testUnDeleteAsset(){
            System.assertEquals([Select Total_Assets__c from Account][0].Total_Assets__c, 1);
            Asset deleteAsset = [Select Id from Asset][0];
            delete deleteAsset;
            System.assertEquals([Select Total_Assets__c from Account][0].Total_Assets__c, 0);
            Test.startTest();
                undelete deleteAsset;
            Test.stopTest();
            System.assertEquals([Select Total_Assets__c from Account][0].Total_Assets__c, 1);
        }
        
        @isTest static void testReparent(){
            System.assertEquals([Select Total_Assets__c from Account][0].Total_Assets__c, 1);
            
            Account newAccount = new Account(Name='New Account', Total_Assets__c=0);
            insert newAccount;
            
            Asset updateAsset = [Select Id, AccountId from Asset][0];
            updateAsset.AccountId = newAccount.Id;
            
            Test.startTest();
                update updateAsset;
            Test.stopTest();
            
            System.assertEquals([Select Total_Assets__c from Account][0].Total_Assets__c, 0);
            System.assertEquals([Select Total_Assets__c from Account where Id=:newAccount.Id][0].Total_Assets__c, 1);
        }
    }
    

    Apex SFDC99 Final Project Challenge Solution

    Here is a fun Apex challenge to test your Apex skills:
    https://www.sfdc99.com/2019/01/16/final-project-challenge/

    Optimizations:
    1. Add static variables to custom variables/custom metadata
    2. Cache mapOfZipOwner for faster retrieval + less SOQL queries

    Assumption: updating an account owner can only be when:
    1. Zip code was empty (null) and now has a value
    2. Zip code updated from one zip code to another
    3. Zip code updated to the same zip code will NOT update owner

    Account Zip Code Trigger

    trigger AccountZipCodeTrigger on Account (after insert, after update) {
       Map<Id, Account> accountsZipCodeChanges = new Map<Id, Account>();
       Map<Id, Account> oldAccount = Trigger.oldMap;
       Map<Id, String> oldZipCodes = new Map<Id, String>();
       for (Account acc : Trigger.new){
           if (oldAccount!=null && oldAccount.containsKey(acc.Id)){
            Account oldAccount = oldAccount.get(acc.Id);
            if (acc.BillingPostalCode!=oldAccount.BillingPostalCode){
                accountsZipCodeChanges.put(acc.Id, acc);
                oldZipCodes.put(acc.Id, oldAccount.BillingPostalCode);
            }
           } else if (acc.BillingPostalCode!=null){
               accountsZipCodeChanges.put(acc.Id, acc);
           }
       }
       
       if (!accountsZipCodeChanges.isEmpty()){
           AccountZipCodeHelper.updateOwner(accountsZipCodeChanges, oldZipCodes);
       }
    }
    

    Account Zip Code Helper class

    public with sharing class AccountZipCodeHelper {
    
        static Map<String, List<Territory__c>> mapOfZipOwner = new Map<String, List<Territory__c>>();
        static Map<String, Set<Id>> mapOfZipOwnerIds = new Map<String, Set<Id>>();
        
        @TestVisible private static Boolean throwDMLException;
        private static Boolean noExceptionsOccurSendEmail = true;
        
        public static void createTerritoryMap(Map<String, List<Territory__c>> mapOfZipOwner){
            for (Territory__c territory : [select Id, Zip_Code__c, Owner__c from Territory__c Order By Last_Assignment_Received__c ASC NULLS FIRST]){
                if (territory.Zip_Code__c.contains('*')){
                    for (Integer k = 0; k <= 9; k++){
                        String zipCodeReplace = territory.Zip_Code__c.replace('*', String.valueOf(k));
                        if (mapOfZipOwner.containsKey(zipCodeReplace)){
                            List<Territory__c> territoryLst = mapOfZipOwner.get(zipCodeReplace);
                            territoryLst.add(territory);
                            mapOfZipOwner.put(zipCodeReplace, territoryLst);
                        } else {
                            mapOfZipOwner.put(zipCodeReplace, new List<Territory__c>{territory});
                        }
                    }
                } else {
                    if (mapOfZipOwner.containsKey(territory.Zip_Code__c)){
                        List<Territory__c> territoryLst = mapOfZipOwner.get(territory.Zip_Code__c);
                        territoryLst.add(territory);
                        mapOfZipOwner.put(territory.Zip_Code__c, territoryLst);
                    } else {
                        mapOfZipOwner.put(territory.Zip_Code__c, new List<Territory__c>{territory});
                    }
                }
                
            }
            throwDMLException = false;
        }
        
        static void createTerritoryOwnerIdSet(Map<String, List<Territory__c>> mapOfZipOwners){
            for (String mapOfZipOwner : mapOfZipOwners.keySet()){
                 Set<Id> accountOwnerSet = new Set<Id>();
                 for (Territory__c territory : mapOfZipOwners.get(mapOfZipOwner)){
                     accountOwnerSet.add(territory.Owner__c);
                 }
                 mapOfZipOwnerIds.put(mapOfZipOwner, accountOwnerSet);
            }
        }
        
        static {
            createTerritoryMap(mapOfZipOwner);
            createTerritoryOwnerIdSet(mapOfZipOwner);
        }
        
        public static void checkTerritory(List<Territory__c> newTerritories){
            for (Territory__c newTerritory : newTerritories){
                if (mapOfZipOwner.containsKey(newTerritory.Zip_Code__c)){
                    List<Territory__c> existingTerritories = mapOfZipOwner.get(newTerritory.Zip_Code__c);
                    if (existingTerritories.size() >= 3){
                        newTerritory.addError('Only 3 owners for zip code ' + newTerritory.Zip_Code__c);
                    }
                }
            }
        }
        
        public static List<Account> getAccountContactOpporunityFromId(Set<Id> accountIds){
           return [Select Id, OwnerId, (Select Id, OwnerId from Contacts), (Select Id, OwnerId from Opportunities) from Account where Id IN :accountIds];
        }
        
        
        private class TerritoryCompare implements Comparable {
            private Territory__c originalTerritory;
        
            public Integer compareTo(Object obj){
                Territory__c t = (Territory__c)obj;
                Integer compareAssignmentReceived = 0;
                
                if (originalTerritory.Last_Assignment_Received__c < t.Last_Assignment_Received__c){
                    compareAssignmentReceived = -1;
                } else if (t.Last_Assignment_Received__c < originalTerritory.Last_Assignment_Received__c){
                    compareAssignmentReceived = 1;
                }
                
                return compareAssignmentReceived;
            }
        }
        
        public static List<Territory__c> getRoundRobinTerritoryOwner(List<Territory__c> territories){
           territories.sort();
           territories.get(0).Last_Assignment_Received__c = DateTime.now();
           return territories;
        }
    
        public static Map<Id, Account> checkAccountsOwnership(List<Account> accounts){
            Map<Id, Account> accountOwnerFixMap = new Map<Id, Account>();
            for (Account acc : accounts){
                if (mapOfZipOwnerIds.containsKey(acc.BillingPostalCode)){
                    Set<Id> accountOwnerSet = mapOfZipOwnerIds.get(acc.BillingPostalCode);
                    if (!accountOwnerSet.contains(acc.OwnerId)){
                        accountOwnerFixMap.put(acc.Id, acc);
                    }
                }
            }
            return accountOwnerFixMap;
        }
    
        public static void updateOwner(Map<Id, Account> accountsToUpdateOwnerFromZip, Map<Id, String> oldZipCodes){
            List<Account> accountOwnersToUpdate = new List<Account>();
            List<Contact> contactOwnersToUpdate = new List<Contact>();
            List<Opportunity> opportunityToUpdate = new List<Opportunity>();
            Map<Id, Territory__c> territoryUpdate = new Map<Id, Territory__c>();
            List<Assignment_History__c> assignmentHistoryToInsert = new List<Assignment_History__c>();
            
            List<Account> accountContactOppLst = getAccountContactOpporunityFromId(accountsToUpdateOwnerFromZip.keySet());
            for (Account accountToUpdateOwner : accountContactOppLst){
                Account acc = accountsToUpdateOwnerFromZip.get(accountToUpdateOwner.Id);
                
                Id newUserId = null;
                Territory__c newTerritory = null;
                if (mapOfZipOwner.containsKey(acc.BillingPostalCode)){
                    List<Territory__c> territoryLst = mapOfZipOwner.get(acc.BillingPostalCode);
                    
                    List<Territory__c> territoryIndex = getRoundRobinTerritoryOwner(territoryLst);
                    newTerritory = territoryIndex.get(0); 
                    newUserId = newTerritory.Owner__c;
                }
                
                if (newUserId!=null){
                    if (acc.OwnerId != newUserId){
                        accountOwnersToUpdate.add(new Account(Id=accountToUpdateOwner.Id, OwnerId=newUserId, Ownership_Check__c=DateTime.now()));
                        
                        Assignment_History__c assignmentHistory = new Assignment_History__c();
                        assignmentHistory.New_Owner__c = newUserId;
                        assignmentHistory.Previous_Owner__c = acc.OwnerId;
                        assignmentHistory.Changed_By__c = UserInfo.getUserId();
                        assignmentHistory.New_Territory__c = newTerritory.Id;
                        String oldZip = oldZipCodes.get(acc.Id);
                        if (mapOfZipOwner.containsKey(oldZip)){
                            assignmentHistory.Previous_Territory__c = mapOfZipOwner.get(oldZip).get(0).Id;
                        }
                       
                        assignmentHistory.Account__c = accountToUpdateOwner.Id;
                        assignmentHistoryToInsert.add(assignmentHistory);
                        
                        territoryUpdate.put(newTerritory.Id, new Territory__c(Id=newTerritory.Id, Last_Assignment_Received__c=DateTime.now()));
                    }
                       
                    if (accountToUpdateOwner.contacts!=null){
                        for (Contact con : accountToUpdateOwner.contacts){
                            if (con.OwnerId != newUserId){
                                contactOwnersToUpdate.add(new Contact(Id=con.Id, OwnerId=newUserId));
                            }
                        }
                    }
                    
                    if (accountToUpdateOwner.opportunities!=null){
                        for (Opportunity opp : accountToUpdateOwner.opportunities){
                            if (opp.OwnerId != newUserId){
                                opportunityToUpdate.add(new Opportunity(Id=opp.Id, OwnerId=newUserId));
                            }
                        }
                    }
                
                }
            }
            
            SavePoint sp = Database.setSavepoint();
            List<Messaging.SingleEmailMessage> mails = new List<Messaging.SingleEmailMessage>();
            try {
               Database.update(accountOwnersToUpdate);
               Database.update(contactOwnersToUpdate);
               Database.update(opportunityToUpdate);
               Database.update(new List<Territory__c>(territoryUpdate.values()));
               List<Database.SaveResult> assignmentInserts = Database.insert(assignmentHistoryToInsert);
               mails = createEmails(assignmentHistoryToInsert, assignmentInserts);
                
               if (throwDMLException){
                   throw new DMLException('This is a test dml exception');
               }
              
            } catch(DMLException ex){
                noExceptionsOccurSendEmail = false;
                Database.rollback(sp);
            } 
             
            if (noExceptionsOccurSendEmail){
                Messaging.sendEmail(mails);
            }
              
        }
        
        public static List<Messaging.SingleEmailMessage> createEmails(List<Assignment_History__c> assignmentHistories, List<Database.SaveResult> assignmentInserts){  
            Map<Id, Assignment_History__c> asignmentHistorySuccessMap = new Map<Id, Assignment_History__c>();
            for (Integer k=0; k < assignmentHistories.size(); k++){
                if (assignmentInserts.get(k).isSuccess()){
                    asignmentHistorySuccessMap.put(assignmentHistories.get(k).Id, assignmentHistories.get(0));
                }
            }
            
            List<Assignment_History__c> usersToSendEmails = [Select Id, Account__c, New_Owner__r.Email, Previous_Owner__r.Email from Assignment_History__c where Id IN :asignmentHistorySuccessMap.keySet()];
            
            
            List<Messaging.SingleEmailMessage> mails = new List<Messaging.SingleEmailMessage>();
            
            for (Assignment_History__c usersToSendEmail : usersToSendEmails){
                if (usersToSendEmail.New_Owner__r.Email!=null && usersToSendEmail.Previous_Owner__r.Email!=null)
                    mails.add(sendEmailToBothOwner(usersToSendEmail.Account__c, usersToSendEmail.Previous_Owner__r.Email, usersToSendEmail.New_Owner__r.Email));
            }
        
            return mails;
        }
        
        private static Messaging.SingleEmailMessage sendEmailToBothOwner(Id accId, String fromUserEmail, String toUserEmail){
            Messaging.reserveSingleEmailCapacity(2);
             
            Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
            
            String[] toAddresses = new String[] {fromUserEmail, toUserEmail}; 
            
            // Assign the addresses for the To and CC lists to the mail object.
            mail.setToAddresses(toAddresses);
            
            mail.setReplyTo('youremail@gmail.com');
            
            // Specify the name used as the display name.
            mail.setSenderDisplayName('Account Ownership Support');
            
            // Specify the subject line for your email address.
            mail.setSubject('Account Ownership changed for : ' + accId);
              
            mail.setUseSignature(false);
            
            // Specify the text content of the email.
            mail.setPlainTextBody('Your Account: ' + accId +' has been changed owenership.');
            
            mail.setHtmlBody('Your account:<b> ' + accId +' </b>has changed ownership.
    
    '+
                 'To view your case <a href=https://na132.salesforce.com/'+accId+'>click here.</a>');
            
            return mail;
        }
    }
    

    Account Zip Code Batch Job

    global class AccountZipCodeBatch implements Database.Batchable<sObject>, Database.Stateful {
    
        global Integer queryLimit = 1000;
        global String queryAccount = 'select Id, OwnerId, BillingPostalCode from Account where BillingPostalCode!=null and (Ownership_Check__c=Yesterday or Ownership_Check__c=null) limit ' + queryLimit;
        global Id currentBatchJobId;
        @TestVisible static Boolean testFlag = true;
        
        global Database.QueryLocator start(Database.BatchableContext BC){
            return Database.getQueryLocator(queryAccount);
        }
        
        global void execute(Database.BatchableContext Bc, List<Account> accounts){
            Map<Id, Account> accountOwnerCheckMap = AccountZipCodeHelper.checkAccountsOwnership(accounts);
            if (accountOwnerCheckMap.size() > 0)
                AccountZipCodeHelper.updateOwner(accountOwnerCheckMap, new Map<Id, String>());
        }
        
        global void finish(Database.BatchableContext Bc){
             if(!Test.isRunningTest()){
                if(Database.query(queryAccount).size()> 0) {
                    if (testFlag){
                         currentBatchJobId = Database.executeBatch(this, 200);
                    }
                } else if (bc.getJobId()!=null){
                        System.AbortJob(bc.getJobId());
                }
            } else {
                System.abortJob(bc.getJobId());
            }
        }
    }
    

    Account Zip Code Test (100% code coverage)

    @isTest
    public class AccountZipCodeHelperTest {
        
        static List<User> uu = new List<User>();
        static {
            Profile p = [SELECT Id FROM Profile WHERE Name='Standard User']; 
            
            while (uu.size() < 6) {
              Blob b = Crypto.GenerateAESKey(128);
              String h = EncodingUtil.ConvertTohex(b);
              String uid = h.SubString(0,8);
              User u = new User(Alias = uid, Email= uid + '@myorg.com', 
                  EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US', 
                  LocaleSidKey='en_US', ProfileId = p.Id, 
                  TimeZoneSidKey='America/New_York', UserName= uid + '@myorg.com');      
              uu.add(u);
            }
            insert(uu);
        } 
      
        @TestSetup static void setup(){
            Territory__c t1 = new Territory__c(Zip_Code__c='95070', Owner__c=uu.get(0).Id);
            Territory__c t2 = new Territory__c(Zip_Code__c='95071', Owner__c=uu.get(1).Id);
            Territory__c t3 = new Territory__c(Zip_Code__c='95072', Owner__c=uu.get(2).Id);
            Territory__c t4 = new Territory__c(Zip_Code__c='95072', Owner__c=uu.get(3).Id);
            Territory__c t5 = new Territory__c(Zip_Code__c='95075', Owner__c=uu.get(4).Id);
            Territory__c t6 = new Territory__c(Zip_Code__c='95072', Owner__c=uu.get(0).Id);
            Territory__c t7 = new Territory__c(Zip_Code__c='9506*', Owner__c=uu.get(0).Id);
            Territory__c t8 = new Territory__c(Zip_Code__c='95061', Owner__c=uu.get(2).Id);
            List<Territory__c> tLst = new List<Territory__c>{t1, t2, t3, t4, t5, t6, t7, t8};
            insert tLst;
          
            Account acc = new Account(Name='Test Account', BillingPostalCode = '95070');
            insert acc;
            
            Contact con = new Contact(FirstName='First', LastName='Last', Email='youremail@gmail.com', AccountId=acc.Id);
            insert con;
            
            Opportunity opp = new Opportunity(Name='Opp Test', AccountId = acc.Id, StageName='Open', CloseDate=Date.today());
            insert opp;
        }
        
        @isTest static void checkTerritoryMore3Zip(){
            Test.startTest();
                Territory__c t7 = new Territory__c(Zip_Code__c='95072', Owner__c=uu.get(0).Id);
                try{
                    insert t7;
                } catch(Exception ex){
                    System.assert(ex.getMessage().contains('Only 3 owners for zip code'));
                }
            Test.stopTest();
        }
        
        @isTest static void checkZipCodeUpdate(){
            Account accOwner = [Select Id, OwnerId from Account][0];  
        
            Test.startTest();
                accOwner.BillingPostalCode = '95075';
                update accOwner;
            Test.stopTest();
            
            Id qAccAfter = [Select Id, OwnerId from Account][0].OwnerId;
            Id qConConIdAfter = [Select OwnerId from Contact][0].OwnerId;
            Id qOppOwnerId = [Select OwnerId from Opportunity][0].OwnerId;
            List<Assignment_History__c> assignmentHistories = [Select Account__c, Id, Name, New_Owner__c, New_Territory__c, Previous_Owner__c, Previous_Territory__c from Assignment_History__c];
             
            System.assert(uu.size() == 6);
            
            Territory__c territory = [Select Id, Owner__c from Territory__c where Zip_Code__c='95075'][0];
            
            System.assertEquals(assignmentHistories.size(), 1);
            Assignment_History__c assignmentHistory = assignmentHistories.get(0);
            System.assertEquals(assignmentHistory.Account__c, accOwner.Id);
            System.assertEquals(assignmentHistory.New_Owner__c, territory.Owner__c);
            System.assertEquals(assignmentHistory.New_Territory__c, territory.Id);
            System.assert(assignmentHistory.Previous_Territory__c!=null);
           
            System.assertEquals(qAccAfter, territory.Owner__c);
            System.assertEquals(qconConIdAfter, territory.Owner__c);
            System.assertEquals(qOppOwnerId, territory.Owner__c);
            
            Integer invocations = Limits.getEmailInvocations();
            System.assertEquals(1, invocations, 'An email should be sent');
        }
        
        @isTest static void checkWildCardZipCode(){
            Account accOwner = [Select Id, OwnerId from Account][0];  
        
            Test.startTest();
                accOwner.BillingPostalCode = '95063';
                update accOwner;
            Test.stopTest();
            
            Id qAccAfter = [Select Id, OwnerId from Account][0].OwnerId;
            Id qConConIdAfter = [Select OwnerId from Contact][0].OwnerId;
            Id qOppOwnerId = [Select OwnerId from Opportunity][0].OwnerId;
            List<Assignment_History__c> assignmentHistories = [Select Account__c, Id, Name, New_Owner__c, New_Territory__c, Previous_Owner__c, Previous_Territory__c from Assignment_History__c];
             
            System.assert(uu.size() == 6);
            
            Territory__c territory = [Select Id, Owner__c from Territory__c where Zip_Code__c='9506*'][0];
            
            System.assertEquals(assignmentHistories.size(), 1);
            Assignment_History__c assignmentHistory = assignmentHistories.get(0);
            System.assertEquals(assignmentHistory.Account__c, accOwner.Id);
            System.assertEquals(assignmentHistory.New_Owner__c, territory.Owner__c);
            System.assertEquals(assignmentHistory.New_Territory__c, territory.Id);
            System.assert(assignmentHistory.Previous_Territory__c!=null);
            
            System.assertEquals(qAccAfter, territory.Owner__c);
            System.assertEquals(qconConIdAfter, territory.Owner__c);
            System.assertEquals(qOppOwnerId, territory.Owner__c);
            
            Integer invocations = Limits.getEmailInvocations();
            System.assertEquals(1, invocations, 'An email should be sent');
        }
        
        
         @isTest static void checkZipCodeRoundRobin(){
            Account accOwner = [Select Id, OwnerId from Account][0];
            Id conConId = [Select OwnerId from Contact][0].OwnerId;
            Id oppOwnerId = [Select OwnerId from Opportunity][0].OwnerId;
            
            System.assertEquals(accOwner.OwnerId, conConId);
            System.assertEquals(oppOwnerId, conConId);
            System.assertEquals(oppOwnerId, accOwner.OwnerId);
            
            Test.startTest();
                accOwner.BillingPostalCode = '95072';
                update accOwner;
            Test.stopTest();
            
            Id qAccAfter = [Select Id, OwnerId from Account][0].OwnerId;
            Id qConIdAfter = [Select OwnerId from Contact][0].OwnerId;
            Id qOppOwnerId = [Select OwnerId from Opportunity][0].OwnerId;
            List<Assignment_History__c> assignmentHistories = [Select Account__c, Id, Name, New_Owner__c, New_Territory__c, Previous_Owner__c, Previous_Territory__c from Assignment_History__c];
             
            System.assert(uu.size() == 6);
            
            Set<Id> ownerIds = new Set<Id>();
            for (Territory__c territory :  [Select Id, Owner__c from Territory__c where Zip_Code__c='95072']){
                ownerIds.add(territory.Owner__c);
            }
            
            System.assertEquals(assignmentHistories.size(), 1);
            Assignment_History__c assignmentHistory = assignmentHistories.get(0);
            System.assertEquals(assignmentHistory.Account__c, accOwner.Id);
            //System.assertEquals(assignmentHistory.New_Owner__c, territory.Owner__c);
            //System.assertEquals(assignmentHistory.New_Territory__c, territory.Id);
            System.assert(assignmentHistory.Previous_Territory__c!=null);
            
            System.assert(ownerIds.contains(qAccAfter));
            System.assert(ownerIds.contains(qConIdAfter));
            System.assert(ownerIds.contains(qOppOwnerId));
            
            Integer invocations = Limits.getEmailInvocations();
            System.assertEquals(1, invocations, 'An email should be sent');
            
            System.debug('>>> Test second BillingPostalCode update >>> ');
            //start test round robin assignment
            //have to assign to another postalcode first
            accOwner.BillingPostalCode = '95061';
            update accOwner;
            
            //assign back to 95072 to be assigned to next owner
            accOwner.BillingPostalCode = '95072';
            update accOwner;
            
     
            Id qAccAfterRR = [Select Id, OwnerId from Account][0].OwnerId;
            Id qConIdAfterRR = [Select OwnerId from Contact][0].OwnerId;
            Id qOppOwnerIdRR = [Select OwnerId from Opportunity][0].OwnerId;
            
            System.assert(ownerIds.contains(qAccAfterRR));
            System.assert(ownerIds.contains(qConIdAfterRR));
            System.assert(ownerIds.contains(qOppOwnerIdRR));
            
            System.assert(qAccAfter!=qAccAfterRR);
            System.assert(qConIdAfter!=qConIdAfterRR);
            System.assert(qOppOwnerId!=qOppOwnerIdRR);
            //end test round robin assignment
            
            List<Territory__c> countTerritories = [Select Id from Territory__c where Zip_Code__c='95072' and Last_Assignment_Received__c!=null];
            System.assertEquals(countTerritories.size(), 2);
        }
        
        @isTest static void testDMLExceptionAndRollback(){
            Account accOwner = [Select Id, OwnerId from Account][0];
            Id conConId = [Select OwnerId from Contact][0].OwnerId;
            Id oppOwnerId = [Select OwnerId from Opportunity][0].OwnerId;
            
            AccountZipCodeHelper.throwDMLException = true;
            
            Test.startTest();
                accOwner.BillingPostalCode = '95075';
                update accOwner;
            Test.stopTest();
            
            Id qAccAfter = [Select Id, OwnerId from Account][0].OwnerId;
            Id qConConIdAfter = [Select OwnerId from Contact][0].OwnerId;
            Id qOppOwnerId = [Select OwnerId from Opportunity][0].OwnerId;
            List<Assignment_History__c> assignmentHistories = [Select Account__c, Id, Name, New_Owner__c, New_Territory__c, Previous_Owner__c, Previous_Territory__c from Assignment_History__c];
            
            System.assertEquals(assignmentHistories.size(), 0);
           
            System.assertEquals(qAccAfter, accOwner.OwnerId);
            System.assertEquals(qconConIdAfter, conConId);
            System.assertEquals(qOppOwnerId, oppOwnerId);
            
            Integer invocations = Limits.getEmailInvocations();
            System.assertEquals(1, invocations, 'An email should be sent');
        }
        
        @isTest static void testCheckAccountOwnership(){
            List<Account> accs = [select Id, OwnerId, BillingPostalCode from Account where BillingPostalCode!=null and (Ownership_Check__c=Yesterday or Ownership_Check__c=null) limit 1000]; 
            Map<Id, Account> accountOwnerCheckMap = AccountZipCodeHelper.checkAccountsOwnership(accs);
            System.assertEquals(accountOwnerCheckMap.size(), 1);
            AccountZipCodeHelper.updateOwner(accountOwnerCheckMap, new Map<Id, String>());
            
            List<Account> accsAfter = [select Id, OwnerId, BillingPostalCode from Account where BillingPostalCode!=null and (Ownership_Check__c=Yesterday or Ownership_Check__c=null) limit 1000]; 
            Map<Id, Account> accountOwnerCheckMapAfter = AccountZipCodeHelper.checkAccountsOwnership(accsAfter);
            System.assertEquals(accountOwnerCheckMapAfter.size(), 0);
        }
        
        @isTest static void testOwnershipBatch(){
        
            Test.startTest();
                AccountZipCodeBatch accountZipBatch = new AccountZipCodeBatch();
                Database.executeBatch(accountZipBatch);
            Test.stopTest();
            
            List<Account> accsAfter = [select Id, OwnerId, BillingPostalCode from Account where BillingPostalCode!=null and (Ownership_Check__c=Yesterday or Ownership_Check__c=null) limit 1000]; 
            Map<Id, Account> accountOwnerCheckMapAfter = AccountZipCodeHelper.checkAccountsOwnership(accsAfter);
            System.assertEquals(accountOwnerCheckMapAfter.size(), 0);
        }
        
        @isTest static void testTerritoryCompare(){
             List<Territory__c> territories = [Select Id, Zip_Code__c, Owner__c, Last_Assignment_Received__c from Territory__c];
             territories.sort();
             System.debug('>>> territories before >> ' + territories);
             
             Integer counter = 0;
             for (Territory__c territory : territories){
                 if (counter > 1){
                     territory.Last_Assignment_Received__c = DateTime.now().addDays(counter * -1);
                 } 
                 counter++;
             }
              
             territories.sort();
             System.debug('>>> territories after >> ' + territories);
        }
    }
    

    Apex extend standard objects to add business logic

    Currently it is not possible to extend a standard or custom object in Salesforce. Why would you want to do that you may ask, think about the following, how would you check the age of a contact, if they have a mobilephone, sort list of objects, serialize, deserialize, .

    Contact con = [Select Birthdate from Contact][0];
    Integer conAge = con.Birthdate.daysBetween(Date.Today())/365;
    
    if (conAge > 35){
    ...
    }
    
    let's abstract the business logic to isAgeGreater(35) method: cleaner and reusable for any age comparison. 
    
    Contact con = [Select Birthdate from Contact][0];
    Obj.IContact icontact = new Obj.IContact(con);
    if (icontact.isAgeGreater(35)){
    ...
    }
    

    Define an Obj class that will have inner classes for all the standard objects that needs extra business logic.
    1. Constructor for 1 object
    2. Constructor for List of objects
    3. Methods with business logic getAge, hasMobileNumber, add more if needed.
    4. Sorting by any field by assigning SORT_BY
    5. Sorting by any direction by assigning SORT_DIRECTION (DESC, ASC)
    6. Checking if two objects are equals
    7. Serialize object
    8. Deserialize object

    public class Obj {
         
        public static String NAME_SORT = 'Name';
        public static String EMAIL_SORT = 'Email';
        public static String SORT_BY = NAME_SORT;
        
        public static String SORT_DESC = 'DESC';
        public static String SORT_ASC = 'ASC';
        public static String SORT_DIRECTION = SORT_ASC;
        
        public class IContact implements Comparable{
    
            private Contact con;
            private List<Contact> cons;
            private List<IContact> conLst;
            
            public IContact(List<Contact> cons){
                this.cons = cons;
                this.conLst = new List<IContact>();
                for (Contact con : cons){
                    conLst.add(new IContact(con));
                }
            }
            
            public IContact(Contact con){
                this.con = con;
            }
            
            public Contact getContact(){
                return con;
            }
            
            public Boolean hasMobile(){
                return con.MobilePhone!=null;
            } 
            
            public Integer getAge(){
                return con.Birthdate!=null ? (con.Birthdate.daysBetween(Date.Today())/365) : 0;
            }
    
            public Boolean isAgeGreater(Integer compareAge){
                Integer contactAge = getAge();
                return contactAge > 0 ? compareAge < contactAge : false;
            }
            
            public List<IContact> getList(){
                return conLst;
            }
            
            public List<IContact> sort(){
               conLst.sort();
               return conLst;
            }
    
            private Integer sortByName(Contact compare){
                Integer compVal = 0;
                 if (compare.Name < con.Name){
                    return SORT_DIRECTION.equals(SORT_ASC) ? 1 : -1;
                } else if (con.Name < compare.Name){
                    return SORT_DIRECTION.equals(SORT_ASC) ? -1 : 1;
                }
                return compVal;
            }
            
             private Integer sortByEmail(Contact compare){
                Integer compVal = 0;
                 if (compare.Email < con.Email){
                    return SORT_DIRECTION.equals(SORT_ASC) ? 1 : -1;
                } else if (con.Email < compare.Email){
                    return SORT_DIRECTION.equals(SORT_ASC) ? -1 : 1;
                }
                return compVal;
            }
            
            public Integer compareTo(Object obj){
                if (SORT_BY.equals(NAME_SORT)){
                    Contact compare = ((IContact)obj).con;
                    return sortByName(compare);
                } else if (SORT_BY.equals(EMAIL_SORT)){
                    Contact compare = ((IContact)obj).con;
                    return sortByEmail(compare);
                }
                return 0;
            } 
            
            public Boolean isEquals(Contact concompare){
                return System.equals(con, concompare);
            }
            
            public Integer getHashCode(){
                return System.hashCode(con);
            }   
            
            public String serialize(){
                return JSON.serialize(this);
            }
            
            public IContact deserialize(String jsonCon){
                return (IContact)JSON.deserialize(jsonCon, IContact.class);
            }
        }   
    }
    

    Testing standard object Wrapper

    List<Contact> contacts = [Select Id, Name, MobilePhone, Birthdate from Contact];
    Obj.SORT_BY = Obj.NAME_SORT;
    Obj.SORT_DIRECTION = Obj.SORT_ASC;
    List<Obj.IContact> icontacts = new Obj.IContact(contacts).sort();
    for (Obj.IContact c : icontacts){
        System.debug(c.hasMobile());
        System.debug(c.getAge());  
        System.debug(c.getHashCode());
        System.debug(c.isEquals(c.getContact()));
    }
    

    Apex Coding Interview Challenge #5

    Given an encoded string, return its decoded string. The encoding rule is: k[encoded_string], where the encoded_string inside the square brackets is being repeated exactly k times. Note that k is guaranteed to be a positive integer.

    s = "3[a]2[bc]", return "aaabcbc"
    s = "3[a2[c]]", return "accaccacc"
    s = "2[abc]3[cd]ef", return "abcabccdcdcdef"
    

    DecodeHelper class

    public class DecodeHelper {
    
        private Integer idx; 
        private String zero;
    
        public String decodeString(String s) {
            zero ='0';
            idx = 0;
            return decodeStringHelper(s);
        }
        
        private String decodeStringHelper(String s) {
         String ans = '';
         Integer repeat = 0;
    
         while (idx < s.length()) {
             Integer ch = s.charAt(idx);
             Integer[] intArr = new Integer[1];
             intArr[0] = ch;
             String chStr = String.fromCharArray(intArr);
             if (chStr == ']') {
                 return ans;
             } else if (chStr == '[') {
                 ++idx;
                 String str = decodeStringHelper(s);
                 while (repeat > 0) {
                     ans+=str;
                     --repeat;
                 }
             } else if (chStr.isNumeric()) {
                 repeat = repeat * 10 + ch - zero.charAt(0);
                 
             } else {
                 ans+=chStr;
             }
             ++idx;
         }
         return ans;
       }
    }
    

    DecodeHelper Test

    @isTest class DecodeHelperTest {
    
        @isTest static void testDecodeString(){
            Test.startTest();
                DecodeHelper dh = new DecodeHelper();
                String decodedString1 = dh.decodeString('3[a]2[bc]');
                System.assertEquals(decodedString1, 'aaabcbc');
                String decodedString2 = dh.decodeString('3[a2[c]]');
                System.assertEquals(decodedString2, 'accaccacc');
                String decodedString3 = dh.decodeString('2[abc]3[cd]ef');
                System.assertEquals(decodedString3, 'abcabccdcdcdef');
            Test.stopTest();
        }
    }
    

    Apex passing parameters to Batch job during scheduling

    Here it the use case:

    You have 1 job that you want to run weekly and monthly but the monthly job also has to generate a notification. You don’t need to create two classes but can pass a parameter to the schedule to know it is a weekly or monthly job.

    The batch Job class that accepts constructor parameter jobRunFrequency
    global class AccountBatchJob implements System.Schedulable, Database.Batchable&lt;SObject&gt;, Database.Stateful, Database.AllowsCallouts {
    
    	private static final String RUN_WEEKLY_JOB = 'RUN_WEEKLY_JOB';
    	//This is the key that needs to be used to generate notifications for Monthly 
    	private static final String RUN_MONTHLY_JOB = 'RUN_MONTHLY_JOB';
    	private String jobRunFrequency;
    
    	String query = 'Select Id, FirstName, LastName from Account';
    
    	public AccountBatchJob(){
    		this.jobRunFrequency = RUN_WEEKLY_JOB;
    	}
    
    	public AccountBatchJob(String jobRunFrequency){
    		this.jobRunFrequency = jobRunFrequency;
    	}
    
            global Database.QueryLocator start(Database.BatchableContext BC) {
    		return Database.getQueryLocator(query);
    	}
    
        global void execute(Database.BatchableContext BC, List&lt;Account&gt; accounts) {
         ....
         if (RUN_MONTHLY_JOB.equalsIgnoreCase(jobRunFrequency)){
         ......
         }
        }
    }
    

    Test class for batch Job passing parameter check if it is the Monthly job

    Test.startTest();
    	Database.executeBatch(new AccountBatchJob('RUN_MONTHLY_JOB'), 1);
    Test.stopTest();
    

    Test class Scheduler Job to check it if is the Monthly job

    Test.startTest();
    	String jobId = System.schedule('AccountBatchJob', '0 0 0 15 3 ? 2022', new AccountBatchJob('RUN_MONTHLY_JOB'));
    	CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, NextFireTime FROM CronTrigger WHERE id = :jobId];
    	System.assertEquals('0 0 0 15 3 ? 2022', ct.CronExpression);
    Test.stopTest();
    

    Apex Cache Tracker to remove session cache

    Here is the problem that needed to be solved:
    1. Use session cache to cache some user information like their profile or user settings
    2. When a external systems update the information and try to remove the session cache as it got updated it fails because it does not have access to a users session cache

    Solution:
    Build a session tracker in Org Cache to track missed removal of session cache

    Step 1: When a session cache remove fails add it to the Org level tracker cache. The format of session keys are the following:
    key + userId (Salesforce userId)

    That way I can keep track of what userId’s I need to remove session cache
    String splitStrKey = key.substring(0, key.length()-18);
    String splitUserId = key.substring(key.length()-18, key.length());

    will split into key and userId

    public Boolean remove(String key) {
          if (!System.isBatch() && !System.isFuture() && !Test.isRunningTest()){
            Boolean removed = false;
            if (sessionCacheEnabled && sessionPartition!=null) {
                  System.debug('inside session cache remove key: ' + key);
                  removed = sessionPartition.remove(key);
                  if (!removed && key!=null && key.length() > 18){
                    String splitStrKey = key.substring(0, key.length()-18);
                    String splitUserId = key.substring(key.length()-18, key.length());
                    System.debug('Session remove failed key ' + splitStrKey + ' for ' + splitUserId + ' adding to tracker');
                    removeFailedAddToTracker(splitStrKey, splitUserId, null);
                  }
              } else if (orgPartition!=null){
                  removed = orgPartition.remove(key);
              }
    
            if (removed) {
              System.debug(LoggingLevel.DEBUG, 'Removed key ' + key);
              return true;
            } else{
              System.debug(LoggingLevel.DEBUG, 'Not removed key not found ' + key);
              return false;
            }
          } else {
            System.debug(LoggingLevel.DEBUG, 'Skipping cache remove in batch ' + key);
            return false;
          }
        }
    

    Step 2: Add session keys to Org level tracker cache (CACHE_USER_CACHE_TRACKER), we will add it as Map<Id, Map> where Map contains UserId as key and Map of all the session keys that needs to be removed

    public void removeFailedAddToTracker(String keyPrefix, Id userId, Set<Id> clientIds){
          if (App_Service.isIntegrationUser(userId)){
            Map<String, String> mapOfContactUserIds = (Map<String, String>)getOrgPartition().get(App_Rest_Constants.CACHE_USER_CONTACT_MAP);
            Map<Id, Map<String, Id>> mapOfExistingCacheQueueTracker = (Map<Id, Map<String, Id>>)getOrgPartition().get(App_Rest_Constants.CACHE_USER_CACHE_TRACKER);
            if (clientIds!=null && !clientIds.isEmpty()){
              for (Id clientId : clientIds){
                if (mapOfContactUserIds!=null && mapOfContactUserIds.containsKey(clientId)){
                  Id userIdToClearCache = mapOfContactUserIds.get(clientId);
    
                  if (mapOfExistingCacheQueueTracker!=null){
                    if (mapOfExistingCacheQueueTracker.containsKey(userIdToClearCache)){
                      Map<String, Id> existingCacheStrings = mapOfExistingCacheQueueTracker.get(userIdToClearCache);
                      existingCacheStrings.put(keyPrefix, userIdToClearCache);
                      mapOfExistingCacheQueueTracker.put(userIdToClearCache, existingCacheStrings);
                    } else {
                      mapOfExistingCacheQueueTracker.put(userIdToClearCache, new Map<String, Id>{keyPrefix=>userIdToClearCache});
                    }
                  } else {
                    mapOfExistingCacheQueueTracker = new Map<Id, Map<String, Id>>();
                    mapOfExistingCacheQueueTracker.put(userIdToClearCache, new Map<String, Id>{keyPrefix=>userIdToClearCache});
                  }
                }
              }
            } else {
              if (mapOfExistingCacheQueueTracker!=null && mapOfExistingCacheQueueTracker.containsKey(userId)){
                Map<String, Id> existingMap = mapOfExistingCacheQueueTracker.get(userId);
                existingMap.put(keyPrefix, userId);
                mapOfExistingCacheQueueTracker.put(userId, existingMap);
              } else {
                if (mapOfExistingCacheQueueTracker==null){
                  mapOfExistingCacheQueueTracker = new Map<Id, Map<String, Id>>();
                }
                mapOfExistingCacheQueueTracker.put(userId, new Map<String, Id>{keyPrefix=>userId});
              }
            }
    
            if (mapOfExistingCacheQueueTracker!=null)
              getOrgPartition().put(App_Constants.CACHE_USER_CACHE_TRACKER, mapOfExistingCacheQueueTracker);
          }
        }
    

    Step 3: Every time before we check if session cache contains a key we will see if the same key is contained in the CACHE_USER_CACHE_TRACKER cache. If yes remove it from the users session cache so that cache can be removed and new query can be done

      public Boolean containsKey(String cacheKey){
          Boolean containsKey = false;
          if (!System.isBatch() && !System.isFuture() && !Test.isRunningTest()){
               if (sessionCacheEnabled==false && orgPartition!=null){
                  if (orgPartition.get(cacheKey)!=null){
                    containsKey = true;
                  }
              } else if (sessionCacheEnabled && sessionPartition!=null) {
                  if (sessionPartition.get(cacheKey)!=null){
                    containsKey = true;
    
                    Map<Id, Map<String, Id>> mapOfExistingCacheQueueTracker = (Map<Id, Map<String, Id>>)getOrgPartition().get(App_Rest_Constants.CACHE_USER_CACHE_TRACKER);
                  	if (mapOfExistingCacheQueueTracker!=null && mapOfExistingCacheQueueTracker.containsKey(UserInfo.getUserId())){
                			Map<String, Id> flagsToClear = mapOfExistingCacheQueueTracker.get(UserInfo.getUserId());
    
                      Boolean removeFlag = false;
                			for (String flagToClear : flagsToClear.keySet()){
                				if (flagToClear.equalsIgnoreCase(cacheKey)){
                          String keyToRemove = flagToClear + flagsToClear.get(flagToClear);
                					Boolean removeItemFromCache = getSessionPartition().remove(keyToRemove);
                          
                          removeFlag = true;
                          containsKey = false;
                				}
                			}
    
                      if (removeFlag){
                        for (String flagToClear : flagsToClear.keySet()){
                          flagsToClear.remove(cacheKey);
                          if (flagsToClear.isEmpty() && flagsToClear.size()==0){
                            mapOfExistingCacheQueueTracker.remove(UserInfo.getUserId());
                          } else {
                            mapOfExistingCacheQueueTracker.put(UserInfo.getUserId(), flagsToClear);
                          }
                        }
                        
                        getOrgPartition().put(App_Rest_Constants.CACHE_USER_CACHE_TRACKER, mapOfExistingCacheQueueTracker);
                      }
    
                		}
                  }
              }
           }
          return containsKey;
        }
    

    Apex Coding Challenge Find Highest Frequency of Numbers

    Problem: Find the number that has the highest frequency in a list of integers.

    Input: 1,6,2,1,6,1

    Output: 1 //because 1 occurs 3 times in the list

    Option 1: Use Hashmap to iterate list

    	List<Integer> nums = new List<Integer>{1,6,2,1,6,1};
    		Map<Integer, Integer> numMap = new HashMap<>();
    		for (Integer num : nums){
    			if (numMap.containsKey(num)){
    				Integer numFreq = numMap.get(num);
    				numMap.put(num, numFreq+1);
    			} else {
    				numMap.put(num, 1);
    			}
    		}
    		
    		Integer biggestFreq = 0;
    		Integer biggestVal = 0;
    		for (Integer num : numMap.keySet()){
    			if (numMap.get(num) > biggestFreq){
    				biggestFreq = numMap.get(num);
    				biggestVal = num;
    			}
    		}
    		
    		System.debug(biggestVal);
    

    Option 2: Use wrapper class with compare to sort wrapper

    List<Integer> nums = new List<Integer>{1,6,2,1,6,1};
    
    Map<Integer, NumFrequencyWrapper> numMap = new HashMap<>();
    for (Integer num : nums){
    	if (numMap.containsKey(num)){
    		NumFrequencyWrapper numFreqWrapper = numMap.get(num);
    		numFreqWrapper.setFrequency(numFreqWrapper.getFrequency()+1);
    		numMap.put(num, numFreqWrapper);
    	} else {
    		NumFrequencyWrapper numFrequencyWrapper = new NumFrequencyWrapper();
    		numFrequencyWrapper.setNum(num);
    		numFrequencyWrapper.setFrequency(1);
    		numMap.put(num, numFrequencyWrapper);
    	}
    }
    	
    List<NumFrequencyWrapper> frequencyWrapperList = new List(numMap.values());
    Collections.sort(frequencyWrapperList, new Untitled.NumFrequencyWrapperCompare());
    System.debug(frequencyWrapperList.get(0).getNum());
    
    public class NumFrequencyWrapper {
    	private Integer num;
    	private Integer frequency;
    	
    	public void setNum(Integer num){
    		this.num = num;
    	}
    	
    	public Integer getNum(){
    		return num;
    	}
    	
    	public void setFrequency(Integer frequency){
    		this.frequency = frequency;
    	}
    	
    	public Integer getFrequency(){
    		return this.frequency;
    	}
    }
    	
    public class NumFrequencyWrapperCompare implements Comparator<NumFrequencyWrapper>{
    	public int compare(NumFrequencyWrapper a, NumFrequencyWrapper b) { 
    		return  b.getFrequency() - a.getFrequency(); 
    	} 
    }  
    

    Option 3: Using buckets to group index of frequencies together

    List<Integer> nums = new List<Integer>{1,6,2,1,6,1};
    Integer returnNums = 2;
    Map<Integer, Integer> numMap = new Map<Integer, Integer>();
    for (Integer num : nums){
    	if (numMap.containsKey(num)){
    		Integer numFreq = numMap.get(num);
    		numMap.put(num, numFreq+1);
    	} else {
    		numMap.put(num, 1);
    	}
    }
    
    Map<Integer, List<Integer>> mapOfBucketWithValues = new Map<Integer, List<Integer>>();
    for (Integer num : numMap.keySet()){
    	Integer numFrequency = numMap.get(num);
    	if (mapOfBucketWithValues.containsKey(numFrequency)){
    		List<Integer> existingIndexNum = mapOfBucketWithValues.get(numFrequency);
    		existingIndexNum.add(num);
    		mapOfBucketWithValues.put(numFrequency, existingIndexNum);
    	} else {
    		List<Integer> numList = new ArrayList<>();
    		numList.add(num);
    		mapOfBucketWithValues.put(numFrequency, numList);
    	}
    }
    
    for (Integer k=nums.size(), returnedNums=0; 1<=k; k--){
    	if (mapOfBucketWithValues.containsKey(k)){
    		for (Integer numBucket : mapOfBucketWithValues.get(k)){
    			if (returnedNums < returnNums){
    				System.debug(numBucket);
    				returnedNums++;
    			}
    		}
    	}	
    }
    

    Apex Clear all fields for a SObject record

    The clearOutRecords would iterate all the fields passed as the currentRecord, then:
    1. Exclude the fields as part of the fieldsToExcludeForClearOut Set and relationship fields
    2. Check if the field is not null and updateable
    3. Special logic to set fields to predefined values
    4. Set all other fields to null
    5. Return the SObject with fields as null

    
    private static Set<String> fieldsToExcludeForClearOut = new Set<String>{'Cases', 'DoNotCall', 
    'HasOptedOutOfFax', 'HasOptedOutOfEmail', 'LastName', 
    'FirstName', 'Email', 'AccountId', 'CreatedDate',
    'IsDeleted','Interval__c','OwnerId',
    'OtherGeocodeAccuracy','MailingGeocodeAccuracy',
    'BillingGeocodeAccuracy','ShippingGeocodeAccuracy'};
    
        public SObject clearOutRecords(SObject currentRecord, String sObjectName){
          SObjectType objToken = Schema.getGlobalDescribe().get(sObjectName);
          DescribeSObjectResult objDef = objToken.getDescribe();
          Map<String, SObjectField> fieldsSobject = objDef.fields.getMap();
          Map<String, Object> fields = currentRecord.getPopulatedFieldsAsMap();
          Type classType = Type.forName(sObjectName);
          SObject mergedRecord = (SObject)JSON.deserialize('{}', classType);
          for (String field : fields.keySet()){
            if (!fieldsToExcludeForClearOut.contains(field) && !field.contains('__r')){
              if (currentRecord.get(field)!=null && fieldsSobject.get(field).getDescribe().isUpdateable()){
                if ('User_Status__c'.equals(field)){
                  mergedRecord.put(field, 'Incomplete');
                } else if ('Is_Mail_Same_As_Home__c'.equals(field)){
                  mergedRecord.put(field, false);
                } else {
                  mergedRecord.put(field, null);
                }
                } else if ('Id'.equals(field)){
                mergedRecord.put(field, currentRecord.get(field));
              }
            }
          }
          return mergedRecord;
        }
    

    Initializing the clearOutRecords method

    1. Query the fields that you would like to clear
    2. Pass the Object to the clearOutRecords method

    Contact queryContact = [Select Id, FirstName, LastName, Email, Birthdate, MailingState, Age__c from Contact where MailingState!=null limit 1 ];
    
    Contact clearedOutContact = (Contact)App_Service.instance.clearOutRecords(queryContact, 'Contact');
            
    

    Output

    Contact:{Id=0036300000TQZIwAAP, Birthdate=null, MailingState=null}
    

    Apex remove sensitive data from json

    When you need to remove sensitive data from json before logging the following method will remove a predefined list of keywords

    String bodyArgs = '{"name":"test", "ssn":"324234234", "email":"test@mail.com"}';
    Object bodyObj = (Object)JSON.deserializeUntyped(bodyArgs);
    
    Map<String, Object> mapObj = new Map<String, Object>();
    if (bodyObj instanceof List<Object>){
    	List<Object> lstObjs = (List<Object>)JSON.deserializeUntyped(bodyArgs);
        for (Object lstObj : lstObjs){
           Map<String,Object> parseLstObj = (Map<String,Object>)JSON.deserializeUntyped(JSON.serialize(lstObj));
           mapObj.putAll(parseLstObj);
        }
    } else {
    	mapObj = (Map<String,Object>)JSON.deserializeUntyped(bodyArgs);
    }
    
    Map<String, String> newMappedValues = new Map<String, String>
    System.debug(removeAttributes(mapObj, newMappedValues));
    >>> Output: '{"name":"test"}'
    

    removeAttributes method will iterate all the keys in the payload and remove the sensitive keys from the payload

    private Set<String> removeSensitiveKeyValue = new Set<String>{'ssn', 'email', 'dob'};
    
    public Map<String, String> removeAttributes(Map<String,Object> jsonObj, Map<String, String> mappedKeys)  {
    	for(String key : jsonObj.keySet()) {
    		if (removeSensitiveKeyValue.contains(key)){
    			jsonObj.remove(key);
    		} else {
    	      if(jsonObj.get(key) instanceof Map<String,Object>) {
    	          removeAttributes((Map<String,Object>)jsonObj.get(key), mappedKeys);
    	      } else if(jsonObj.get(key) instanceof List<Object>) {
    	          for(Object listItem : (List<Object>)jsonObj.get(key)) {
    	           if(listItem instanceof Map<String,Object>)  {
    	        	removeAttributes((Map<String,Object>)listItem, mappedKeys);
    	           }
    	         }
    	      } else {
    			mappedKeys.put(key, String.valueOf(jsonObj.get(key)));
    		  }
    	  	}
    	}
    	return mappedKeys;
    }