Best Practices for Using Global Apex in Managed Packages
The global modifier provides the widest level of access. It makes your Apex classes, methods, interfaces, and variables accessible and callable from any Apex code outside of your package namespace, including code written by subscribers or other installed packages.
A key benefit of Apex in managed packages is Intellectual Property (IP) protection. Although global Apex signatures, such as global class and method names, parameters, and return types, are visible so subscribers can use your API, the underlying implementation logic within your methods is encapsulated and hidden. This allows you to provide powerful functionality without exposing your proprietary source code.
When to Use Global Apex in Managed Packages
Use global Apex when:
- Exposing a global API. You intend for subscribers to use your package’s core services by directly calling your Apex methods or instantiating your classes from their own Apex code, such as from their triggers, batch classes, or other custom logic.
- Providing extensibility points. You’re designing global interfaces or global abstract base classes that subscribers must implement or extend to customize or enhance specific behaviors within your app.
- Creating web service endpoints. You’re exposing Apex classes to handle incoming REST API requests or SOAP web service calls from external systems. These endpoint classes and their methods must be declared global to receive calls from external systems.
Avoid global Apex for:
- Internal package functionality, meaning any Apex code, such as business logic, helper classes, or utility methods, that’s designed exclusively for use within your managed packages. For these internal components, access modifiers such as public, protected, or private avoid the manageability constraints of global. You can even combine the public access modifier with the @NamespaceAccessible annotation to allow access throughout your package namespace.
- Controller methods, or methods within your package that are called by your Lightning web components (LWC) and aren’t used by your subscribers directly. Make these methods public and annotate them with @AuraEnabled.
- Method signatures with external global types. Avoid using global Apex types from another managed package as parameters or return types in your own global methods. This practice locks your package into a rigid dependency. If the other package later changes or deprecates the types that your global signature relies on, it can become difficult or impossible for your package to adopt newer versions of the dependency package.
Global Apex Manageability Rules
The decision to use global is significant because it subjects your code to strict manageability rules after your package is released. Manageability rules protect subscribers against package upgrades that break existing functionality. These rules make your initial global Apex design critical, as changes in later versions are heavily restricted.
After your managed package is released, these restrictions apply to the global Apex in the package.
- You can’t delete a global class, interface, method, variable, or enum.
- You can’t change the name of a global class, interface, method, variable, or enum.
- You can’t change the access modifier of a global class, interface, method, variable, or enum from global to another access modifier.
- You can’t change a global method’s signature, including the order, number, and types of parameters, as well as the method’s return type. You can add new methods that overload existing global methods.
- You can’t change the value of a global static final variable.
- You can’t change the data type of a global variable to an incompatible one.
- You can’t change most annotations or modifiers on global members. For example, you can’t add or remove the @AuraEnabled annotation or any Apex REST annotation from a global method. Similarly, you can’t add or remove the static or final keyword from a global method.
- You can’t remove a zero-argument constructor from a global class. This restriction applies both to explicitly deleting a constructor you wrote, and to implicitly removing the default constructor by adding a new constructor that requires arguments.
- You can’t remove a global interface from a global class.
- You can’t extend a global interface with a new interface.
- You can’t change a global class to an interface, or vice versa.
- You can’t add abstract methods to global interfaces or abstract classes.
- You can’t add final methods to global virtual or global abstract classes.
- You can’t remove or reorder global enum values, although you can add new enum values.
- You can't add, remove or change that a global class extends another global class.
Given the strict manageability rules, a thoughtful design of your package’s global API is crucial. These best practices for defining and managing global Apex promote flexibility, reduce maintenance, and help create a stable, evolvable API.
Only Use Global When Necessary
When designing global Apex, expose the fewest global members possible. Every global part of your package is a contract with your subscribers. Fewer global members mean fewer restrictions and more freedom for you to update your package later.
Before making something global, consider these questions.
- Is it truly necessary? Can you use public instead? For example, use public for access only by other Apex within the same package, or public and @NamespaceAccessible for access only by other Apex in packages that have the same namespace.
- Are there other designs? Can you achieve the same result without making methods or classes global?
Delegate from Thin Global Entry Points
Make your global classes and methods act only as thin entry points. Place any business logic or complex processes in public classes and methods within your package. The global method then calls these public methods.
Delegating logic to public classes and methods promotes:
- Flexibility. You can freely add new features or completely change the internal public implementation of your service in future versions without altering the global method signature.
- Maintainability. Fixing bugs and refactoring your existing business logic is now safer and lower-risk because the work is decoupled from the stable global API contract that your subscribers depend on.
- Testability. Your public business logic classes can be unit tested thoroughly and independently of their global entry point.
Example
In this example, MyPackageApi.greetUsers is the stable global contract. The actual work happens in createGreetings, which is public and can be easily modified in future package versions as long as the greetUsers signature in MyPackageApi remains unchanged. For example, the ISV developer can later optimize createGreetings or add new helper methods without violating global Apex manageability rules.
1// --- ISV's Managed Package Code ---
2// Global Entry Point
3global with sharing class MyPackageApi {
4 // Note: This is a simplified example using a primitive data type. For a
5 // more flexible and future-proof design, we recommend using parameter
6 // objects for global method signatures, as explained in the next section.
7 global static List<String> greetUsers(List<String> userNames) {
8 // Delegate directly to a public class to do the actual work.
9 // GreetingService is a 'public' class within your package.
10 GreetingService service = new GreetingService();
11 return service.createGreetings(userNames);
12 }
13}
14
15// Public Class (Lives inside your package)
16// This class can be updated easily in future package versions.
17public with sharing class GreetingService {
18 // This public method contains all the business logic to process a list of names.
19 public List<String> createGreetings(List<String> names) {
20 List<String> greetings = new List<String>();
21 // Input validation and error handling, e.g. Return an empty list if input is null or empty.
22 // Process each name in the list.
23 for (String name : names) {
24 greetings.add('Hello, ' + name + '!');
25 }
26 return greetings;
27 }
28
29 // If needed, you can freely add private methods here
30 private void someHelperMethod() {
31 // ...
32 }
33}
34
35
36// --- Subscriber Code ---
37// Subscriber creates a list and calls the global method
38List<String> welcomeMessages = TheIsvNamespace.MyPackageApi.greetUsers(
39 new List<String>{'Jane Doe', 'Rose Gonzalez'});
40// The output is a corresponding list of greetings.
41// welcomeMessages will be: ['Hello, Jane Doe!', 'Hello, Rose Gonzalez!']Use Parameter Objects for Global Method Inputs and Return Types
Using primitive data types, such as String or ID, directly in global method signatures creates a rigid contract that you can’t change. To avoid this restriction, we recommend using custom global Apex classes as parameter objects for both inputs and outputs. This pattern provides a stable API contract that’s evolvable, safer, and readable for subscribers.
Benefits of using parameter objects include:
- Evolvability. You can add new parameters to your API by adding new properties to your input or output classes. The global method signature itself never changes, which avoids breaking changes.
- Safety and discoverability. Subscribers get compile-time safety and code completion while constructing the request object, which reduces the risk of run-time errors from typos or incorrect data types.
- Readability. Grouping related parameters into a dedicated class makes your method signature clean and its purpose clear. For example, getCoordinates(Address location) is easier for your subscribers to understand and use than getCoordinates(String street, String city, String state, String zip, ...).
Despite the benefits of using parameter objects, consider these costs.
- Increased upfront complexity. This pattern requires more initial code, such as multiple classes, and can be excessive for simple actions that aren’t expected to change.
- Expanded global surface area. Although the primary method signature remains stable, each global property added to the input object becomes a new, permanent part of your package’s API.
Example
In this example, the GeocodingService.getCoordinates method is the global contract. It uses the custom global AddressRequest class as its input, and the custom global GeolocationResponse class as its output.
1// --- ISV's Managed Package Code ---
2// The global entry point class contains the global method and input & output wrapper inner classes.
3global with sharing class GeocodingService {
4 // 1. The Global Method
5 // The signature is clean, using the inner classes for its input parameter and return type.
6 global static GeolocationResponse getCoordinates(AddressRequest location) {
7 // --- Internal logic to process the request ---
8 // We recommend calling a public method to delegate all business logic. Skipping this for brevity in this example
9 // This could involve callouts to an external geocoding service.
10 // For this example, we will return mock data.
11 if (location.postalCode == '94105') {
12 return new GeolocationResponse(37.7749, -122.4194);
13 } else {
14 return new GeolocationResponse(0, 0);
15 }
16 }
17
18 // 2. The Input Parameter Object (as an inner class)
19 // A simple inner class with global properties to bundle input parameters.
20 global with sharing class AddressRequest {
21 global String street;
22 global String city;
23 global String state;
24 global String postalCode;
25
26 // A constructor helps ensure required fields are provided.
27 global AddressRequest(String street, String city, String state, String postalCode) {
28 this.street = street;
29 this.city = city;
30 this.state = state;
31 this.postalCode = postalCode;
32 }
33 }
34
35 // 3. The Output Parameter Object (as an inner class)
36 // A simple inner class to provide a structured result.
37 global with sharing class GeolocationResponse {
38 // Using 'private set' makes these properties read-only for subscribers.
39 // @AuraEnabled makes these properties visible to Lightning web components, if necessary.
40 @AuraEnabled global Decimal latitude { get; private set; }
41 @AuraEnabled global Decimal longitude { get; private set; }
42
43 // This constructor is public, not global, so subscribers can't create their own response objects.
44 public GeolocationResponse(Decimal lat, Decimal lon) {
45 this.latitude = lat;
46 this.longitude = lon;
47 }
48 }
49}
50
51// --- Subscriber Code ---
52// The subscriber's experience is clean and type-safe.
53// 1. Create an instance of the input object, referencing it via the outer class.
54TheIsvNamespace.GeocodingService.AddressRequest sfAddress = new TheIsvNamespace.GeocodingService.AddressRequest(
55 '415 Mission St',
56 'San Francisco',
57 'CA',
58 '94105'
59);
60
61// 2. Call the global method with the single parameter object.
62TheIsvNamespace.GeocodingService.GeolocationResponse coordinates = TheIsvNamespace.GeocodingService.getCoordinates(sfAddress);
63
64// 3. Process the structured, strongly-typed result.
65// For this example we will just log the results.
66 System.debug('Coordinates found: ' + coordinates.latitude + ', ' + coordinates.longitude);Alternative for Unpredictable Inputs: The Map Pattern
For scenarios where the inputs aren’t known at compile time, you can use a Map for your method’s input parameters. An example signature with this pattern is global static GeolocationResponse processUnstructuredRequest(Map<String, Object> inputs).
Accepting a Map allows the subscriber to construct the Map and populate the keys and values themselves. Although this pattern provides maximum flexibility, returning a Map forces the subscriber to guess key names and data types, which leads to fragile code.
Additionally, using a Map for inputs sacrifices compile-time safety and discoverability. Subscribers must rely entirely on documentation to know the required keys and their data types. Simple typos in map keys can result in hard-to-debug run-time errors instead of compile-time errors.
Instead of using the Map pattern, prefer strongly-typed parameter objects as shown in the Use Parameter Objects for Global Method Inputs and Return Types section. Reserve the Map pattern for advanced use cases where the inputs are truly unpredictable and its risks are acceptable, but always return a strongly-typed object.
Use Global Interfaces with Factory Methods
The facade pattern is effective if you want to define a capability and then provide one or more ways to use it. In this pattern, a global interface defines the capability, and the public classes that implement the interface define how to use the capability. A global factory method gives the subscriber the specific implementation of the interface.
Benefits of using the facade pattern include:
- You can change the public class that does the work as long as it still follows the global interface rules.
- You can offer new implementations of the interface later.
Example
In this example, Notifier is the global contract, and the public class EmailNotifierImpl does the work. Because the factory class is global but the classes that implement the Notifier interface are public, the ISV developer can later change or add to these classes. For example, if the ISV developer later wants to offer an SMS notifier, they can add getSmsNotifier() to the factory class and implement a new public SmsNotifierImpl class.
1// --- ISV's Managed Package Code ---
2// Global Interface - Defines a capability
3global interface Notifier {
4 global void send(String message);
5}
6
7// Global Factory Class - Provides instances
8global with sharing class NotificationFactory {
9 global static Notifier getEmailNotifier() {
10 return new EmailNotifierImpl(); // EmailNotifierImpl is public
11 }
12}
13
14// Public Implementation (Lives inside your package)
15public with sharing class EmailNotifierImpl implements Notifier {
16 public void send(String message) {
17 // Logic to send an email (simplified here)
18 System.debug('Emailing: ' + message);
19 }
20}
21
22// --- Subscriber Code ---
23// Subscriber gets an instance of a Notifier
24Notifier myEmailer = NotificationFactory.getEmailNotifier();
25
26// Subscriber uses the interface method
27myEmailer.send('Welcome to our service!'); // Outputs: Emailing: Welcome to our service!Strategies for Retiring Global Apex
Even though deprecating Apex only affects future versions of your package, phasing out a global Apex member requires careful planning and clear communication to prevent subscriber disruption.
Here’s some recommendations for effectively retiring a global Apex member.
- Communicate early and clearly. Inform subscribers well in advance—ideally, multiple release cycles—of any deprecation. Explain reasons, timelines, and migration paths. Update the documentation, and share it prominently.
- Provide alternatives. Release well-documented and robust alternatives before or concurrent with the deprecation announcement.
- Implement soft deprecation with non-breaking warnings.
- Add the @Deprecated annotation to the global member. This annotation generates compile-time warnings in developer tools, but doesn’t alter run-time behavior.
- Consider run-time logging when the deprecated member is invoked, guiding subscribers to the new alternative.
- Enforce non-operation with exceptions. Change the code to throw an informative exception, such as FeatureDeprecatedException('Method X is retired. Use Method Y.'). This breaking change at run time stops the old logic from running and forces attention to the deprecation. However, any breaking change requires extensive prior communication.
- Retire code for obsolete global Apex. After ample time and communication, confirm that subscribers are no longer using the deprecated global member. Then minimize the member’s internal code. Although the global signature must remain, its logic can become non-operational (no-op), return a safe default, or throw a specific Feature Retired exception. Implementing these changes reduces the risk and effort of maintaining the old code.
Always thoroughly test changes related to deprecating global Apex, including testing your package’s behavior if a subscriber attempts to call the deprecated member. The goal is a graceful transition for your subscribers.