Engineering

Efficient Record Grouping with MultiMaps

Aidan Harding
November 22, 2023
4 min read

As a Salesforce developer, how many times have you had to make a Map of records where the key is a property of those records, and the values are lists of matching records? For example, a Map of Contact records where the keys are the last names or the Account IDs.

i.e. How often have you written something like this?

List<Contact> contacts = new List<Contact>{
    new Contact(FirstName = 'John', LastName = 'Smith'),
    new Contact(FirstName = 'Jane', LastName = 'Smith'),
    new Contact(FirstName = 'John', LastName = 'Not-Smith')
};
Map<String, List<Contact>> lastNameToContacts = new Map<String, List<Contact>>();

for (Contact thisContact : contacts) {
    List<Contact> thisList = lastNameToContacts.get(thisContact.LastName);
    if (thisList == null) {
        thisList = new List<Contact>{ thisContact };
        lastNameToContacts.put(thisContact.LastName, thisList);
    } else {
        thisList.add(thisContact);
    }
}

Assert.areEqual(
    new List<Contact>{ contacts[0], contacts[1] },
    lastNameToContacts.get('Smith')
);
Assert.areEqual(
    new List<Contact>{ contacts[2] },
    lastNameToContacts.get('Not-Smith')
);

Too many times! It's perfectly serviceable code, but it doesn't have to be so long-winded — and it doesn't have to be created from scratch every time. What if there were a reusable data structure to do this?

Well, the general name for a Map where the values are lists is a MultiMap, and we can write one (even though we don't have the ability to write our own generics yet 😥). With a MultiMap, the above example becomes:

MultiMap multiMap = new MultiMap(List<Contact>.class, new FieldFromSObject(Contact.LastName));

List<Contact> contacts = new List<Contact>{
    new Contact(FirstName = 'John', LastName = 'Smith'),
    new Contact(FirstName = 'Jane', LastName = 'Smith'),
    new Contact(FirstName = 'John', LastName = 'Not-Smith')
};

multiMap.putAll(contacts);

Assert.areEqual(
    new List<Contact>{ contacts[0], contacts[1] },
    multiMap.getWithKey('Smith')
);
Assert.areEqual(
    new List<Contact>{ contacts[2] },
    multiMap.getWithKey('Not-Smith')
);

FieldFromSObject reads a field value from an SObject record and can be reused whenever you create a MultiMap of SObjects. If you only ever wanted SObjects, you could use the SObjectIndex class I wrote years ago, but MultiMap lets you store plain Apex objects (DTOs), Maps, or even primitives.

Key insights behind MultiMap:

  1. Functions as interfaces: Define an interface (MultiMap.KeyReader) to extract keys from items, allowing Apex to treat objects like functions (strategy pattern).
  2. Runtime type creation: Pass Type instances (e.g., List<Contact>.class) so MultiMap can instantiate the right typed lists internally.

MultiMap Implementation

public class MultiMap {
    private Map<Object, List<Object>> theMap;
    private Type listType;
    private KeyReader keyReader;

    public interface KeyReader {
        Object getKey(Object item);
    }

    public MultiMap(Type listType, KeyReader keyReader) {
        this.theMap = new Map<Object, List<Object>>();
        this.listType = listType;
        this.keyReader = keyReader;
    }

    public MultiMap(KeyReader keyReader) {
        this(List<Object>.class, keyReader);
    }

    public void putAll(List<Object> items) {
        for (Object item : items) {
            put(item);
        }
    }

    public void put(Object item) {
        Object key = keyReader.getKey(item);
        List<Object> list = theMap.get(key);
        if (list == null) {
            list = (List<Object>) listType.newInstance();
            list.add(item);
            theMap.put(key, list);
        } else {
            list.add(item);
        }
    }

    public List<Object> getWithKey(Object key) {
        return theMap.get(key);
    }

    public List<Object> getWithItem(Object item) {
        return getWithKey(keyReader.getKey(item));
    }

    public Set<Object> keySet() {
        return theMap.keySet();
    }
}

Example: Partitioning Integers

private class IsEvenKey implements MultiMap.KeyReader {
    public Object getKey(Object item) {
        return ((Integer) item & 1) == 0;
    }
}

// Test method:
@IsTest static void testEvenOddPartition() {
    MultiMap multiMap = new MultiMap(new IsEvenKey());
    multiMap.put(5);
    multiMap.put(6);
    // 5 maps to false (odd), 6 maps to true (even)
    System.assertEquals(5, multiMap.getWithItem(5)[0]);
    System.assertEquals(6, multiMap.getWithKey(true)[0]);
}

Here at Processity, we're doing cool stuff around Field History Tracking, Audit Trail, Observability, and Process Mining on Salesforce. When we have bits of engineering to share, we love to put them out—get the full MultiMap source on GitHub as a Gist!

Ready to Transform Your Salesforce Processes?

Discover how Processity can help you gain insights into your business processes.

Get a DemoCall to action icon
Efficient Record Grouping with MultiMaps | Processity Blog | Processity