Understanding and Following Magento's Backward Compatibility Policy
AI generated
M2
di.xml
Magento 2 · Backward Compatibility · API · Upgrades
Magento's Backward Compatibility Policy
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.

16 min read @api · Plugins · Deprecation · Upgrade Safety Magento 2.4.8 · PHP 8.4

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.

11. FAQ: Magento's Backward Compatibility Policy

1What does the BC policy regulate?
It defines which code marked @api stays stable within a major version.
2What does the @api tag mean?
It marks a stable public interface with a guaranteed unchanged signature.
3Is every public method BC protected?
No, only the @api tag decides, not PHP visibility.
4Why are plugins safer than preferences?
They only depend on the stable public signature, not on unprotected internals.
5What does @deprecated mean?
Still functional, but scheduled for removal in the next major release.
6Are templates BC protected?
No, layout handles and templates carry no formal guarantee.
7How do I find risky extensions?
Grep for preference entries and extends relationships, then manually check for @api.
8Can minor updates change behavior?
Yes, for code without an @api tag this is explicitly allowed.
9What belongs on an upgrade checklist?
Audit of risky extensions, changelog review, and a full test run against staging.
10What is the riskiest practice?
Direct inheritance from a core class without an @api tag, it can break with any patch release.