Safely Upgrade Packages from Developer and Subscriber Perspectives
These sections follow an example package developer and package subscriber as they move through a managed package upgrade process.
1. Package Developer: Publishes Version 1.0
The package developer publishes version 1.0 of a 1GP managed package in the eshop namespace. The package contains Apex CustomCart and CartCalculator classes.
1/**
2 * CustomCart
3 * Simple container for item prices used in a managed package context.
4 * @version 1.0
5 * @since 1.0
6 */
7global with sharing class CustomCart {
8
9 global List<Decimal> itemPrices;
10
11 global CustomCart() {
12 this.itemPrices = new List<Decimal>{0.0};
13 }
14}1/**
2 * CartCalculator
3 * Handles tasks about calculating items and prices in customer carts.
4 * @version 1.0
5 * @since 1.0
6 */
7global virtual class CartCalculator {
8 /**
9 * Adds item prices in a custom cart.
10 * @param c A CustomCart object that represents a list of items that the customer
11 * wants to buy
12 * @return A Decimal object that represents the total price of items in the
13 * cart
14 * @version 1.0
15 * @since 1.0
16 */
17 global virtual Decimal getTotalPrice(CustomCart c) {
18 Decimal price = 0.0;
19 // Add up items in cart
20 for (Decimal itemPrice : c.itemPrices) {
21 price += itemPrice;
22 }
23 return price;
24 }
25}2. Package Subscriber: Adds Functionality by Overriding a Method
The package subscriber installs version 1.0 of the managed package, but they find that the existing CartCalculator class is inadequate. They want the ability to factor shipping costs into the total cart price.
So, the subscriber extends the CartCalculator class in the managed package with a custom CartCalculatorWithShipping class. They override the getTotalPrice() method so that the total price includes the shipping cost.
1// Package Subscriber - CartCalculatorWithShipping.cls
2
3/**
4* Handles tasks about calculating items and prices in customer carts,
5* including shipping costs.
6*/
7public with sharing class CartCalculatorWithShipping extends eshop.CartCalculator {
8
9 /**
10 * Adds item prices in a cart and adds the shipping cost to the total price.
11 * @param c A CustomCart object that represents a list of items that the customer
12 * wants to buy
13 * @return A Decimal object that represents the total price of items in the
14 * cart, including the shipping cost
15 */
16 public override Decimal getTotalPrice(eshop.CustomCart c) {
17 return super.getTotalPrice(c) + getShippingCost(c);
18 }
19
20 /**
21 * Get the shipping cost based on the items in a customer's cart
22 * @param c A CustomCart object that represents a list of items that the customer
23 * wants to buy
24 * @return A Decimal object that represents the total shipping cost
25 * for the cart
26 */
27 public Decimal getShippingCost(eshop.CustomCart c) {
28 // Flat rate shipping
29 return 20.0;
30 }
31
32}3. Package Developer: Releases Version 2.0 and Implements the Subscriber’s Custom Functionality
The package developer releases version 2.0 of the managed package. In this version, the CartCalculator class now includes a native shipping cost calculator. The updated getTotalPrice() method calls the new getShippingCost() method. Notice that the package developer uses the same method name for getShippingCost() as the subscriber does for their custom override method.
1// Package Developer - CartCalculator.cls
2
3/**
4* Handles tasks about calculating items and prices in customer carts.
5* @version 2.0
6* @since 1.0
7*/
8global virtual class CartCalculator {
9 /**
10 * Adds item prices in a cart, including the shipping cost
11 * @param c A CustomCart object that represents a list of items that the customer
12 * wants to buy
13 * @return A Decimal object that represents the total price of items in the
14 * cart, including the total shipping cost
15 * @version 2.0
16 * @since 1.0
17 */
18 global virtual Decimal getTotalPrice(CustomCart c) {
19 Decimal price = 0.0;
20 // Add up items in cart
21 for (Decimal itemPrice : c.itemPrices) {
22 price += itemPrice;
23 }
24 return price + getShippingCost(c);
25 }
26
27 /**
28 * Get the shipping cost based on the items in a customer's cart
29 * @param c A CustomCart object that represents a list of items that the customer
30 * wants to buy
31 * @return A Decimal object that represents the total shipping cost
32 * for the cart
33 * @version 2.0
34 * @since 2.0
35 */
36 global virtual Decimal getShippingCost(CustomCart c) {
37 // Flat rate shipping
38 return 20.0;
39 }
40}4. Package Subscriber: Upgrades to Version 2.0 Without Specifying a Package Version for the Apex Class
By default, an Apex class or trigger is associated with the version of the managed package installed when the class or trigger was most recently deployed.
In this example, the package subscriber created and saved the CartCalculatorWithShipping class when the eshop managed package was on version 1.0. If the package subscriber upgrades their eshop managed package to version 2.0, and doesn’t redeploy the CartCalculatorWithShipping class, then that class is still associated with version 1.0 of the managed package.
Let’s say that the package subscriber upgrades their eshop managed package to version 2.0, but does try to redeploy CartCalculatorWithShipping. In this case, the subscriber encounters this compilation error: Method must use the override keyword: public Decimal getShippingCost(CustomCart c).
This error occurs because there’s a mismatch in the shape of the API. The subscriber’s original CartCalculatorWithShipping class has a getShippingCost() method, and the CartCalculator class in version 2.0 of the managed package now also includes a getShippingCost() method. The subscriber didn’t specify a package version for the CartCalculatorWithShipping class, so upon redeployment, the class is now associated with version 2.0 of the managed package. Therefore, the subscriber’s getShippingCost() method technically overrides the getShippingCost() method in CartCalculator, and so the Apex compiler requires an override keyword for the method.
5. Package Subscriber: Sets Apex Class to Package Version 1.0
To avoid this compilation error, the package subscriber explicitly sets a package version for the CartCalculatorWithShipping class. When is set to a specific package version, the class views the package’s global Apex as if that version was installed.
In this case, setting CartCalculatorWithShipping to version 1.0 of the managed package avoids a compilation error because the package’s CartCalculator class doesn’t define a getShippingCost() method until version 2.0. As long as the CartCalculatorWithShipping class is set to an earlier package version, the package’s CartCalculator class doesn’t expose the getShippingCost() method to the subscriber. Therefore, the Apex compiler doesn’t flag the subscriber’s own getShippingCost() method as needing to override the method in the managed package.
To override the default package version for an Apex class or trigger, use the Salesforce Setup UI or the packageVersions field of the class’s ApexClass metadata type. See Set Package Versions for Apex Classes and Triggers.
For example, here’s the metadata file for the subscriber’s CartCalculatorWithShipping class, where the class is set to version 1.0 of the managed package. Because the package is a 1GP managed package, the namespace is specified instead of the package ID.
1<!-- CartCalculatorWithShipping.cls-meta.xml -->
2<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3 <apiVersion>66.0</apiVersion>
4 <status>Active</status>
5 <packageVersions>
6 <namespace>eshop</namespace> <!-- For only 1GP -->
7 <majorNumber>1</majorNumber>
8 <minorNumber>0</minorNumber>
9 </packageVersions>
10</ApexClass>6. Package Subscriber: getTotalPrice() Returns an Incorrect Value
The subscriber sets the CartCalculatorWithShipping class to version 1.0 of the managed package and successfully recompiles the class. However, the subscriber now encounters a new issue at run time: the getTotalPrice() method in the CartCalculatorWithShipping class returns the wrong total price.
Recall that specifying an earlier package version for an Apex class or trigger hides globals that are defined in later versions during compilation. In other words, setting a package version preserves the shape of the API. However, it doesn’t necessarily preserve the behavior of the API at run time.
In version 1.0, the getTotalPrice() method in the CartCalculator class returns only the base price. But in version 2.0, the getTotalPrice() method now returns the price plus the result of getTotalShippingCost(). The getTotalPrice() method exists in both versions of the managed package, even though its behavior differs. Therefore, even if the subscriber sets the CartCalculatorWithShipping class to version 1.0, the getTotalPrice() executes with its version 2.0 behavior at run time.
Remember that the subscriber’s getTotalPrice() method in their CartCalculatorWithShipping class overrides the native getTotalPrice() method in the managed package. The subscriber’s override method adds the getShippingCost() result to the result of the native getTotalPrice() method. In version 2.0 of the managed package, the native getTotalPrice() method already adds the shipping cost, so the shipping cost is erroneously added twice.
1// Package Developer - CartCalculator.cls (v2.0) (code unchanged)
2global virtual class CartCalculator {
3
4 global virtual Decimal getTotalPrice(CustomCart c) {
5 Decimal price = 0.0;
6 // Add up items in cart
7 for (Decimal itemPrice : c.itemPrices) {
8 price += itemPrice;
9 }
10 return price + getShippingCost(c);
11 }
12
13 global virtual Decimal getShippingCost(CustomCart c) {
14 // Flat rate shipping
15 return 20.0;
16 }
17
18}
19
20// Package Subscriber - CartCalculatorWithShipping.cls (code unchanged)
21public with sharing class CartCalculatorWithShipping extends eshop.CartCalculator {
22
23 // Now returns the wrong price because getShippingCost is added twice
24 public override Decimal getTotalPrice(eshop.CustomCart c) {
25 return super.getTotalPrice(c) + getShippingCost(c);
26 }
27
28 public Decimal getShippingCost(eshop.CustomCart c) {
29 // Flat rate shipping
30 return 20.0;
31 }
32}To resolve this problem without requiring the subscriber to change their code, the package developer must version the behavior of Apex classes and triggers in the package.
7. Package Developer: Implements Backward Compatibility with System.requestVersion()
After the package subscriber informs the package developer about the unexpected getTotalPrice() behavior, the package developer releases a patch update. Version 2.1 of the package allows the subscriber to keep their original CartCalculatorWithShipping class by implementing backwards compatibility with System.requestVersion().
Here’s version 2.1 of the CartCalculator class that contains an updated getTotalPrice() method. In the method, a callerVersion variable is set to System.requestVersion(), which returns a Version object that represents the managed package version of the calling class. A minVersionWithShippingCost variable is set to the managed package version that introduced the changed getTotalPrice() behavior.
Then, the Version.compareTo() method compares callerVersion and minVersionWithShippingCost. If the caller version is earlier than the version that the shipping cost feature was introduced in, then getTotalPrice() returns the price. This value aligns with the original behavior in version 1.0 of the managed package. If the caller version matches or is later than the version that the shipping cost feature was introduced in, then getTotalPrice() returns the price addition to the shipping cost.
1// Package Developer - CartCalculator.cls
2
3
4/**
5* Handles tasks about calculating items and prices in customer carts.
6* @version 2.1
7* @since 1.0
8*/
9global virtual class CartCalculator {
10
11
12 /**
13 * Adds item prices in a cart.
14 * @param c A CustomCart object that represents a list of items that the customer
15 * wants to buy
16 * @return A Decimal object that represents the total price of items in the
17 * cart. Total price includes the shipping cost for v2.0 and later.
18 * @version 2.1
19 * @since 1.0
20 */
21 global virtual Decimal getTotalPrice (CustomCart c) {
22 Decimal price = 0.00;
23 // Add up items in cart
24 Version callerVersion = System.requestVersion();
25 Version minVersionWithShippingCost = new Version(2, 0);
26 if (callerVersion.compareTo(minVersionWithShippingCost) < 0) {
27 // callVer < minVer that you introduced the shipping cost feature in
28 return price;
29 } else {
30 return price + getShippingCost(c);
31 }
32 }
33
34
35 /**
36 * Get the shipping cost based on the items in a customer's cart
37 * @param c A CustomCart object that represents a list of items that the customer
38 * wants to buy
39 * @return A Decimal object that represents the total shipping cost
40 * for the cart
41 * @version 2.1
42 * @since 2.0
43 */
44 global virtual Decimal getShippingCost(CustomCart c) {
45 return 20.00;
46 }
47}By versioning the behavior of getTotalPrice(), the package developer has implemented basic backward compatibility for the class. Now, as long as package subscribers set Apex classes to the desired managed package version, then their existing implementations won’t break when they upgrade from version 1.0 to version 2.1 of the package.
8: Package Developer: Tests Backward Compatibility with System.runAs()
To ensure that getTotalPrice() now behaves differently based on the package version of the calling code, the package developer can use System.runAs() in their unit tests. This method, which can only be used in test methods, changes the current package version to the package version specified in the argument Here’s a basic unit test that the package developer implements for getTotalPrice().
1// Package Developer - CartCalculatorTest.cls
2@isTest
3private class CartCalculatorTest {
4
5 private static final List<Decimal> prices = new List<Decimal>{
6 10.0,
7 20.0,
8 30.0
9 };
10
11 @isTest
12 static void testGetTotalPrice_WithShippingCost() {
13 CustomCart cart = new CustomCart();
14 cart.itemPrices = prices;
15
16 CartCalculator calculator = new CartCalculator();
17
18 //Version 2.0 includes the shipping cost calculation
19 System.runAs(new Version(2, 0)) {
20 Decimal totalPrice = calculator.getTotalPrice(cart);
21 // The expected total is sum of item prices (60.0) plus the shipping cost (20.0)
22 Assert.areEqual(80.0, totalPrice, 'The total price should be 80.0');
23 }
24 }
25
26 @isTest
27 static void testGetTotalPrice_WithoutShippingCost() {
28 CustomCart cart = new CustomCart();
29 cart.itemPrices = prices;
30 CartCalculator calculator = new CartCalculator();
31 // Version 1.0 doesn't include the shipping cost calculation
32 System.runAs(new Version(1, 0)) {
33 Decimal totalPrice = calculator.getTotalPrice(cart);
34 // The expected total is the sum of item prices (60.0)
35 Assert.areEqual(60.0, totalPrice, 'The total price should be 60.0');
36 }
37 }
38}Summary: Shared Responsibilities for Safe Package Upgrades
The extended example demonstrates that the package developer and package subscriber both play a role in ensuring safe package upgrades. Here’s a table that summarizes the recommended actions that each actor can take so that the package can evolve without compromising subscriber implementations.
| Goal | Actor | Action |
|---|---|---|
| Version API Shape | Package Subscriber |
Be aware of the default versioned behavior: an Apex class or trigger is associated with the version of a managed package installed when that class or trigger was most recently deployed or saved. If necessary, override the default by explicitly setting dependent Apex classes and triggers to a specific package version. |
| Version API Behavior | Package Developer | Version changed behavior with System.requestVersion(), and test it with System.runAs(). See Version Apex in Managed Packages. |