Salesforce System Integration Interview Prep

Remote Process Invocation – Request and Reply

Best solutions

  1. External Services – invokes a REST API call – allows to invoke externally hosted service in declarative manner. External REST service are available in a an OpenAPI or Integrant schema definition. Contains primitive data types, nested objects not supported. Transaction invoked from Lightning Flow. Integration transaction doesn’t risk exceeding the synchronous Apex governor limits.
  2. Salesforce Lighting – consume WSDL and generate Apex proxy. Enable HTTP (REST) services, GET, PUT, POST, DELETE methods.
  3. Custom Visualforce page or button initiates Apex HTTP callout – user initiated action calls and Apex action that executed this proxy Apex class

Suboptimal

  1. Trigger – calls must be asynchronous (@future)
  2. Batch job callout – bundle responses together and make 1 callout for every 200 records processed.

Endpoint capability

Endpoint should be able to receive a web services call via HTTP. Salesforce must be able to access endpoint via the internet, curl or postman. Apex SOAP callout- WSDL 1.1, SOAP 1.1

Apex HTTP callout -REST service using standard GET, POST, PUT, DELETE methods

Data Volumes

Small volume, real time activities, due to small timeout values and maximum size of the request or response from Apex call solution.

Timeliness

  1. Request is typically invoked from user interface, must not keep user waiting
  2. Governor limit of 120 second timeout for callouts
  3. Remote process needs to be completed in SF limit and user expectations else seen as slow system
  4. Apex synchronous apex limits are 10 transaction than run for 5 seconds
    • Make sure external server callout is less than 5 seconds

State management

  1. Salesforce stores the external system External Id for specific record
  2. The remote system stores the Salesforce unique record ID or other unique key

Security

Any call to external service should maintain:

  1. Confidentiality
  2. Integrity
  3. Availability

Apex SOAP and HTTP callouts security considerations

  1. One way SSL is enabled by default
  2. 2 way SSL is enabled by self-signed certificate or CA-signed certificate
  3. WS-Security is not supported
  4. If necessary use one way hash or digital signature using Apex Crypto to ensure message integrity
  5. Remote system must be protected by appropriate firewall mechanism

Error Handling – error occurs in form of error http error codes

400 – Bad request, 401 – Unauthorized, 404 – Not Found, 500 – Internal server error

Recovery – changes not committed to Salesforce until successful response, retry

x amount of times until success received else log error.

Idempotent Design consideration – duplicate calls when something goes wrong needs

need to have a message ID to make sure duplicates are not created

Http h = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint(url); //
req.setMethod('GET'); //

//Basic Authentication - specify in Named Credentials so no code change needed
String username = 'myname';
String password = 'mypwd';

Blob headerValue = Blob.valueOf(username + ':' + password);
String authorizationHeader = 'Basic ' +
EncodingUtil.base64Encode(headerValue);
req.setHeader('Authorization', authorizationHeader);

HttpResponse response = h.send(req);

if (response.getStatusCode() == 200) {
    Map<String, Object> results = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
    List<Object> animals = (List<Object>) results.get('animals');
    System.debug('Received the following animals:');
    for (Object animal: animals) {
        System.debug(animal);
    }
}

