understood before the next upgrade breaks
A Magento upgrade that suddenly tears apart your own modules almost always has the same root cause: somewhere in the code, a class was extended or overridden that Adobe never promised as a stable interface. Whoever understands what the @api tag means and why plugins are the safe extension method builds modules that survive Magento updates without nasty surprises.
Table of Contents
- 1. What Magento's Backward Compatibility Policy regulates
- 2. The @api tag: what counts as a stable interface
- 3. Why plugins are the BC safe extension method
- 4. Reading deprecation in Magento core correctly
- 5. What the BC policy explicitly does not protect
- 6. Implications for your own modules: protecting yourself
- 7. Spotting BC breaks in minor updates early
- 8. Practical checklist before every Magento upgrade
- 9. BC compliant vs. risky extension practice
- 10. Summary
- 11. FAQ
1. What Magento's Backward Compatibility Policy regulates
Adobe's Backward Compatibility Policy for Magento 2 defines which parts of the framework count as a stable interface that stays unchanged long term, and which can be changed at any time without warning. This is not an academic nuance, it is the practical foundation for building your own modules and customizations so that a Magento minor or patch update does not destroy them. Without this understanding, every upgrade feels like a gamble.
The core of the policy is stated simply: classes, interfaces and methods marked as @api count as a public interface and will not be changed incompatibly within a MAJOR version. Everything else, no matter how stable it looked for years, can change with any patch release. This Backward Compatibility guarantee applies exclusively to code marked as API, not to the codebase as a whole.
Adobe publishes the details of this policy openly as part of the Magento developer documentation, including a list of the criteria by which a class is classified as API. Once these criteria are internalized, they can be applied immediately to any Magento class, without having to look them up again for every single decision.
For agencies maintaining many Magento shops over years, knowing these rules is the difference between a predictable upgrade process and recurring firefighting after every patch release. Whoever knows exactly what Magento actually commits to can build their own extensions deliberately against precisely those guarantees, instead of relying on internal code that only appears stable by chance.
This policy is also not a purely technical detail, it has direct economic consequences. Every hour that must be spent on an upgrade because an unprotected internal class changed is an hour not available in the project budget for new features. Teams that factor in Backward Compatibility from the start shift that effort from reactive troubleshooting after every update to a one time, deliberate architecture decision.
2. The @api tag: what counts as a stable interface
The @api tag in the PHPDoc block of a class or interface is the central signal of Magento's Backward Compatibility Policy. If a class is annotated with @api, Adobe promises that its public signature stays stable within the same MAJOR version. Service contracts such as \Magento\Catalog\Api\ProductRepositoryInterface are the classic example: they are consistently marked with @api and exist specifically to be used by custom code.
If the @api tag is missing, a class is implicitly treated as an internal implementation detail, even if it is public and has been unchanged for years. This is exactly what surprises many developers: visibility in the PHP sense and stability in the sense of the Backward Compatibility Policy are two completely independent properties. A public method without @api can change in any patch release without that counting as a breaking change.
<?php
declare(strict_types=1);
namespace Magento\Catalog\Api;
/**
* Product repository interface.
*
* @api
*/
interface ProductRepositoryInterface
{
public function get($sku, $editMode = false, $storeId = null, $forceReload = false);
public function save(\Magento\Catalog\Api\Data\ProductInterface $product, $saveOptions = false);
public function delete(\Magento\Catalog\Api\Data\ProductInterface $product);
}
// Classes WITHOUT @api, even if public, are considered internal
// implementation details and may change in any patch release.
Practical consequence: before a custom extension references a Magento class, it pays to check the docblock. If @api is there, the reference is safe in terms of the Backward Compatibility Policy. If the tag is missing, the dependency should either be avoided or at least deliberately documented as a risk, so a later upgrade does not come as a surprise.
The @api tag can also be evaluated automatically. A simple script that reads the docblocks of all Magento classes referenced in a custom module and checks for the presence of @api makes the risk assessment reproducible instead of repeating it manually case by case in code review. Larger agencies often embed this check directly into their own CI pipeline and run it as a standalone report alongside PHPStan and the static tests.
3. Why plugins are the BC safe extension method
A plugin (interceptor) hooks into the flow of a method marked as @api by method name, without knowing or touching its internal implementation. That makes plugins the preferred extension method under Magento's Backward Compatibility Policy: as long as the public method signature stays stable, the plugin keeps working regardless of how the internal implementation of the method changes across versions.
This mechanism works because Magento's Object Manager places a generated interceptor between caller and target class at runtime. The interceptor only knows the public signature of the method, never its body. This exact indirection is the technical reason why plugins are so robust against internal refactoring: Adobe can swap out the complete internal logic of a method as long as parameters and return type stay the same, without a single plugin needing to be adjusted.
A preference (class override via preference in di.xml) or direct inheritance from a core class behaves fundamentally differently. It binds itself to the complete internal structure of the parent class, including private and protected methods, which are never covered by the Backward Compatibility Policy. If Adobe changes an internal method or property in a patch release, the custom preference breaks, without Magento treating that as a policy violation.
<!-- app/code/Mironsoft/LoyaltyPoints/etc/di.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<!-- SAFE: plugin only depends on the stable, @api-marked public interface -->
<type name="Magento\Catalog\Api\ProductRepositoryInterface">
<plugin name="Mironsoft_LoyaltyPoints::afterSave" type="Mironsoft\LoyaltyPoints\Plugin\ProductRepositoryPlugin"/>
</type>
</config>
The rule of thumb is therefore clear: plugins on @api interfaces are the safe choice, preferences on classes without @api are a deliberately accepted risk, and direct inheritance from core classes without @api is the riskiest practice of all, because it binds itself to internal details that are never guaranteed. This priority order is not a style question, it follows directly from what Magento's Backward Compatibility Policy actually promises.
4. Reading deprecation in Magento core correctly
Before Adobe actually removes a class or method marked @api, it first gets marked with @deprecated, often supplemented with a @see reference to the recommended alternative. This deprecation phase is part of the Backward Compatibility Policy: a method marked deprecated but still considered @api stays functional within the current MAJOR version, but is guaranteed to disappear in the next MAJOR version.
The decisive mistake many teams make is ignoring @deprecated warnings as long as the code keeps running. That exact habit backfires at the next MAJOR upgrade, when suddenly several dependencies marked as deprecated get removed at once, turning a single upgrade into a large refactoring project instead of something handled incrementally across several minor releases.
PHPStorm and other IDEs display @deprecated markers as strikethrough text by default, which noticeably increases day to day visibility, but only helps if developers actually treat that visual warning as a call to action rather than ignoring it as cosmetic detail. A brief note during team onboarding that strikethrough code actively signals migration need prevents many later surprises.
<?php
declare(strict_types=1);
namespace Magento\Framework\App\Config;
/**
* @deprecated 101.0.0 Use ScopeConfigInterface::getValue() with explicit scope instead.
* @see \Magento\Framework\App\Config\ScopeConfigInterface::getValue()
* @api
*/
public function getConfigDataValue($path, $default = null)
{
// Still functional in the current major version, but scheduled
// for removal. Migrate proactively instead of waiting for the break.
}
A regular grep for @deprecated across the vendor classes in use, tied to a firmly scheduled migration window, prevents technical debt from silently piling up. Good practice is to log every found deprecation as a backlog ticket, instead of discovering it only at the next forced MAJOR upgrade.
5. What the BC policy explicitly does not protect
A common misunderstanding is that Magento's Backward Compatibility Policy covers the entire codebase. In reality, private and protected methods, internal helper classes, classes under namespaces like *\Model\ResourceModel\* without @api, and all implementation details behind a service contract are explicitly excluded. These areas can change in any patch release, even if they appeared stable for years.
Particularly tricky are layout XML handles, block classes without @api, and template files themselves: they carry no formal Backward Compatibility guarantee, even though many themes and modules access them heavily. A theme that copies a core template via copy-over binds itself to that template's exact structure at the moment of copying and receives no update protection whatsoever if Adobe changes the original template later.
Database tables and their column structure generally do not count as protected API either, even when managed declaratively through db_schema.xml. Direct SQL access to core tables bypassing a repository or a collection is therefore one of the most underestimated sources of upgrade breakage, because Adobe can adjust table structures as part of performance optimizations or data model changes without separate announcement.
JavaScript modules and Knockout templates in the frontend also follow no formal Backward Compatibility assurance, even though many Hyvä and Luma extensions access them directly. Whoever extends a RequireJS module via a mixin should apply the same caution as with PHP preferences: the tighter the coupling to internal implementation details, the greater the risk at the next frontend update.
6. Implications for your own modules: protecting yourself
The practical consequence for your own modules is a deliberate dependency strategy: every reference to Magento core code should be evaluated for whether it hits an @api target. For @api targets, a plugin or direct use of the interface via dependency injection is safe. For non-@api targets, either an alternative through the service contract should be sought, or the dependency gets deliberately documented with a comment pointing out the risk.
This strategy applies equally to your own module architecture: modules used by multiple consumers should themselves consistently distinguish between public interfaces marked with @api and internal implementation details. Whoever enforces this separation in their own code from the start can transfer Magento's Backward Compatibility mindset one to one onto their own module family and benefit from the same update safety expected from Adobe.
A simple first step is to consistently set @api on every interface in your own module that is meant as an extension point for other teams, and to treat classes without this marker clearly as internal. The same prioritization demanded of Magento should not be skipped in your own codebase.
An effective tool is an automated check in your own CI that searches for extends relationships to non-@api classes and for preference entries in di.xml pointing to classes without an @api tag. That makes every risky extension visible before it ships in a release, instead of surfacing as a bug at the next Magento update.
#!/usr/bin/env bash
set -euo pipefail
# Quick audit: list preference overrides in own modules and flag
# targets that are not marked @api in Magento core (manual review needed)
grep -r "preference for=" app/code/Mironsoft --include="di.xml" -A 0 | \
while read -r line; do
class=$(echo "$line" | grep -oP 'for="\K[^"]+')
echo "Reviewing preference target: $class"
done
# Search for direct inheritance from Magento core classes
grep -rn "extends \\\\Magento\\\\" app/code/Mironsoft --include="*.php"
# Search for plugins targeting classes without an @api tag nearby
grep -rln "<plugin " app/code/Mironsoft --include="di.xml" | while read -r file; do
echo "Plugin definitions in: $file"
done
# Flag any leftover copy-over templates that shadow a core template
find app/design/frontend -path "*/Magento_*/templates/*" -newer composer.lock
7. Spotting BC breaks in minor updates early
Even within a single MAJOR version, Magento minor and patch releases can change behavior that was not formally protected by @api but was used by many modules anyway. Adobe's release notes and the public changelog list known behavioral changes, but they are often overlooked because teams treat Magento updates purely as security patches without reading the accompanying documentation.
A proven approach is to specifically search for changes to the classes used by your own modules before every minor update, for example through a diff of the relevant vendor files between the current and the new Magento version. That is more effort than a blind update, but it prevents exactly the class of bugs that only becomes visible in live operation, because they do not throw an obvious exception, they just slightly alter behavior.
An additional safeguard is a full MFTF regression run against a staging copy with the new Magento version, before a code review of the vendor diffs even begins. Browser based tests uncover exactly those subtle behavioral changes that are hard to spot in a pure code diff, such as a slightly changed order of observer calls or a changed default sort order in a collection, which is not a bug in the strict sense but renders the frontend visibly differently.
8. Practical checklist before every Magento upgrade
A structured checklist substantially reduces the risk of a Magento upgrade, precisely because Backward Compatibility is rarely binary in practice, it knows many shades of gray between "guaranteed safe" and "guaranteed risky". Before every upgrade, it pays to specifically review your own preferences, plugins on non-@api targets, and copied templates.
Equally important is a look at the Magento changelog for entries explicitly marked "breaking change" or "deprecation" for the target version, combined with a full MFTF and integration test run against a staging environment before the upgrade goes to production. This combination of code audit and automated test coverage catches most Backward Compatibility issues before they reach the live shop.
For your own roadmap planning, it also helps to schedule a fixed cadence for BC reviews, for example once a quarter, instead of only doing them ad hoc around an upcoming Magento upgrade. That keeps preferences and plugins current with the latest Magento API documentation, instead of silently clinging to outdated assumptions for years.
A rollback plan defined before the upgrade, instead of improvised under time pressure when something goes wrong, is also worthwhile. A complete database backup, a tagged deployment state before the update, and a clearly defined decision boundary for when to roll back instead of patching forward belong on the checklist just as much as pure code analysis. Especially for MAJOR upgrades removing several deprecations at once, this safety net character of the checklist is often more important than any single line of code.
9. BC compliant vs. risky extension practice
The choice of extension method has direct consequences for a module's update safety. Not every method is equally risky, and the differences are substantial enough to weigh deliberately against each other in every architecture decision.
| Extension method | BC safety | Risk at Magento updates |
|---|---|---|
| Plugin on @api interface | High, formally guaranteed | Low, signature stays stable |
| Preference on @api class | Medium, signature stable, internals not | Medium, on internal changes |
| Preference without @api | Low, not guaranteed | High, any patch can break it |
| Direct inheritance without @api | None | Very high, entire internals bound |
| Core file patched directly | None, violates every guarantee | Maximal, patch is lost at every Composer update |
The practical recommendation follows directly from this table: wherever possible, prefer plugins on interfaces marked @api. If a preference is unavoidable, it should target only @api classes and be documented with a comment explaining why a plugin was not sufficient. Direct inheritance from non-@api classes should be consistently challenged in code review.
In practice, it is worth establishing this table as a fixed part of the pull request checklist. A reviewer who briefly checks the matching row in this table for every new preference or new plugin reliably prevents risky extension practice from slipping unnoticed into the main branch.
Mironsoft
Upgrade safe Magento 2 architecture and BC compliant extension practice
Want the next Magento upgrade without surprises?
We audit your preferences, plugins and copied templates for backward compatibility risks and build custom modules consistently against Magento's @api guarantees instead of against internal code that only looks stable by chance.
BC audit
Identifying preferences and plugins targeting non-@api code
Refactoring
Replacing risky extensions with BC safe plugins
Upgrade support
Systematic checklist and test run before every Magento update
10. Summary
Magento's Backward Compatibility Policy is not a vague statement of intent, it is a precise set of rules with a single central signal: the @api tag. Only code marked as API is guaranteed stable within a MAJOR version, everything else can change at any time. Plugins on @api interfaces are therefore the preferred extension method, while preferences and direct inheritance from non-@api classes take on deliberate risks that should be documented and reviewed regularly.
Whoever applies these rules systematically turns Magento upgrades from a risky event into a predictable process. A regular look at @deprecated markers, an automated check for risky preferences, and a fixed checklist before every upgrade form the practical core of an architecture that grows with Magento instead of being patched back together with every release.
In the end, Magento's Backward Compatibility Policy is less a restriction than an offer: Adobe shows precisely which code is reliable long term, and whoever uses that offer consistently no longer has to fear upgrades, and can instead treat them as a predictable, recurring part of everyday project work.
Magento's BC policy: the essentials at a glance
The @api tag
The only reliable signal for code that stays stable within a MAJOR version.
Prefer plugins
Plugins on @api interfaces are the safest extension method against upgrades.
Take deprecation seriously
Migrate @deprecated markers early instead of waiting for the forced MAJOR upgrade.
Upgrade discipline
Checklist, code audit and test run before every update instead of blind trust.
The following questions summarize the most common practical uncertainties around Magento's Backward Compatibility Policy.