Managed package extensibility ideas

A managed package is a collection of application components posted as a unit on AppExchange and associated with a namespace and a License Management Organization.

  • Published 21 Feb 2023
  • 9 mins read
Managed package extensibility ideas
Table of contents

Article highlights

  • The article discusses how Salesforce managed packages can be made more extensible and customizable through application settings, dependency injection, and configurable user interfaces, allowing for personalized customer experiences.
  • It explains the use of coding patterns like Dependency Injection and Inversion of Control in Apex to modify managed package behavior at runtime, using examples such as the Type.ForName and Type.newInstance methods.
  • The article provides examples of how to implement extensibility in managed packages, including the creation of data structure classes, use of global methods, and publish-and-subscribe patterns for event-driven actions.

Salesforce offers two ways to build managed packages: first-generation packaging (1GP) and second-generation packaging (2GP).

A Salesforce application from a managed package is typically static – meaning its functionality can’t be changed after installation.

However, customers usually have differing requirements, so it’s often advantageous to be able to change the application for specific customers.
In fact:

🔎
So, let’s explore different options to make a managed package more extensible and customizable to help fulfill customers’ personalized needs.

Extensibility options application settings

An application setting is a value that controls how a feature behaves within the application. One or more settings can be used to manage the application.

For example, the validation rules can all use a master switch to turn an application’s setting on or off by having them all reference an ‘active’ checkbox on a custom setting or metadata record.

This is usually used during data imports after an initial installation. The installation ensures customers’ data can be imported by bypassing the application’s validation.

✔️
Another example is with a threshold. Often, customers want to distinguish their large accounts from their regular ones, so they determine some sales threshold that – when it is met or exceeded – it marks the customer as a ‘significant’ one.

The threshold can be stored as a number in the managed package’s metadata and used via automation to maintain each account’s status.

Who can control the application settings is also essential. Through using protected, public custom settings, or metadata records, the managed package developer controls who can adjust the settings.

Protected custom settings or metadata records can only be updated by the managed package developer – not the customer. The developer does this by logging into the customer’s org using the Licensing Management Application (LMA).

Dependency injection and inversion of control

Dependency Injection and Inversion of Control is a coding pattern that lets a person represent the code required at runtime. This allows the user to create different codes outside of the package in a customer’s org to change the behavior in the managed package.

〽️
In Apex, this coding pattern is made using the “Type.ForName” and “Type.newInstance” methods. These methods let you represent the specified class at runtime. 

The class can be read from your application setting. Your setting allows the managed package’s Apex application code to be changed according to customers’ requirements. This change is achieved by implementing a new class in the customer’s org and updating the application setting to specify the new class.

For example, an independent software vendor (ISV) has an order management module in their product that lets their customers offer products for sale. Salesforce has a standard set of product pricing in the app, but many customers often have unique pricing requirements.

From a coding standpoint, their managed package uses Dependency Injection with the Apex “Type.ForName” and “Type.newInstance” methods.

An order price code example

Let’s see how a customer’s pricing requirements come to light through the necessary coding.

  • First, data structure classes are needed. A product information class is designed to store information about a single product, representing all the details required to price it.
global class ProductInfo {
    global Id ProductId { get; set; }
    
    global Decimal Quantity { get; set; }
}
  • Each product has a “ProductId” to query its information from the database. The class also has a decimal for the quantity so that the pricing logic knows how many of these products are being ordered.
  • Discounts based on bulk pricing are global so that anyone can use this class outside the managed package. A class is also used to add additional properties for future package releases.
  • A “ProductPricingRequest” class is created as a parameter object to the product pricing function. It’s a container holding the list of product information. If needed, you can add more input in a future version.
global class ProductPricingRequest {
    global List<ProductInfo> ProductsToPrice { get; set; }
}
  • The product pricing class represents the pricing information for a single product and contains the “ProductInfo” instance used and its unit price.
global class ProductPricing {

    global ProductInfo ProductInfo { get; set; }
    
    global Decimal UnitPrice { get; set; }
    
}
  • Next, a product pricing response class is created containing a list of “ProductPricing.” The product pricing function will return this data.
