API Versioning and Backward Compatibility
API Versioning and Backward Compatibility
~6 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026
Chapters 79-84 firmly established four contracts: RewardInterface, RewardRepositoryInterface, the webapi.xml routes, and schema.graphqls. Once external clients - a mobile app, a partner system, another team - build against these contracts, every later change to them becomes a backward-compatibility question. This chapter deliberately stays conceptual instead of building new code - it sets the rules every future change to this module has to follow.
The uncomfortable truth about PHP interfaces
Extending a class with a new method is backward compatible - existing code using the class notices nothing. For an interface, the opposite is true: adding a new abstract method getStockStatus(): string to RewardInterface forces PHP to require every implementation to add it - including third-party classes outside this project that implement RewardInterface themselves (say, an alternative Model\Data\Reward via their own preference). A breaking change, disguised as a seemingly harmless addition.
Achtung: That's exactly why RewardInterface deliberately extends ExtensibleDataInterface in chapter 79: getExtensionAttributes()/setExtensionAttributes() are Magento's own sanctioned, safe extension path. A new field goes into extension_attributes.xml instead of being added directly as a new interface method - existing implementations stay valid unchanged, and the code generator simply rebuilds RewardExtensionInterface.
When a V2 is actually needed
- Never needed for additive REST changes: a new, optional response field via extension attributes leaves existing V1 clients untouched - they simply ignore what they don't know about.
- Never needed to remove a field nobody uses anymore - provided that can actually be confirmed from real access logs, which is rarely the case for a public API.
- Genuinely needed as soon as the meaning of an existing field changes (example below), a required parameter gets added, or a method's return type changes incompatibly.
A concrete, hypothetical example of the last case: if points_per_euro (chapter 7) were to eventually vary by product category, and PointsSummaryInterface::getPointsBalance() therefore had to return an entire breakdown instead of a plain points count - a genuine meaning change, not an additive field. The clean path would then be a dedicated Api\PointsManagementV2Interface with a new getPointsSummaryV2() method, registered under a second webapi.xml route:
<route url="/V2/loyalty/points/mine" method="GET">
<service class="Mironsoft\Loyalty\Api\PointsManagementV2Interface" method="getPointsSummary"/>
<resources>
<resource ref="self"/>
</resources>
<data>
<parameter name="customerId" force="true">%customer_id%</parameter>
</data>
</route>
/V1/loyalty/points/mine from chapter 80 would stay in place unchanged - existing clients don't break, new clients migrate to /V2 voluntarily once they're ready. Purely illustrative: PointsManagementV2Interface isn't actually built in this module, the example only shows the pattern.
GraphQL versions itself differently from REST
GraphQL has no /V2 URL prefix - a schema keeps evolving continuously instead of being cut into versions. The official GraphQL path for a meaning change is the @deprecated directive: the old field keeps working but is marked deprecated and becomes visible to tooling through introspection (chapter 87).
type LoyaltyReward @doc(description: "A single redeemable reward") {
reward_id: Int!
identifier: String!
title: String!
is_active: Boolean! @deprecated(reason: "Use reward_status instead, which distinguishes active/paused/archived")
reward_status: String @doc(description: "One of active, paused, archived")
}
Purely illustrative here too - reward_status doesn't actually exist in this module, is_active from chapter 82 remains this series' actual state. The example only shows what a later, real meaning change to LoyaltyReward would look like without immediately breaking existing clients.
A checklist for every future change
- Can the change be modeled as a new, optional field via extension attributes (REST) or an additive schema extension (GraphQL)? Then no new version is needed.
- Does the meaning of an existing field change, not just its presence? Then a V2 route (REST) or @deprecated plus a new field (GraphQL).
- Is a method being removed from an existing Api interface, or its signature changed? Practically never without a new version - PHP otherwise forces an error on every third-party implementation.
- Is the change documented (chapter 87) and recorded in this series' spec document, so later blocks know the same contract?
With clear stability rules in place, chapter 86 turns to a different threat: what happens when a client knows the rules but simply calls the redemption route from chapter 81 too often, too fast?