//Implement httpCalloutMock
global class YourHttpCalloutMockImpl implements HttpCalloutMock {
    global HTTPResponse respond(HTTPRequest req) {
    

Test.setMock(HttpCalloutMock.class, new YourHttpCalloutMockImpl());

Remote Process Invocation – Fire and forget

Best solutions

  1. Process-driven platform events – using process builder/workflow rules to create process to publish insert or update event
  2. Event messages – the process of communicating changes and responding to them without writing complex logic. One or more subscribers can listen to the same event and carry out actions through CometD.
  3. Customized-driven platform events – events are created by Apex triggers or batch classes
  4. Workflow driven outbound messaging – remote process is invoked from an insert or update event. Salesforce provides workflow-driven outbound messaging capability that allow sending SOAP messages to remote systems.
  5. Outbound messaging and callbacks – callback helps mitigate out-of-sequence messaging also 2 things Idempotency and Retrieve of more data. When creating a new record and update externalId with the recordId from external system.

Suboptimal

  1. Lightning components – user interface based scenarios, must guarantee delivery of message in code
  2. Triggers – Apex triggers to perform automation based on records changes
  3. Batch jobs – calls to remote system can be performed from a batch job. Solution allows batch remote process execution and for processing of reposes from remote system.
MyEventObject__e newEvent = new MyEventObject__e();
newEvent.Name__c = 'Test Platform Event';
newEvent.Description__c = 'This is a test message';
List<Database.SaveResult> results = EventBus.publish(newEvent);

	
Test.startTest();
// Create test events
// ...
// Publish test events with EventBus.publish()
// ...
// Deliver test events
Test.getEventBus().deliver();
// Perform validation 
// ...
Test.stopTest();


//Listener - EMPConnector use login to and create EmpConnector(bayeuxParameters)
import static com.salesforce.emp.connector.LoginHelper.login;
//create consumer
SalesforceEventPayload eventPayload = new SalesforceEventPayload();
	Consumer<Map<String, Object>> consumer = event -> {
			eventPayload.setPayload(event);
			itsReqHandler.handleRequest(eventPayload.getPayload());
};

//add subscription to event
subscription = empConnector.subscribe("/event/" + EVENT_NAME, replayFrom, consumer).get(WAIT_TIME, TimeUnit.SECONDS);

Error handling and recovery

Error handling – because this pattern is asynchronous the callout of event publish handles need to be handles as well as the remote system needs to handle queuing, processing and error handling.

Recovery

More complex, need some retry strategy if no retry is receive in the QoS (Quality of Service) time period

Idempotent Design considerations

Platform events are published once, there is no retry on the Salesforce side. It is up to the ESB to request that the events be replayed. In a replay, the platform events reply ID remains the same and ESB can try to duplicate messages based on reply ID.

Unique Id for outbound messages is sent and needs to be tracked by the remote system to make sure duplicate messages or events are not repossessed.

Security

  1. Platform events – conforms to the security of the existing Salesforce org
  2. Outbound messages – One way SSL enabled, two way SSL can be used with outbound message certificate. Whitelist the IP ranges of the remote integration servers, remote server needs appropriate firewalls
  3. Apex callout – one way SSL enabled, 2 way SSL through self signed or CA signed certificates. Use one way hash or digital signature (Apex Crypto class) to ensure integrity of message. Remote system protected by appropriate firewall mechanisms.

Timeliness

Less important, control handed back to client immediate or after successful deliver message. Outbound message acknowledgment must occur in 24 hours (can be extended to seven days) otherwise message expires.

Platform events – send events to event bus and doesn’t wait for confirmation or acknowledgement from subscriber. If subscribe doesn’t pick up the message they can reply the event using replayID. High volume message are stored for 72 hours (3 days). Subscriber use CometD to subscribe to channel.

State management

Unique record identifiers are important for ongoing state tracking:

  1. Salesforce store remote system primary or unique surrogate key for remote record
  2. Remote system store Salesforce unique record ID or some unique surrogate key

Governor Limits

Limits depends on the type of outbound call and timing of the call

Reliable Messaging

  1. Platform Events – form of reliable messaging. Salesforce pushed the event to subscribers. If the message doesn’t gets picked up it can be replayed using reply ID.
  2. Apex callout – recommend that the remote system implement JMS, MQ however it doesn’t guarantee delivery to remote system, specific techniques such as processing positive acknowledgement from remote endpoint in addition to custom retry logic must be implemented.
  3. Outbound messaging – if no positive acknowledgment receive, retry up to 24 hours. Retry interval increase exponentially 15 sec to 60 min intervals.

Publisher and subscriber not in same transaction

Publish event before committed to the database, subscriber receives the event and does lookup to not find the record.

Publish behavior – publish immediately/publish after commit

Publish events – @future or @queueable to callout to events only when commit has complete

Message sequencing

Remote system discard message with duplicate message ID

Salesforce send RecordId, remote system makes callback to Salesforce

Handing Deletes

Salesforce workflow can’t track deletion of records, can’t call outbound message for deletion. Workaround:

  1. Create custom object called Deleted_Records__c
  2. Create trigger to store info (unique identifier) in custom object
  3. Implement workflow rule to initiate message based on the creation of custom object

Batch Data Synchronization

Best solutions

  1. Salesforce change data capture (Salesforce master)
  2. Replication via third party ETL Tool (Remote system master) – Bulk API
  3. Replication via third party ETL Tool (Salesforce master) – SOAP API getUpdated()

Suboptimal

  1. Remote call-in – call into SF, causes lots of traffic, error handling, locking
  2. Remote process invocation – call to remote system, causes traffic, error handing, locking

Extract and transform accounts, contacts, opportunities from current CRM to Salesforce (initial data load import)

Extract, transform, load billing data into Salesforce from remote system on weekly basis (ongoing)

Data master

Salesforce or Remote system

 

Salesforce Change Data Capture

Publish insert, update, delete, undelete events which represents changes to Salesforce. Receive near real time changes of records and sync to external data store.

Takes care of continuous synchronization part, needs integration app to receive events and perform update to external system

Channel – /data/{objectName}_Change

Error handling – Pattern is async remote system should handle message queuing, processing and error handling.

Recovery – initiate retry based on service quality of service requirement. Use replyID to reply stream of events. Use CometD to retrieve past messages up to 72 hours.

Bulk API – Replication via 3rd party ETL tool (more than 100 000 records)

Allow to run change data capture against source data. Tool reacts to change in the source data set, transforms the data and them call Salesforce Bulk API to issue DML statement, can also use SOAP API.

  1. Read control table to determine last time job ran, other control values needed
  2. Use above control values to filter query
  3. Apply predefine processing rules, validation rules, enrichments
  4. Use available connectors/transformation capability to create destination data set
  5. Write the data to Salesforce objects
  6. If processing is success update the control variable
  7. If process fail update control variable for process to restart

Consider

  1. Chain and sequence ETL jobs to provide cohesive process
  2. Use primary key for both systems to match incoming data
  3. Use specific API methods to extract only updated data
  4. Importing master-detail or lookup, consider using the parent key at the source to avoid locking. Group contacts for an account to be imported at the same time.
  5. Post-import processing should only process data selectively
  6. Disable Apex triggers, workflow and validation rules
  7. Use the defer calculations permission to defer sharing calculations until all data loaded

Error handling

If error occur during read operation, retry for errors. If errors repeat implement control tables/error tables in context of:

  1. Log the error
  2. Retry the read operation
  3. Terminate if successful
  4. Send a notification

Security

  1. Lightning Platform license with at least “API Only” user permission
  2. Standard encryption to keep password access secure
  3. Use HTTPS protocol

Timeliness

Take care to design the interface so all batch processes complete in a designated batch window

Loading batches during business hours not recommended

Global operations should run all batch processes at the same time

State management

Use surrogate key between two systems.

Standard optimistic locking occurs on platform and any updates made using the API require the user who is editing the record to initiate a refresh and initiate their transaction. Optimistic locking means:

  1. Salesforce doesn’t maintain state of record being edited
  2. Upon read, records time when data was extracted
  3. If user updated the record and before save checks if another user has updated
  4. System notified user update was made and use retrieve the latest version before updating

Middleware capabilities

Middleware tool that supports Bulk API

Supports the getUpdated() function – provide closest implementation to standard change data capture capability in Salesforce

Extracting data

Use the getUpdated() and getDeleted() SOAP API to sync an external system with Salesforce at intervals greater than 5 minutes. Use outbound messaging for more frequent syncing.

When querying can return more than an million results, consider the query capability of the Bulk API.

Remote Call-In

Best solutions

  1. SOAP API – Publish events, query data, CRUD. Synchronous API, waits until it receives a response. Generated WSDL – enterprise (strongly-typed), partner (loosely typed). Must have a valid login and obtain session to perform calls. Allows partial success if the records are marked with errors, also allows “all or nothing” behavior.
  2. REST API – Publish events, query data, CRUD. Synchronous API, waits until it receives a response. Lightweight and provides simple method for interacting with Salesforce. Must have a valid login and obtain session to perform calls. Allows partial success if the records are marked with errors, also allows “all or nothing” behavior. Output of one to use as input to next call.

Suboptimal

  1. Apex Web services – use when: full transaction support is required, custom logic needs to be applied before commenting.
  2. Apex REST services – lightweight implementation of REST services
  3. Bulk API – submitting a number of batches to query, update, upsert or delete large number of records.

Authentication

Salesforce supports SSL (Security Socket Layer) and TLS (Transport Later Security), Ciphers must be at least length 128 bits

Remote system has to authenticate before accessing any REST service. Remote system can use OAuth 2.0 or username/password. Client has to set the authorization HTTP header with the appropriate value.

Recommend client caches the session ID rather than creating a new session ID for every call.

Accessibility

Salesforce provides a REST API that remote system can use to:

  1. Query data in org
  2. Publish events to org
  3. CRUD of data
  4. Metadata

Synchronous API

After call to server it waits for a response, asynchronous call to Salesforce is not supported

REST vs SOAP

REST exposes resources as URI and uses HTTP verbs to define CRUD operations. Unlike SOAP, REST required no predefined contract, utilize XML and JSON for responses, and has loosely typed. Advantage includes ease of integration and great use for mobile and web apps.

Security

Client executing the REST needs a valid Salesforce login and obtain a access token. API respects the object and field level security for the logged in user

Transaction/Commit Behavior

By default every record is treated as a separate transaction and committed separately. Failure of one records does not cause rollback of other changes. Using the composite API makes a series of updates in one call.

Rest composite resource

Perform multiple operation in a single API call. Also use output of one call to be input of next call. All response bodies and HTTP statuses are returned in a single response body. The entire requires counts as single call towards API limit.

Bulk API

For bulk operations use Rest-based BULK API

Event driven architecture

Platform events are defined the same way as you define a Salesforce object. Publishing to event bus is same as inserting Salesforce record. Only create and insert is supported.

Error handling

All remote call-in methods or custom API require remote system to handle any subsequent errors such as timeouts and retries. Middleware can be used to provide logic for recovery and error handling

Recovery

A custom retry mechanism needs to be created if QoS requirements dictate it. Important to consider impotent design characteristic.

Timeliness

Session timeout – session timeout when no activity based on SF org session timeout

Query timeout – each query has a timeout limit of 120 seconds

Data volumes

CRUD – 200 records per time

Blob size – 2GB ContentVersion (Chatter)

Query – query(), queryMore() return 500 records, max 2000

State management

Salesforce stores remote system primary key or unique key for the remote record

The remote system stores the Salesforce ID unique record ID or some unique

Governor limits

5000 API calls per 24 hour

10 query cursors open at time

Reliable messaging

Resolve the issue that delivery of a message to remote system where the individual components may be unreliable. SOAP API and REST API are synchronous and don’t provide explicit support for any reliable messaging protocols.

Data visualization

Best solutions

  1. Salesforce Connect

Suboptimal

  1. 1. Request and reply – Salesforce web services APIs (SOAP or REST) to make ad-hoc data requests to access and update external system data

Access data from external sources along with Salesforce data. Pull data from legacy systems, SAP, Microsoft, Oracle in real time without making a copy of the data.

Salesforce connect maps data tables in external systems to external objects in your org. External objects are similar to custom objects, except they map to data located outside SF org. Uses live connection to external data to keep external objects up to date.

Salesforce connects lets you

  1. Query data in a external system
  2. CRUD data in external system
  3. Define relationships between external objects and standard or custom objects
  4. Enable Chatter feed on external object page for collaboration
  5. Run reports on external data

Salesforce Connect Adapters

OData adapter 2.0 or OData adapter 4.0connects data to exposed by any OData 2.0 or OData 4.0 producer

Cross-org adapter – connects to data that’s stored in another Salesforce org. Used the Lightning Platform REST API

Custom adapter created via Apex – develop own adapter with the Apex Connector framework

Calling mechanism

External Objects – maps SF external objects to data tables in external systems. Connect access the data on demand and in real time. Provides seamless integration with Lightning Platform can do global search, lookup relationships, record feeds.

Also available to Apex, SOSL, SOQL queries, Salesforce API, Metadata API, change sets and packages.

Error handling

Run Salesforce Connector Validator tool to run some common queries and notice error types and failure causes

Benefits

  1. Doesn’t consume data storage in SF
  2. Don’t have to worry about regular sync between systems
  3. Declarative setup can be setup quickly
  4. Users can access external data with same functionality
  5. Ability to do federated search
  6. Ability to run reports

Considerations

Impact reports performance

Security considerations

Adhere to Salesforce org-level security, use HTTPS connect to any remote system.

OData understand behaviors, limitations and recommendations for CSRF (Cross-Site Request Forgery)

Timeliness

Request invoked by user interface, should not keep the user waiting

May take long to relieve data from external system, SF configured 120 sec maximum timeout

Completion of remote process should execute in timely manner

Data volumes

Use mainly for small volume, real time activities, due to small timeout and maximum size of request or response for Apex call solution.

State management

Salesforce stores primary or unique surrogate key for the remote record

Remote system store SF unique record ID or other unique surrogate key

Salesforce Platform Events, Change Data Capture, Real-Time Event Monitoring explained

Below details the different Salesforce events types for different use cases

1. Change Data Capture

Select the entities that generate change event notifications. Change Data Capture sends notifications for created, updated, deleted, and undeleted records. All custom objects and a subset of standard objects are supported.

/data/AccountChangeEvent – any changes to accounts

  • Create account
 INFO  c.g.g.s.s.SalesforcePlatformEventHandler - {"LastModifiedDate":"2020-05-11T19:32:35.000Z","CleanStatus":"Pending","OwnerId":"0054P000009tuVoQAI","CreatedById":"0054P000009tuVoQAI","Match_Billing_Address__c":false,"ChangeEventHeader":{"commitNumber":10834564859700,"commitUser":"0054P000009tuVoQAI","sequenceNumber":1,"entityName":"Account","changeType":"CREATE","changedFields":[],"changeOrigin":"","transactionKey":"00031938-a4a4-1f2e-d772-c2527e20cff6","commitTimestamp":1589225555000,"recordIds":["0014P00002lJ0ltQAC"]},"CreatedDate":"2020-05-11T19:32:35.000Z","LastModifiedById":"0054P000009tuVoQAI","Name":"Account Event","Total_Assets__c":0.0}
  • Update account name
c.g.g.s.s.SalesforcePlatformEventHandler - {"LastModifiedDate":"2020-05-11T19:33:42.000Z","ChangeEventHeader":{"commitNumber":10834565354322,"commitUser":"0054P000009tuVoQAI","sequenceNumber":1,"entityName":"Account","changeType":"UPDATE","changedFields":["Name","LastModifiedDate"],"changeOrigin":"","transactionKey":"000320f7-ff66-5c6d-e66d-540dc1dba750","commitTimestamp":1589225622000,"recordIds":["0014P00002lJ0ltQAC"]},"Name":"Account Event123"}
  • Delete account
INFO  c.g.g.s.s.SalesforcePlatformEventHandler - {"ChangeEventHeader":{"commitNumber":10834566118523,"commitUser":"0054P000009tuVoQAI","sequenceNumber":1,"entityName":"Account","changeType":"DELETE","changedFields":[],"changeOrigin":"","transactionKey":"000273af-397f-c5ce-b95a-412eb6071ee2","commitTimestamp":1589225728000,"recordIds":["0014P00002lJ0ltQAC"]}}

2. Real-Time Event Monitoring

Real-Time Event Monitoring helps you monitor and detect standard events in Salesforce in near real-time. You can store the event data for auditing or reporting purposes.

/event/LoginEventStream – LoginEventStream tracks login activity of users who log in to Salesforce. This object is available in API version 46.0 and later.

c.g.g.s.s.SalesforcePlatformEventHandler - {"EventDate":"2020-05-11T22:25:59.000Z","CountryIso":"US","Platform":"Unknown","EvaluationTime":0.0,"CipherSuite":"ECDHE-RSA-AES256-GCM-SHA384","PostalCode":"20149","ClientVersion":"N/A","LoginGeoId":"04F4P00008ybrGk","LoginUrl":"login.salesforce.com","LoginHistoryId":"0Ya4P0000DF17laSQB","CreatedById":"0054P000009y5URQAY","ApiType":"N/A","LoginType":"Remote Access 2.0","Status":"Success","AdditionalInfo":"{}","ApiVersion":"N/A","EventIdentifier":"46f6deba-e3de-4b07-b294-3ed56736da36","LoginLatitude":39.0481,"City":"Ashburn","Subdivision":"Virginia","NetworkId":"000000000000000","SourceIp":"34.230.66.105","Username":"blah@gmail.com","UserId":"0054P000009tuVoQAI","CreatedDate":"2020-05-11T22:26:05.711Z","Country":"United States","LoginLongitude":-77.4728,"TlsProtocol":"TLS 1.2","LoginKey":"LlUdqQsfSrgiRTDY","Application":"Event App","UserType":"Standard","HttpMethod":"POST","SessionLevel":"STANDARD","Browser":"Unknown"}

/event/ApiEventStream – Tracks these user-initiated read-only API calls: query(), queryMore(), and count()

[HttpClient@771a660-23] INFO  c.g.g.s.s.SalesforcePlatformEventHandler - {"EventDate":"2020-05-11T22:28:34.000Z","Platform":"iOS/Mac","Query":"select AccountNumber, AccountSource, Active__c, AnnualRevenue, BillingAddress, BillingGeocodeAccuracy, ChannelProgramLevelName, ChannelProgramName, CleanStatus, CreatedById, CreatedDate, CustomerPriority__c, DandbCompanyId, Description, DunsNumber, Fax, Id, Industry, IsCustomerPortal, IsDeleted, IsPartner, Jigsaw, JigsawCompanyId, LastActivityDate, LastModifiedById, LastModifiedDate, LastReferencedDate, LastViewedDate, MasterRecordId, Match_Billing_Address__c, NaicsCode, NaicsDesc, Name, Number_of_Contacts__c, NumberOfEmployees, NumberofLocations__c, OwnerId, Ownership, Ownership_Check__c, ParentId, Phone, PhotoUrl, Rating, ShippingAddress, ShippingGeocodeAccuracy, Sic, SicDesc, Site, SLA__c, SLAExpirationDate__c, SLASerialNumber__c, SystemModstamp, TickerSymbol, Total_Assets__c, Tradestyle, Type, UpsellOpportunity__c, Website, YearStarted from Account limit 1","EvaluationTime":0.0,"ElapsedTime":51,"Operation":"Query","LoginHistoryId":"0Ya4P0000DF17dtSQB","CreatedById":"0054P000009y5URQAY","SessionKey":"do1y6L0Hc/MNQPEU","ApiType":"SOAP Partner","UserAgent":"SoqlXplorer/3.3 CFNetwork/902.6 Darwin/17.7.0 (x86_64)","Client":"SoqlXplorer/3.3","Records":"{\"totalSize\":1,\"done\":true,\"records\":[{\"attributes\":{\"type\":\"Account\"},\"Id\":\"0014P00002gqPpcQAE\"}]}","AdditionalInfo":"{}","ApiVersion":47.0,"EventIdentifier":"f91c687b-91ab-4c11-8235-d4db95eeb7cc","RowsProcessed":1.0,"SourceIp":"69.181.110.193","Username":"thysmichels@gmail.com","UserId":"0054P000009tuVoQAI","CreatedDate":"2020-05-11T22:28:38.217Z","LoginKey":"PsOEa5SkbjVf4QBC","Application":"N/A","QueriedEntities":"Account","SessionLevel":"STANDARD"}

3. Custom Platform Events

/event/ESBBus__e – custom event bus messages published via Apex: EventBus.publish()

[HttpClient@51c668e3-20] INFO  c.g.g.s.s.SalesforcePlatformEventHandler - {"Name__c":"Test","CreatedById":"0054P000009tuVoQAI","CreatedDate":"2020-05-12T02:39:33.955Z"}

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:

    SFDC99 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 Max Cases Per Account Trigger

    Fun coding challenge from:

    Show me the code: Extravagant Record Creation

    Max cases Trigger

    trigger MaxCaseTrigger on Case (before insert) {
        CaseHelper ch = new CaseHelper();
        ch.checkMaxCase(Trigger.new);
    }
    

    Max cases helper class

    public with sharing class CaseHelper {
        
        private static Case_Settings__c caseSettings;
        private static Integer maxCases = 99;
        
        static {
         Map<String, Case_Settings__c> setting = Case_Settings__c.getall();
         caseSettings = setting.get('Sandbox_Settings');
         
         if (caseSettings!=null && caseSettings.Max_Cases__c!=null){
             maxCases = Integer.valueOf(caseSettings.Max_Cases__c);
         }
        }
        
        public void checkMaxCase(List<Case> newCases){
            Map<Id, Case> accountIdToCaseMap = new Map<Id, Case>();
            for (Case newCase :newCases){
                if (newCase.AccountId!=null)
                    accountIdToCaseMap.put(newCase.OwnerId, newCase);
            }
            
            List<AggregateResult> agList = [Select count(id) caseCount, OwnerId, Owner.Name from Case where OwnerId IN :accountIdToCaseMap.keySet() Group By OwnerId, Owner.Name Having count(id)>=:maxCases];
            
            List<Case> casesToThrowError = new List<Case>();
            Map<Id, String> caseToContactNameMap = new Map<Id, String>();
            for (AggregateResult ag : agList){
                String ownerId = (String)ag.get('OwnerId');
                if (accountIdToCaseMap.containsKey(ownerId)){
                    Case errorCase = accountIdToCaseMap.get(ownerId);
                    casesToThrowError.add(errorCase);
                    caseToContactNameMap.put(ownerId, (String)ag.get('Name'));
                }
            }
             
            if (!casesToThrowError.isEmpty()){
                for (Case caseToThrowError : casesToThrowError){
                    caseToThrowError.addError('Too many cases created this month for user ' + caseToContactNameMap.get(caseToThrowError.OwnerId) + ' (' + caseToThrowError.OwnerId +  ') : ' + maxCases);
                }
            }
        }
    }
    

    Max cases helper test

    @isTest
    public class CaseHelperTest {
    
        static Integer maxCasesToCreate = 99;
        
        @TestSetup static void setup(){
            Case_Settings__c caseSettings = new Case_Settings__c(Name='Sandbox_Settings', Max_Cases__c=maxCasesToCreate);
            insert caseSettings;
        
            Account newAccount = new Account(Name='Test');
            insert newAccount;
            
            Contact newContact = new Contact(FirstName='First', LastName='Last', AccountId=newAccount.Id, Email='test@google.com');
            insert newContact;
        
            List<Case> caseLst = new List<Case>();
            
            for (Integer k = 0; k < maxCasesToCreate-1; k++){
                Case newCase = new Case();
                newCase.OwnerId = UserInfo.getUserId();
                newCase.AccountId = newAccount.Id;
                newCase.ContactId = newContact.Id;
                newCase.Status = 'Open';
                caseLst.add(newCase);
            }
            
            insert caseLst;
        }
    
         @isTest static void belowThreshold(){
            Case_Settings__c cs = Case_Settings__c.getInstance('Sandbox_Settings');
            cs.Max_Cases__c = 101;
            update cs;
            
            Test.startTest();
                System.assertEquals([Select Id from Case where ContactId!=null].size(), 98);
                Case newCase = new Case();
                newCase.OwnerId = UserInfo.getUserId();
                newCase.AccountId = [Select Id from Account][0].Id;
                newCase.ContactId = [Select Id from Contact][0].Id;
                newCase.Status = 'Open';
                insert newCase;
            Test.stopTest();
        }
    
        @isTest static void errorAboveThreshold(){
            Test.startTest();
                System.assertEquals([Select Id from Case where ContactId!=null].size(), 98);
                Case newCase = new Case();
                newCase.AccountId = [Select Id from Account][0].Id;
                newCase.ContactId = [Select Id from Contact][0].Id;
                newCase.Status = 'Open';
                try{
                    insert newCase;        
                } catch(Exception ex){         
                    System.assert(ex.getMessage().contains('Too many cases created this month for user'));
                }
            Test.stopTest();
        }
    }
    

    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 Coding Interview Challenge #4

    Coding question asked by Google

    Find the top most parent accountId by traversing the account hierarchy

    Recursive method

    public with sharing class AccountHierachy {
     
        //recursive method to find parentId of top account
        private Id topParentId = null;
        public Id getParentAccountsFromAccountId(Id accountId){
             Id parentId = null;
             Account acc = [Select Id, Parent.Id, Parent.Parent.Id, Parent.Parent.Parent.Id, Parent.Parent.Parent.Parent.Id, Parent.Parent.Parent.Parent.Parent.Id 
                            from Account where Id=:accountId][0];
             if (acc!=null)
                parentId = findParentFromAccountId(acc, parentId); 
             
              if (parentId!=null){
                topParentId = parentId;
                getParentAccountsFromAccountId(parentId);
              }
               
             return topParentId;            
        }
        
       
        public Id findParentFromAccountId(Account acc, Id parentId){
            //level1
            if (acc.Parent.Id!=null){
                parentId = acc.Parent.Id;
                
                //level2
                if (acc.Parent.Parent.Id!=null){
                    parentId = acc.Parent.Parent.Id;
                    
                   //level3
                    if (acc.Parent.Parent.Parent.Id!=null){
                        parentId = acc.Parent.Parent.Parent.Id;
                        
                        //level4
                        if (acc.Parent.Parent.Parent.Parent.Id!=null){
                            parentId = acc.Parent.Parent.Parent.Id;
    
                            //level5
                            if (acc.Parent.Parent.Parent.Parent.Parent.Id!=null){
                                parentId = acc.Parent.Parent.Parent.Parent.Parent.Id;
                            }
                        }
                    }
                } 
            }
    
            return parentId;
        }
    }
    

    Batch class

    global class AccountHierachyBatch implements Database.Batchable<sObject>, Database.Stateful {
    
        private static Id nextParentId;
        @TestVisible private static Id lastParentId;
        
        private Id parentId;
        private Id accountId;
        
        global AccountHierachyBatch(Id accountId){
            this.accountId = accountId;
        }
        
        String query = 'Select Id, Parent.Id, Parent.Parent.Id, Parent.Parent.Parent.Id, Parent.Parent.Parent.Parent.Id, Parent.Parent.Parent.Parent.Parent.Id from Account';
        
        global Database.QueryLocator start(Database.BatchableContext bc){
            query+=' where Id=\''+ accountId + '\''; 
            return Database.getQueryLocator(query);
        }
        
        public void execute(Database.BatchableContext bc, List<Account> accounts){
           AccountHierachy accountHierachy = new AccountHierachy();
           Account selectedAccount = null;
           if (accounts!=null && accounts.size() == 1){
               selectedAccount = accounts.get(0);
           }
         
           Id parentId = accountHierachy.findParentFromAccountId(selectedAccount, parentId);
           if (parentId!=null){
               nextParentId = parentId;
           } else {
               nextParentId = null;
           }
        }
        
        public void finish(Database.BatchableContext bc){
            if (nextParentId!=null){
                lastParentId = nextParentId;
                Database.executeBatch(new AccountHierachyBatch(lastParentId));
            } 
        }
    }
    

    Test class

    @isTest public class AccountHierachyTest {
    
        @TestSetup static void setup(){
            Account a1 = new Account(Name='a1');
            insert a1;
            
            Account a2 = new Account(Name='a2');
            a2.ParentId = a1.Id;
            insert a2;
          
            Account a3 = new Account(Name='a3');
            a3.ParentId = a2.Id;
            insert a3;
            
            Account a4 = new Account(Name='a4');
            a4.ParentId = a3.Id;
            insert a4;
            
            Account a5 = new Account(Name='a5');
            a5.ParentId = a4.Id;
            insert a5;
            
            Account a6 = new Account(Name='a6');
            a6.ParentId = a5.Id;
            insert a6;
            
            Account a7 = new Account(Name='a7');
            a7.ParentId = a6.Id;
            insert a7;
            
            Account a8 = new Account(Name='a8');
            a8.ParentId = a7.Id;
            insert a8;
            
            Account a9 = new Account(Name='a9');
            a9.ParentId = a8.Id;
            insert a9;
            
            Account a10 = new Account(Name='a10');
            a10.ParentId = a9.Id;
            insert a10;
        }
        
       @isTest static void testFindParentFromAccountId(){
            Account pAccount = [Select Id from Account where Name='a10'][0];
            Id topParentId = null;
            Test.startTest();
               AccountHierachy ah = new AccountHierachy();
               topParentId = ah.getParentAccountsFromAccountId(pAccount.Id);
            Test.stopTest();
            Account topParent = [Select Id from Account where Name='a1'][0];
            System.assertEquals(topParentId, topParent.Id);
        }
        
        @isTest static void testFindParentFromAccountIdBatch(){
            Account pAccount = [Select Id from Account where Name='a10'][0];
            Test.startTest();
               AccountHierachyBatch ah = new AccountHierachyBatch(pAccount.Id);
               Database.executeBatch(ah);        
            Test.stopTest();
            
            Account topParent = [Select Id from Account where Name='a1'][0];
            System.assertEquals(AccountHierachyBatch.lastParentId, topParent.Id);
        }
    }
    

    Cracking the Coding Interview: Unscripted Interview Video Solutions

    Anagram word search problem

    import java.util.*;
    
    class AnagramWordSearch {
    	
    	private static Set<String> dictionarySet;
    	private static Map<String, Set<String>> anagramMap;
    	
    	static {
            //Not optimal to load every time would want to cache the anagramMap
    		dictionarySet = loadDictionary();
    		anagramMap = buildWordMatchMap();
    	}
    	
    	public static Set<String> loadDictionary(){
    		dictionarySet = new HashSet<>(Arrays.asList("may", "student","students","dog","studentssess","god", "cat", "act", "tab", "bat", "flow","wolf", "lambs","amy", "yam", "balms", "looped", "poodle"));
    		return dictionarySet;
    	}
    	
    	public static Map<String, Set<String>> buildWordMatchMap(){
    		anagramMap = new HashMap<>();
    		for (String dictionaryWord : dictionarySet){
    			char[] chars = dictionaryWord.toCharArray();
    			Arrays.sort(chars);
    			String sortDictionaryWord = new String(chars);
    			if (anagramMap.containsKey(sortDictionaryWord)){
    				Set<String> existingWordMatch = anagramMap.get(sortDictionaryWord);
    				existingWordMatch.add(dictionaryWord);
    				anagramMap.put(sortDictionaryWord, existingWordMatch);
    			} else {
    				Set<String> newWordMatchSet = new HashSet<>(Arrays.asList(dictionaryWord));
    				anagramMap.put(sortDictionaryWord, newWordMatchSet);
    			}
    		}
    		return anagramMap;
    	}
    	
    	public static Set<String> getWordsWithSameChars(String word){
    		char[] wordChars = word.toCharArray();
    		Arrays.sort(wordChars);
    		String sortedWord = new String(wordChars);
    		return anagramMap.get(sortedWord);
    	}
    
    	public static void main(String[] args) {
    		Set<String> wordMatchSameChars = getWordsWithSameChars("cat"); 
    		System.out.println(wordMatchSameChars); //cat, act
    	}
    }
    

    Get derivative for Term

    import java.util.*;
    
    class Derivate {
    	
    	public static List<Term> getDerivative(List<Term> polynomials){
    		List<Term> derivativeTerm = new ArrayList<>();
    		for (Term polynomial : polynomials){
    			int expVal = polynomial.getExponent();
    			if (expVal>0){
    				Term newTerm = new Term(polynomial.getCoefficient() * expVal, expVal-1);
    				derivativeTerm.add(newTerm);
    			}
    		}
    		return derivativeTerm;
    	}
    	
    	public static void main(String[] args) {
    		List<Term> terms = new ArrayList<>(Arrays.asList(new Term(3, 2), new Term(5, 1), new Term(9, 0)));
    		System.out.println(getDerivative(terms)); //[{6, 1}, {5, 0}]
    	}
    
        public static class Term {
    		private int exponent;
    		private int coefficient;
    		
    		public Term(int coefficient, int exponent){
    			this.exponent = exponent;
    			this.coefficient = coefficient;
    		}
    		
    		public int getCoefficient(){
    			return coefficient;
    		}
    		
    		public void setCoefficient(int coefficient){
    			this.coefficient = coefficient;
    		}
    		
    		
    		public int getExponent(){
    			return exponent;
    		}
    		
    		public void setExponent(int exponent){
    			this.exponent = exponent;
    		}
    		
    		public String toString(){
    			return "{" + coefficient + ", " + exponent + "}";
    		}
    	}
    
    }
    

    Non Consecutive sequence with the largest sum

    import java.util.*;
    
    class HighestSumOfNonConsecutiveValues {
    	
    	public static Integer setOfConsecutiveElements(int[] intArr){
    		
    		Set<Integer> setOfValues = new HashSet<>();
    		for (int k = 0; k < intArr.length;k++){
    			setOfValues.add(intArr[k]);
    		}
    		
    		Integer largestSum = 0;
    		for (Integer setOfValue : setOfValues){
    			Integer iteratorSum = setOfValue;
    			for (Integer k = 1; k < setOfValues.size();k++){
    				if (setOfValues.contains(setOfValue+k)){
    					iteratorSum += (setOfValue+k);
    				} else if ((setOfValues.contains(setOfValue-k))){
    					iteratorSum += (setOfValue-k);
    				} else {
    					break;
    				}
    			}
    			if (largestSum < iteratorSum){
    				largestSum = iteratorSum;
    			}
    		}
    		
    		return largestSum;
    	}
    	
    	public static void main(String[] args) {
    		int[] arr = new int[]{1,3,5,6,12,11,62,7,101,102,41,37,2,52,2,31,5,69,71,32,33,7,100,11,4};
    		System.out.println(setOfConsecutiveElements(arr)); //303 (100+101+102)
    	} 
    }
    

    Ransome note

     public class Solution {
    
        private static final String YES = "Yes";
        private static final String NO = "No";
    
        static void checkMagazine(String[] magazine, String[] note) {
            Map<String, Integer> notesMap = new HashMap<>();
            for (String n : note){
                if (notesMap.containsKey(n)){
                    Integer wordFreq = notesMap.get(n) + 1;
                    notesMap.put(n, wordFreq);
                } else {
                    notesMap.put(n, 1);
                }
            }
    
            for (String m : magazine){
                if (notesMap.containsKey(m)){
                    Integer currentFreq = notesMap.get(m);
                    if (1 < currentFreq){
                        currentFreq-=1;
                        notesMap.put(m, currentFreq);
                    } else {
                        notesMap.remove(m);
                        if (notesMap.isEmpty()){
                            break;
                        }
                    }
                } 
            }
    
            String foundAllWordsInMagazine = YES;
            if (!notesMap.isEmpty()){
                foundAllWordsInMagazine = NO;
            } 
    
            System.out.println(foundAllWordsInMagazine);
        }
    }
    

    %d bloggers like this: