Salesforce Queueable Interface Technique

Hey, folks. We want to share with you a very elegant and powerful way to work with a queueable interface in Salesforce. The business case we had – is a complicated sales flow that required a lot of processing time and heavy support from the in-house team. The business wanted to automate the entire flow to get better user experience, reduce human interaction, and speed up the process.

A little bit of background:
Let’s say we have a web site or eCommerce, where our Mr. Happy Customer wants to make a purchase. We provided him a very nice interface to configure whatever he wants (build on JS or PHP or any other modern language. That is not a topic for today). Okay, he is been playing for a while and finally done and very excited to click pay now.

So what happens next? We need to accept the JSON (that is where we store online booking) with all the details related to the purchase. And we need to process this data in Salesforce.

Does not sound complicated, right? We create a web service to keep track of all bookings generated online.

@RestResource(urlMapping='/api/v1.0/submit_web_booking')
global with sharing class ApiBooking {

    @HttpPost
    global static Response submit() {
        try {
		WebBooking__c webBooking = new WebBooking__c(
			Status__c = 'Not Started', // We will use it in the future. But it is good to have status anyway.
			JSONBody__c = RestContext.request.requestBody.toString()); // Our JSON with all the details.
		Database.insert(webBooking);
			
        	return new Response('Thank you!');
        } catch(Exception exc) {
        	return new Response('Oops, we do not want to be here. But better to have a safe harbor.');
        }
    }

    global class Response {
    	public String status;
        public String message;

        public Response(String p_message) {
            this.status = '200';
            this.message = p_message;
        }

        public Response(Exception exc) {
            this.status = '400';
            this.message = exc.getMessage() + ' Stack Trace: ' + exc.getStackTraceString();
        }
    }
}

You would wonder, why can not we just create records from JSON right away? Well, we can, but our case required interaction between multiple systems, as well as heavy processing that would not be possible to do in a single transaction due to heap and CPU limitation. Normally it would cost a couple of hours of work for the in-house user.
Queueable interface to rescue. Let’s have a look:

public with sharing class BookingQueueRunner implements Queueable, Database.AllowsCallouts {

    private Id webBookingId; // Our record to process
    private String action; // That is because we can not process even with a single queue all the data.

    public BookingQueueRunner(Id p_webBookingId, String p_action) {
        this.webBookingId = p_webBookingId;
        this.action = p_action;
    }
   
    public void execute(QueueableContext context) {
        WebBooking__c> requests;
        try {
        	requests = [
                	SELECT Id, Status__c, JSONBody__c, JSONLog__c
                	FROM WebBooking__c
                	WHERE Id = :this.webBookingId
                	LIMIT 1 FOR UPDATE]; // We do not want any other process to modify the record. So lock it.
        } catch(Exception exc) {
		// Any loggly mechanism you want to implement.
        	System.debug(LoggingLevel.ERROR, '::::: exc = ' + exc.getMessage() + ': ' + exc.getStackTraceString());
        	return;
        }

        if(requests.isEmpty()) {
            return;
        }

        WebBooking__c currentRequest = requests.get(0);

	// We had 5 steps but you can see that you can have as many as needed.
        try {
            if(action == 'create_records') { // Assuming at this step we create only records.
                createRecords(currentRequest);
		// If all good we register the same request to go to the next stage.
                System.enqueueJob(new BookingQueueRunner(this.webBookingId, 'make_callouts'));
            } else if(action == 'make_callouts') {
                makeCallouts(currentRequest);
		// If all good we move further in processing.
                System.enqueueJob(new BookingQueueRunner(this.webBookingId, 'complete_processing'));
            } else if(action == 'complete_processing') {
                completeProcessing(currentRequest);

		// If all good we finish with the current request and can handle the next one in a queue.
                currentRequest.Status__c = 'Completed';
                Database.update(currentRequest);
				
		// To check if we have any pending records.
                executeNext();
            }
        } catch(Exception exc) {
            	System.debug(LoggingLevel.ERROR, '::::: exc = ' + exc.getMessage() + ': ' + exc.getStackTraceString());
			
		// We want to know which action failed and why. As well as fix failed processing manually if needed.
		currentRequest.Status__c = 'Failed';
		Map<String, String> temp = new Map<String, String> {
			'action' => this.action,
			'message' => exc.getMessage(),
			'stack' => exc.getStackTraceString()
		};
		currentRequest.JSONLog__c = JSON.serialize(temp);
		Database.update(currentRequest);

		// If current request failed. We still want to process pending.
        	executeNext();
        }
    }

    public void executeNext() {
	// If something is already in progress we wait.
        List<WebBooking__c> webBookingInProgress = [
                SELECT Id
                FROM WebBooking__c
                WHERE Status__c = 'In Progress' LIMIT 1];

        if(webBookingInProgress.isEmpty()) {
            List<WebBooking__c> webBookingNotStarted = [
                    SELECT Id
                    FROM WebBooking__c
                    WHERE Status__c = 'Not Started' LIMIT 1];

	    // If we have booking to process. Mark it as in progress, since we need to complete multiple steps and run the first action.
            if( ! webBookingNotStarted.isEmpty()) {
                webBookingNotStarted.get(0).Status__c = 'In Progress';
                Database.update(webBookingNotStarted.get(0));
				
                System.enqueueJob(new BookingQueueRunner(webBookingNotStarted.get(0).Id, 'create_records'));
            }
        }
    }

    /****************** QUEUE FUNCTIONS ******************/

    private void createRecords(WebBooking__c p_webBooking) {
        // Your code here.
    }
	
    private void makeCallouts(WebBooking__c p_webBooking) {
        // Your code here.
    }
	
    private void completeProcessing(WebBooking__c p_webBooking) {
        // Your code here.
    }
}

Now we also need to modify a bit our web service to start a queue.

@HttpPost
global static Response submit() {
	WebBooking__c webBooking;
	try {
		webBooking = new WebBooking__c(
			Status__c = 'Not Started', // We will use it in the future. But it is good to have status anyway.
			JSONBody__c = RestContext.request.requestBody.toString()); // Our JSON with all the details.
		Database.insert(webBooking);
		
		return new Response('Thank you!');
	} catch(Exception exc) {
		return new Response('Oops, we do not want to be here. But better to have a safe harbor.');
	}
	
	List<WebBooking__c> webBookingInProgress = [
		SELECT Id
		FROM WebBooking__c
		WHERE Status__c = 'In Progress' LIMIT 1];
			
	// Only if nothing in progress we need to run a queue. Otherwise we simply create a record and queue will pick it once finished with the current.
	if(webBookingInProgress.isEmpty()) {
		webBooking.Status__c = 'In Progress';
		Database.update(webBooking);

		// What is nice is that we also can re-start any action for failed records from dev console. Or give Users the power to do so via a button or flow with a bit of admin work.
		System.enqueueJob(new BookingQueueRunner(webBooking.Id, 'create_records'));
	}
}

Elegant, right? But what is more important, we were able to automate processing from a few hours up to 2-15 mins (Note that time may depend on the resources available for the org). And no human interaction, so the in-house team is focussed on what they need to focus and leave to the automation all the magic.

Thanks for reading. Hope it will be helpful to you. If you have any questions We’ll be happy to answer.

Comments are closed.

Works with AZEXO page builder
© All rights reserved. Theme by AZEXO