global class ProductPricingResponse {
    global List<ProductPricing> ProductPricings { get; set; }
}
  • Now that the data structures are created, the product price interface is named “IProductPricer.”
global interface IProductPricer {

    ProductPricingResponse priceProducts(ProductPricingRequest pricingRequest);
    
}
  • The interface has a single function that takes in a “ProductPricingRequest” with the list of products to price. This structure returns a “ProductPricingResponse” so that the requestor can return all of the products and their prices.
  • Using parameter and response objects allows an ISV to add properties to each product. These properties create further input and output in future versions without changing the price product’s function signature.
  • The standard product price will then need to be developed, which the managed package will use.
global class StandardProductPricer implements IProductPricer {

    global ProductPricingResponse priceProducts(ProductPricingRequest pricingRequest) {
        ProductPricingResponse response = new ProductPricingResponse();
        
        // Standard Pricing Logic Here

        return response;
    }
    
}
  • The “StandardProductPricer” class implements the “IProductPricer” interface and its “priceProducts” function. This type of class implements the logic needed to price the requested products. Your coding will scan the Salesforce database to get the needed product information, pricing data for each product, and any other information required. It then returns the priced products in the response.
  • Finally, create an “OrderService” class that the front-end order management module invokes to get the pricing information.
public class OrderService {

    public ProductPricingResponse priceProducts(List<ProductInfo> productsToPrice) {
        // The name of the product pricer class to use is stored in an application setting
        // somewhere that's configurable. Its value is used here.
        String productPricerClassName = 'StandardProductPricer';
        
        Type productPricerType = Type.forName(productPricerClassName);
        IProductPricer productPricer = (IProductPricer) productPricerType.newInstance();
        
        ProductPricingRequest pricingRequest = new ProductPricingRequest();
        pricingRequest.ProductsToPrice = productsToPrice;
        
        return productPricer.priceProducts(pricingRequest);
    }
    
}

Configurable user interfaces

Custom user interfaces are often needed for functionality that is not provided by Salesforce. These interfaces are configurable. You can configure them through your application settings stored in the managed package’s metadata or through the design settings of Lightning Components.

For example, a custom form has multiple fields that often change per customer. The managed package uses protected custom metadata records to specify the fields to display.
☁️
Another example is an Aura or Lightning Web Component that controls its behavior and uses design time settings specified in the Lightning App Builder or Experience Cloud Builder. One setting could control how many records to show, while another could control the record type ID for a specific record.

Request and response objects for global apex methods or functions

If a customer’s code uses a global method and its signature changes, it will not compile (which can cause errors).

Using a request object and response object, a user can add adjusted properties and variables as needed for further input and output without changing the function’s signature.

An Apex code example

Let’s say your managed package has an ordering module, and the pricing for each product differs. This means the pricing logic must be flexible enough to accept new input and provide created output. The coding for this functionality will look like this:

 global class ProductPricer {
    global PricingResponse getPricing(ProductPricingRequest request) {
         // determine product pricing using info provided in request and return info 
         // in Pricing Response
    }
}

global class ProductPricingRequest {
    global List<ProductDetail> Products = new List<ProductDetail>()
}

global class ProductDetail {
    global String ProductId = ‘’;

    global Integer Quantity = 0;

    global Map<String, Object> CustomProperties = new Map<String, Object>();
}

global class PricingResponse {
    global List<ProductPrice> Prices = new List<ProductPrice>();
}

global class ProductPrice {
    global String ProductId = ‘’;

    global Decimal Price = 0;

    global Map<String, Object> CustomProperties = new Map<String, Object>();
}
  • The “ProductPricer” class has the “getPricing” function to determine the pricing information for a list of products.
  • The “ProductPricer” class has the “getPricing” function to determine the pricing information for a list of products.
  • Another input is “ProductPricingRequest,” an object parameter used as an input to the “getPricing” function.
  • The function currently accepts a list of products with a product ID and quantity. Each product also has a “CustomProperties” map, so you can provide additional details for custom pricing that may be done in a customer’s org.
  • Your “getPricing” function will use the “PricingResponse” object parameter to return a list of prices where each “ProductPrice” has a product ID and its cost. The “ProductPrice” also has the “CustomProperties” map, so you can pass back extra pricing information from custom pricing logic if needed. Map collections can be used as a generic container for custom pricing logic to pass pricing input and back pricing output per product.
  • These request and response classes can be extended by adding global properties to accept input and provide output in newer managed package releases.

