The short answer: @deprecated stops new subscriber code referencing a global member. It does not remove anything. Once a global class, method, interface or enum ships in a released managed package, it exists in every subscriber org permanently and the implementation has to keep working. Deprecation is a signal to your users and a compile gate on new code, nothing more.
What the annotation does
global class InvoiceService {
@deprecated
global static void postInvoice(Id invoiceId) {
postInvoices(new List<Id>{ invoiceId });
}
global static void postInvoices(List<Id> invoiceIds) {
// current implementation
}
}
A subscriber who installs the version containing that annotation, or any later version, cannot write new Apex calling postInvoice. Their existing code that already references it keeps compiling and keeps running, because it was compiled against an earlier version of your package.
That split is the whole design. You stop the bleeding without breaking anyone.
What you can never take back
The rule that governs package API design: anything global in a released managed package is permanent.
- Global methods and classes cannot be deleted. Deprecating them does not delete them.
- Global interfaces cannot gain methods after release, because every subscriber implementation would stop compiling.
- Method signatures cannot change. A new parameter means a new method.
- Enum values cannot be removed, and adding one can still surprise a subscriber whose switch statement has no default branch.
- Custom fields and objects in a 1GP managed package cannot be deleted from subscriber orgs at all. 2GP supports removing a limited list of metadata types, and Apex global members are not on it.
The obvious conclusion is to be stingy with global. Every global member is an API you have promised to support indefinitely, so make things global because you intend them to be called from outside, not because it was the modifier that made the compile error go away. In second-generation packaging, @NamespaceAccessible covers the case where another package of yours needs access without making the member part of your public surface, and it is the right tool far more often than global is.
Deprecated code still has to work
The most common mistake is treating deprecation as removal and letting the implementation rot. Subscribers who compiled against an older version are still calling it. If the method starts throwing, or silently stops doing anything, they get a production failure from a release they did not ask for.
Keep the deprecated method delegating to the current one, as in the example above. Keep its tests. If behaviour genuinely cannot be preserved, that is not deprecation, it is a breaking change, and it needs the conversation with customers that breaking changes need.
Versioned behaviour
Sometimes the answer is not a new method but different behaviour for subscribers on different versions. System.requestVersion() returns the version of your package that the calling code was compiled against:
Version caller = System.requestVersion();
if (caller.compareTo(new Version(3, 0)) < 0) {
return legacyTotal(lines);
}
return totalWithTax(lines);
Subscriber Apex, Visualforce pages and components each carry a version setting that pins which version of your package they were built against, and that is what this reads. It lets you change a calculation in version 3 without changing the answer for anyone still on version 2.
Use it sparingly. Every version branch is a path you maintain and test forever, and a class with four of them is one nobody wants to touch.
Doing a deprecation properly
- Ship the replacement first, in an earlier version than the deprecation. Subscribers need somewhere to go before you tell them to leave.
- Add
@deprecatedin a later version, with the replacement named in the release notes and in a comment on the member itself. - Leave it working. Indefinitely, in a managed package.
- Where you can see usage, through your own telemetry or support cases, tell the remaining callers directly rather than assuming release notes reached them.
Note what is not on that list: a removal date. In a managed package there is no removal, so a deprecation notice promising one is a promise you cannot keep.
Frequently asked
Does @deprecated work in an unlocked package? The annotation is for managed packages. In an unlocked package you can simply delete the code, which is the freedom you traded away when you chose managed.
Can I deprecate a whole class? Yes, the annotation goes on the class, and it covers the members within it.
What about a webService method? It is part of the WSDL your subscribers generated clients from. Treat any change to one as breaking an integration you cannot see, regardless of what the code allows.
Do deprecated members still count towards coverage? Yes. They are ordinary Apex, they need tests, and those tests are also what stops you accidentally breaking them.
