Reviewed for accuracy May 2020

When developing a managed package, allowing a client to insert custom logic at runtime is often ideal or necessary. For example, a customer might need to perform a business-specific calculation or validation that is processed alongside your package’s business logic. The customer might also need to incorporate the result of a query, or perform a callout, or log tracking information alongside your business logic, etc.

If you are developing a suite of managed packages, a customer may decide to purchase one and not the other. The packages often must be designed to work well together while still allowing the customer to provide their own implementation of a subset of the suite.

One of the difficulties with custom packages is that they can know nothing of an end customer’s specific implementation. How does a customer therefore incorporate their custom business logic? Dependency injection is an approach to help address this.

If you are familiar with Java and in particular the Spring framework, you’ll be familiar with the concept of dependency injection. Basically, dependency injection occurs when the client, through the use of a container, passes at runtime a dependency into a class — allowing it to complete its task.

Interfaces

Interfaces are key to leveraging dependency injection. The principle is that the code is designed to interact with an interface, and the implementation of that interface can vary from customer to customer. The package is dependent at the time it is built on the interface, but has no direct dependency on the customer’s classes.

In the simplest case of dependency injection, the business logic will depend on a particular interface. In the following example, a package with namespace mynamespace defines an interface named DataSource.   The DataSource interface defines a single method, getSourceObjects() . The client is expected to provide their own custom implementation of the DataSource interface method and pass it to a package method, so that the package will operate on this implementation.

 

In the above example, the interface and service are defined in the package. This simple service takes a list of sObjects returned by the interface getSourceObjects method, serializes the result to JSON, and inserts the JSON into an Object called  ObjectStorage__c  . The custom ObjectStorage__c object is included as part of the package.

Custom Setting and Reflection

The disadvantage of this approach is that the client must manually pass this value to the specific service. This means that the service must be made global so that the DataSource property can be passed into it. This is where Custom Settings and Reflection can be used to remove any real need for the client code to know anything about the package code — while still allowing injection of a custom implementation. To accomplish this, we need to add a custom setting and an additional factory method that leverages the Type class.


The custom setting DataSourceConfig__c defines a parameter that contains the class name of the class implementing the IDataSource – in this case AccountDataSource . The Type.forName(className).newInstance() will instantiate an object of the class defined by the custom setting. Error handling was removed for clarity. Assuming this class implements the IDataSource interface (which in the above case it does), the getDataSource factory method will return the AccountDataSource class defined in the custom setting at runtime.

The service class can now simply refer to the DataSourceFactory class instead of needing the dependency passed into it. Because the client no longer needs to manually pass in the IDataSource implementation, the Service class doesn’t need to be made global. The IDataSource implementation can be switched at runtime simply by updating the Class_Name__c field in the Custom setting.

/* Class defined in Package with namespace mynamespace*/
public with sharing class DataSourceService {
   public void processRecords() {
       DataSourceFactory factory = new DataSourceFactory();
       IDataSource dsc = factory.getDataSource();
       List<SObject> objects = dsc.getSourceObjects();
       List<ObjectStorage__c> objectStorages = new  List<ObjectStorage__c>();
       for (SObject obj : objects) {
           ObjectStorage__c s = new ObjectStorage__c();
           s.Data__c = JSON.serialize(obj);
           objectStorages.add(s);
       }
       insert objectStorages;
   }
}

Adding New Methods and Dealing with Immutability

Once the global interface has been published in a released managed package, its definition becomes immutable. You cannot change, remove or add any method signatures on the interface. This is understandable since changes to an interface would violate the contract for any existing client.

There are 3 main ways around this:

1. Define new methods in new interfaces

The simplest approach is to define new methods in new interfaces. The following example shows how to add a new getSourceObjects () method to an interface.

/* Interface defined in Package with namespace mynamespace*/
global interface IDataSource {
   List<SObject> getSourceObjects();
}
/* Interface defined in Package with namespace mynamespace*/
global interface IDataSource2 {
   List<SObject> getSourceObjects(String param);
}

This approach is fine if the methods are completely unrelated. It gets a little confusing for the client when the methods are related (for example overloaded versions of the same basic method) because choosing the right interface to use at that point is unclear. The custom setting would need to define two fields to allow support to different implementations; however it could also be represented under the same (third) class by implementing both interfaces with both fields pointing to the same class implementation name.

2. Extend interfaces

