January 9, 2024

How to build extensible and maintainable Salesforce solutions

Noltic's Certified Technical Architect reveals best Salesforce app extensibility practices.

1. Introduction

Building extensible Salesforce apps involves creating solutions that can easily adapt and scale to changing requirements, enabling developers and users to extend functionality seamlessly. It's about constructing a foundation that accommodates future enhancements without significant rework, fostering a dynamic and sustainable application ecosystem.

Customization and flexibility are paramount in the Salesforce ecosystem, ensuring that applications for AppExchange or internal usage can be tailored to satisfy unique and evolving business processes and requirements. For developers, customization means crafting solutions that align precisely with organizational needs, enhancing productivity and efficiency. For users, flexibility empowers them to adapt the system to their workflows, boosting user adoption and satisfaction.

2. Custom Metadata Types – the heart of customization

Custom Metadata Types (CMT or CMDT) are a powerful feature in Salesforce, serving as a cornerstone for configuration-based customization. Unlike traditional custom settings, CMT allows developers to define and manage metadata records that store customizable data. This metadata-driven approach provides a centralized and scalable way to configure application behavior, allowing for dynamic changes without altering code.

CMDT empowers developers and administrators to design flexible Salesforce solutions, providing a unified mechanism to store and retrieve configuration data for various components, from field labels and picklist values to complex business rules. Leveraging CMDT enhances the maintainability, scalability, and extensibility of Salesforce applications, enabling dynamic adaptation to evolving business requirements.

Let's take a look at some examples.

Defining labels and descriptions

CMDT is extensively used for managing labels and descriptions of custom fields, objects, or even entire applications. Storing this information in metadata records allows for easy modification without altering Apex code, enhancing agility in adapting the user interface over time.

Configuring values for processes

CMDT provides a centralized repository for configuring values used in processes, workflows, and validation rules. For example, storing approval thresholds, escalation criteria, or other dynamic parameters in metadata records allows administrators to tweak these values without engaging in time-consuming code changes.

Structuring UI components

CMDT enables the dynamic structuring of UI components. Metadata records can define the composition and layout of Lightning web components, empowering administrators to adjust the user interface without code changes.

Storing business rules and configurations

CMDT can store complex configurations and business rules, such as the conditions under which certain features should be enabled or disabled. This allows developers to build more generic and extensible solutions where business logic can be fine-tuned through metadata, reducing the need for hard-coded configurations.

Another great thing about CMT is the possibility of using it from different places, like Apex code, validation rules, and flows.

Querying CMT from Apex:

List metadataRecords = [SELECT Label, Value__c FROM MyCustomMetadataType__mdt];

Using CMT in validation rules:


3. Object-Oriented Programming principles

While CMTs are building blocks, Apex Code is the driving force of our application. It is responsible for constructing and executing business logic and processes. As Apex is built in the OOP paradigm, the best we can do is use those principles during solution design.

Utilizing Object-Oriented Programming (OOP) principles in Salesforce development facilitates the creation of a scalable and reusable code. Inheritance enables the establishment of a common foundation for shared functionalities, promoting code reusability and reducing redundancy. Encapsulation enhances modularity, allowing developers to encapsulate data and methods within objects, leading to better organization, maintenance, and extensibility of code.

Let's consider a scenario where a messaging system needs to send messages through different channels based on message priority. By using interfaces, you can implement multiple messaging strategies while maintaining a consistent interface for sending messages.

