The list data type is rather versatile, and its use is essential in many programmatic solutions on the Salesforce platform. However, there are some scenarios when lists alone do not provide the most elegant solution. One example is routing the assignment of accounts based on each user’s current capacity.
Suppose we want to assign the oldest unassigned account to a user at the moment when a new account is entered into Salesforce. When working with these accounts, we might want to order them by received date, with the first entry containing the oldest date. How can we design a routing tool so that the next account to assign is stored at the front of a list?
Queue Abstract Data Type
The queue abstract data type is well suited for this type of problem. Our first step should be to define the Queue interface and what methods we want to include:
public interface Queue{
//returns the number of entries in the queue
Integer size();
//returns true if there are no entries in the queue
boolean isEmpty();
//places the record at the end of the queue
void enqueue(SObject o);
//returns the entry at the front of the queue but does not remove it
SObject first();
//returns and removes the entry at the front of the queue
SObject dequeue();
}
Next we need to implement this interface for accounts:
public class AccountQueue implements Queue{
private List<Account> accounts;
//default constructor
public AccountQueue(){
this.accounts = new List<Account>();
}
//returns the number of accounts in the queue
public Integer size(){
return accounts.size();
}
//returns true if there are no accounts in the queue
public boolean isEmpty(){
return accounts.isEmpty();
}
//places the account at the end of the queue
public void enqueue(SObject o){
Account newAccount = (Account) o;
accounts.add(newAccount);
}
//returns the account at the front of the queue
public Account first(){
if(isEmpty()){
return null;
}
return accounts.get(0);
}
//returns and removes the account at the front of the queue
public Account dequeue(){
if(isEmpty()){
return null;
}
Account firstAccount = accounts.get(0);
accounts.remove(0);
return firstAccount;
}
}
On the Account object, we should create two custom fields. The first is called Assigned, which is a lookup to the User object. The second is the Received Date which is a date field. On the User object, we can create a number field called Capacity, which will tell us to how many more applications a User can be assigned. Once this number reaches zero, we should not assign any more applications to that particular user.
In order for this process to occur when an account is inserted, we will need an Account trigger:
trigger Account on Account (before insert) {
if(trigger.isBefore && trigger.isInsert){
new AccountTriggerHandler().beforeInsert(trigger.new);
}
}
Here is the trigger handler:
public class AccountTriggerHandler {
//list of accounts that are available for assignment to a User
private List<Account> unassignedAccounts {
get{
if(unassignedAccounts == null){
unassignedAccounts = [SELECT ID,
Name,
Received_Date__c
FROM Account
WHERE Received_Date__c != null AND Assigned__c = null ORDER BY Received_Date__c];
}
return unassignedAccounts;
}
private set;
}
//Account queue where the account at the front of the queue has the oldest received date
private AccountQueue unassignedAccountQueue {
get{
if(unassignedAccountQueue == null){
unassignedAccountQueue = new AccountQueue();
for(Account a : unassignedAccounts){
unassignedAccountQueue.enqueue(a);
}
}
return unassignedAccountQueue;
}
private set;
}
//Map of users that are able to receive assigned applications
private Map<ID,User> userMap {
get{
if(userMap == null){
userMap = new Map<ID,User>([SELECT ID, Capacity__c FROM User WHERE Capacity__c != null]);
}
return userMap;
}
private set;
}
public void beforeInsert(List<Account> accountList){
//obtain the number of accounts in the trigger
Integer numberOfAccountsToAssign = accountList.size();
//hold a list of accounts that will be assigned to users
List<Account> accountsToAssign = new List<Account>();
for(Integer i = 0; i < numberOfAccountsToAssign; i++){
//obtain the id of the next user that can receive an application
ID userIDNextToAssign = getNextAssignedUser();
//reduce that user's capacity by 1
reduceCapacity(userIDNextToAssign);
//determine the next account that is to be assigned
Account unassignedAccount = unassignedAccountQueue.dequeue();
//if there were any accounts remaining in the queue, assign that account
if(unassignedAccount != null){
unassignedAccount.Assigned__c = userIDNextToAssign;
accountsToAssign.add(unassignedAccount);
}
}
//update unassigned accounts
update accountsToAssign;
//update the user records
update userMap.values();
}
//return the id of the user that will be assigned to the next available account
private ID getNextAssignedUser(){
ID largestCapacityUserID;
//find the user id of the largest capacity user
Integer maxCapacity = 0;
for(ID userID : userMap.keySet()){
Integer userCapacity = (Integer) userMap.get(userID).Capacity__c;
if(maxCapacity < userCapacity && userCapacity > 0){
maxCapacity = userCapacity;
largestCapacityUserID = userID;
}
}
return largestCapacityUserID;
}
//recude the capacity of the user with id userID by 1
private Map<ID,User> reduceCapacity(ID userID){
//decrease the capacity of that user by 1
if(userID != null){
User usr = userMap.get(userID);
usr.Capacity__c = usr.Capacity__c - 1;
}
return userMap;
}
}
The trigger fires when a new account is entered into Salesforce, then searches for the account with the oldest received date, and assigns it to the user with the highest capacity. To demonstrate, we can set one user to have a capacity of 1, and a second user to have a capacity of 2. If we insert three accounts, then the three accounts with the oldest received date will be distributed between both users.
Here are the existing accounts before we insert the new accounts:
Here are the assignments after we add the new accounts:
Since three accounts were inserted into Salesforce, we needed to assign three accounts.
- Mario had a capacity of 2, so he was assigned the first account in the queue.
- Next, Taylor, who had a capacity of 1, was assigned an account. He was then at a full capacity of 0.
- Mario, now at a capacity of 1, received the next account in the queue.
Alternative Approaches
There are some alternative ways to approach this problem using only lists, but using the Queue interface simplifies the implementation. One approach could have been to reverse the order of the unassigned accounts, starting with the newest account as the first item, and the oldest as the last. This might be unintuitive and would require the developer to store or compute the size of the list in order to access the last element. Another approach might have been to keep the same order as the queue solution, but to simply remove the element from the front of the list using the remove(index) function. The queue implementation abstracts this process nd removes the requirement to continue to check if the list is empty, as the dequeue method already provides that functionality.
The queue abstract data type is a natural fit for any first in first out business requirement. Queues can also be extended to other objects in Salesforce, rather than just the Account object. Queues and other abstract data types can provide templates for solutions to many programming challenges and Salesforce projects are no exception.