/* Interface defined in Package with namespace mynamespace*/
global interface IDataSource {
   List<SObject> getSourceObjects();
}
/* Interface defined in Package with namespace mynamespace*/
global interface IDataSource2 extends IDataSource {
   List<SObject> getSourceObjects(String param);
}

This approach maintains backwards compatibility. Any existing code can continue to refer to the IDataSource interface. Any new code that needs to leverage the second method would simply implement IDataSource2 instead. The advantage of this approach is that it allows related methods to be grouped together and can make updates for the customer a little simpler. Since IDataSource2 is an IDataSource, a class that implements IDataSource2, it by definition also implements IDataSource .  Any code updated to function with the IDataSource2 interface will continues to work as an IDataSource implementation. However, this approach will get unwieldy after two to three extensions for the same interface.

3. Virtual Base class

The most flexible approach involves leveraging virtual base classes instead of Interfaces. Instead of exposing an IDataSource, the code would define the following class:

/* Class defined in Package with namespace mynamespace*/
global virtual with sharing class DataSourceClass {
   global virtual List<SObject> getSourceObjects() {
       return new List<SObject>();
   }
}

Any method that can be overridden by the client must be marked virtual. Since this class is not abstract it must provide an initial default (no-op) implementation of all virtual methods. Instead of implementing an interface, the client extends the virtual base class. The client only needs to override the methods of interest. All other methods will continue with their default implementation.

The advantage of this approach is that there are never any unimplemented methods. Adding a new method is as simple as adding a new virtual method and default implementation to the base class. It won’t break the contract with existing clients because new methods will always come with default implementations. New methods can therefore be added to the virtual global class without breaking compatibility. Existing published methods, however, can’t be removed or changed. This is the simplest way to add new methods, but it means that any client implementation must subclass the base class and therefore can’t be a subclass of any other class.

Considerations and implications

Global

Any subclass that implements a global Interface or the global base virtual class MUST be declared as global. The code will fail at runtime if the class is not marked as global because the package factory won’t have access to the implementation. If you encounter an unexplained NullPointerException this is quite possibly the cause.

Governor limits

The code will run in the same context as the calling code. This means that anybody implementing the global interface or subclassing the virtual class must be careful not to exceed governor limits. A poorly implemented subclass could cause the calling packaged methods to trip a governor limit.

Callouts

In Apex, callouts cannot occur in the same transaction after a DML operation. When attempting to make a callout after a DML operation, the following error will occur.

“You have uncommitted work pending. Please commit or rollback before calling out”

In the following example the above error could occur because there is a callout after the DataSource interface method. The client implementing the interface could not perform any DML in their implementation. If they do then the above error will be thrown.

/* Class defined in Package with namespace mynamespace*/
public with sharing class DataSourceService {
   public void processRecords() {
       DataSourceFactory factory = new DataSourceFactory();
       IDataSource dsc = factory.getDataSource();
       dsc.doSomeProcessing();
       Http h = new Http();
       HttpRequest req = new HttpRequest();
       req.setEndpoint('<some url>');
       req.setMethod('GET');
       HttpResponse res = h.send(req);    // Error thrown here if any DML occurs in the implementation of doSomeProcessing
   }
}

Conversely, if the code performs DML prior to calling the client method, the client method cannot make a callout.

Ideally, the package code should be set up to limit the impact of these interactions. For example restructuring the call this way would eliminate the error:

/* Class defined in Package with namespace mynamespace*/
public with sharing class DataSourceService {
   public void processRecords() {
       // Make a callout
       Http h = new Http();
       HttpRequest req = new HttpRequest();
       req.setEndpoint('<some url>');
       req.setMethod('GET');
       HttpResponse res = h.send(req);   // No error this time
 
       DataSourceFactory factory = new DataSourceFactory();
       IDataSource dsc = factory.getDataSource();
       dsc.doSomeProcessing();
   }
}

Any limitations around performing DML operations or callouts in the customer implementation should be noted in API documentation.

Support

When allowing a customer to incorporate their own extensions in this manner, it is vital to supply a default implementation. If customers log a ticket due to either poor performance, governor limit issues or any other implementation issues, it must be simple for the support staff to help the customer revert back to a known good supported implementation. Being able to quickly identify an issue that resides with the ISV or the customer will be key to customer success.


Looking for a development partner that knows the ins and outs of coding for Salesforce? Our team is an industry leader in helping companies build great apps from scratch or improve their current offerings. We’d love to hear about what’s next for development at your organization.