// Interface definition for MessageSender
public interface MessageSender {
    void sendMessage(String message);
// Implementation for Email Message
public class EmailMessageSender implements MessageSender {
    public void sendMessage(String message) {
        // Logic to send the message via email
        System.debug('Sending email message: ' + message);
// Implementation for SMS Message
public class SMSMessageSender implements MessageSender {
    public void sendMessage(String message) {
        // Logic to send the message via SMS
        System.debug('Sending SMS message: ' + message);
// Implementation for Push Notification
public class PushNotificationSender implements MessageSender {
    public void sendMessage(String message) {
        // Logic to send the message via push notification
        System.debug('Sending push notification: ' + message);

Then, with a combination of CMT and dependency injection, we will have a result similar to this:

public class MessageService {
    private MessageSender messageSender;
    // Constructor with dependency injection using a factory
    public MessageService(String senderClassName) {
        this.messageSender = createMessageSender(senderClassName);
    // Method to send a message
    public void sendMessage(String message) {
    // Factory method to dynamically create instances of MessageSender
    private static MessageSender createMessageSender(String senderClassName) {
        // Query Custom Metadata Type records to get mappings
        List senderMappings = [SELECT SenderClassName__c, SenderImplementation__c FROM MessageSenderMapping__mdt];
        // Map sender class names to their corresponding implementations
        Map senderImplementations = new Map();
        for (MessageSenderMapping__mdt mapping : senderMappings) {
            String className = mapping.SenderClassName__c;
            String implementationName = mapping.SenderImplementation__c;
            // Ensure both class name and implementation are specified
            if (!String.isBlank(className) && !String.isBlank(implementationName)) {
                senderImplementations.put(className, Type.forName(implementationName));
        // Check if the specified sender class name exists in the map
        if (senderImplementations.containsKey(senderClassName)) {
            // Dynamically create an instance of the specified class
            return (MessageSender)senderImplementations.get(senderClassName).newInstance();
        } else {
            // Default to a fallback sender (you can customize this behavior)
            return new PushNotificationSender();

In this example, MessageSenderMapping__mdt is assumed to be a Custom Metadata Type with fields SenderClassName__c and SenderImplementation__c being assumed to store the mappings. Ensure that you adjust the field names and data structure based on your specific Custom Metadata Type. Now, you can manage sender mappings dynamically through Custom Metadata records, avoiding hardcoding.

Moreover, by making this interface global you can allow external developers to write their own implementation and inject it into your solution without making any changes to your code.

4. Modular structure and a unified data format

As you can see, here we have a modular structure, where each implementation is a module of some kind, and all of them accept a unified format. Maintaining a modular structure in Salesforce ensures scalability and agility, allowing developers to work on independent components without disrupting the entire system.

A unified data structure and data format streamline communication between different modules, promoting consistency and reducing integration challenges. In Salesforce, a modular and standardized approach enhances collaboration and reduces risks associated with complexity.

//Our unified data format for location information
public class Location {
    public String street;
    public String city;
    public String postalCode;
    public String countryCode;
// Location service interface representing a unified format
public interface LocationService {
    Location getLocationInfo(String query);
// Third-party location services implementations
public class GoogleLocationService implements LocationService {
    public Location getLocationInfo(String query) {
        // Logic to fetch street information from Google Maps API
public class OpenStreetMapLocationService implements LocationService {
    public Location getLocationInfo(String query) {
        // Logic to fetch street information from OpenStreetMap API

It introduces a unified Location data format with street, city, postal code, and country code. The LocationService interface establishes a contract for fetching location information. Two third-party implementations, GoogleLocationService and OpenStreetMapLocationService, adhere to the interface, providing logic to fetch location details from their respective APIs.

This pattern allows seamless interchangeability between different location services while maintaining a consistent data format and interface.

5. Leveraging New Features

Staying updated on Salesforce features is crucial for maximizing the utilization of platform's capabilities. Regular updates provide access to new functionalities, enhancements, and security features. Staying informed enables organizations to align their Salesforce strategies with evolving industry best practices and emerging trends.

For example, the Dynamic Components feature in Lightning Web Components (LWC) enables developers with the flexibility to create components dynamically. This feature is particularly beneficial for developers creating managed packages, as it allows them to design applications with a modular approach.

By dynamically instantiating components based on configurations, developers can provide users with a choice to use only the components they need, streamlining the development process and optimizing the package's footprint. This capability enhances the overall customization and adaptability of managed packages.

Or another example – Evaluate Dynamic Formulas in Apex. This feature simplifies logic execution and enhances flexibility in handling dynamic business rules. With Evaluate Dynamic Formulas, developers can efficiently incorporate formula evaluations directly in Apex, eliminating the need for complex workarounds.

global class MotorYacht {
   global Integer lengthInYards;
   global Integer numOfGuestCabins;
   global String name;
   global Account owner;
MotorYacht aBoat = new MotorYacht();
aBoat.lengthInYards = 52; 
aBoat.numOfGuestCabins = 4;
aBoat.name = 'RV Foo';
FormulaEval.FormulaInstance isItSuper = FormulaEval.FormulaBuilder.builder()
                            .withFormula('IF(lengthInYards < 100, "Not Super", "Super")')
isItSuper.evaluate(aBoat); //=> "Not Super"
aBoat.owner = new Account(Name='Acme Watercraft', Site='New York');
FormulaEval.FormulaInstance ownerDetails = FormulaEval.FormulaBuilder.builder()
                            .withFormula('owner.Name & " (" & owner.Site & ")"')
ownerDetails.evaluate(aBoat); //=> "Acme Watercraft (New York)"

Keep in mind that when you have a modular structure for an old implementation, when the feature becomes globally available, you can switch the implementation to the new one and just keep using it as usual!

6. Combining Custom Metadata Types (CMT), Object-Oriented Programming (OOP), and new features

Combining CMT, OOP, and the new features in Salesforce allows building adaptable and scalable applications.

By storing configuration data in the CMT records, developers can dynamically manage various aspects of the application without modifying code. Implementing OOP principles, such as encapsulation and inheritance, promotes modular and reusable code, fostering a structured and maintainable codebase. The integration of new features, coupled with dependency injection and interfaces, facilitates loose coupling and dynamic component selection based on configuration data.

This approach allows for a highly configurable and extensible application architecture, where changes can be seamlessly introduced without the need for extensive code modifications.

7. Conclusion

Building extensible Salesforce applications involves a strategic combination of essential concepts and features that empower developers and users alike:

  • Custom Metadata Types serve as the cornerstone of customization, allowing for dynamic configuration and adaptation across various use cases, from UI structures to process values.
  • Implementing Object-Oriented Programming principles, including inheritance and encapsulation, further enhances scalability and code reuse.
  • A modular structure, coupled with SOLID principles, ensures maintainability and extensibility, creating a foundation for robust application architecture.
  • Staying updated on Salesforce features, exemplified by Dynamically Instantiate Components and Evaluate Dynamic Formulas in Apex, is paramount for leveraging the latest tools and functionalities.

Ultimately, the synergy of Custom Metadata Types, OOP principles, and new features enables the creation of powerful, flexible, and adaptive Salesforce applications, exemplified through comprehensive code examples that showcase their seamless integration.

/ More news
August 15, 2023
About Noltic
10 Reasons to Outsource Your Salesforce Product Development
Why outsourcing Salesforce Product Development is an effective solution for your business.
Read more