Publish and subscribe

Using a publish-and-subscribe pattern allows an organization’s application to post events. One or more subscribers can use each event's information to perform the necessary actions. This will enable customers to subscribe to the managed package’s published events and perform customer-specific behavior as needed.

For example, the managed package has an ordering module, and when an order is completed, it publishes a completed order event with a customer receipt.

Let’s say a particular customer has a fulfillment system to integrate. They can now subscribe to these completed order events and listen for new orders. This information can be sent to the fulfillment system to process the order.

A platform event-triggered Flow, for example, can be used to send a customer their receipt. The customer’s IT department can use the new Salesforce publication and subscription application programming interface (API) to integrate with the fulfillment system.

Another example is inter-component communication between Lightning Components in a managed package. Often, multiple custom Lightning Components will interact with each other on a Lightning page.
📲
They can communicate by publishing and subscribing to events in JavaScript. An implementation team can create customer-specific Lightning Components that subscribe to the published managed package. 

These events provide a customer-specific functionality when the package’s component or components publish that event.

Order notification examples

Let’s take a closer look at how you can use a platform event to email a receipt to the customer when an order is completed.

First, a platform event is created with these definitions:

  • Label: Completed order
  • Plural label: Completed orders
  • Description: When an order is completed, this event is published with the order ID
  • Object name: “Completed_Order.”

The custom fields involved:

Field Name

API Name

Date Type

Order ID

“Order_Id__c”

“Text(50)”

  • The event has an order ID, so subscribers can query the information from the Salesforce database as needed. Extra attributes can be added as required.
  • An ISV sells many orders on any given day. Salesforce’s limits make it impractical to email customer receipts, so the software will use a third-party emailing system’s API.
  • Next, a completed order platform event Apex trigger will need to be created, subscribing to new completed order events and sending each customer their receipt.
  • A service class is used to call out the email system’s API.
public class CustomerReceiptService {

    public void sendReceipts(Set<String> orderIds) {
        // Code to send the email receipts using the email system's API(s).
    }
    
}
  • The completed order platform event Apex trigger will need to be created as it invokes that service to send the email receipts.
trigger CompletedOrderTrigger on Completed_Order__e (after insert) {
    Set<String> completedOrderIds = new Set<String>();
    
    for (Completed_Order__e completedOrder : Trigger.New) {
        completedOrderIds.add(completedOrder.Order_Id__c);
    }
    
    CustomerReceiptService crs = new CustomerReceiptService();
    
    crs.sendReceipts(completedOrderIds);
}
  • Your order management code publishes the completed order event, and the subscribers can then use it to perform the actions needed.
  • When an order is completed, order management logic can be separated from all the functionality. This logic allows completed order functionality to be implemented by adding more subscribers.

Get ready to extend your managed packages

Following the managed package steps and parameters, you can create anything for your organization.

You can customize your products, change pricing, and even provide accurate and up-to-date receipts.

🌈
Extensibility is limitless, so treat your managed packages as if the sky’s the limit.

Need more help?

Discover Managed Packages
do's and dont's

Spring Release Updates
Managed Package Extensibility
The Spring '24 release introduces enhancements to make Salesforce managed packages more extensible and customizable. Developers can now leverage application settings, dependency injection, and configurable user interfaces to create personalized customer experiences within managed packages.
Coding Patterns
Apex developers can utilize coding patterns like Dependency Injection and Inversion of Control to modify managed package behavior at runtime. Examples include using methods like Type.ForName and Type.newInstance to dynamically invoke classes within managed packages1.
Customization Techniques
The release provides guidance on implementing extensibility in managed packages. Techniques include creating data structure classes, using global methods, and leveraging publish-and-subscribe patterns for event-driven actions1.

Last updated: 26 Jun 2024