“Use service contracts” is advice every Magento developer hears; fewer can say why. Service contracts - the interfaces under each module’s Api/ directory - are Magento’s stability promise: the layer Adobe commits to keeping backward-compatible between releases. Understanding that promise changes how you write, and upgrade, custom code.
What a Service Contract Is
Two interface families per module:
- Service interfaces (
ProductRepositoryInterface,OrderManagementInterface): the operations - Data interfaces (
ProductInterface,OrderInterface): the data shapes, with getters/setters
Code depending on these interfaces talks to the module through its public API - the same surface the REST API exposes (that is deliberate: webapi.xml routes to these same interfaces).
The Stability Promise
Magento’s backward-compatibility policy treats Api/ interfaces as the protected surface: patch and minor releases must not break them. Internal classes - Model\Product, resource models, collections - carry no such promise; they change freely.
This is the entire argument:
// Stable across upgrades
public function __construct(
private \Magento\Catalog\Api\ProductRepositoryInterface $products
) {}
// Fragile - Model internals change between versions
public function __construct(
private \Magento\Catalog\Model\ProductFactory $productFactory
) {}
Code written against service contracts survives upgrades; code reaching into models accumulates upgrade debt. The price of the discipline is occasionally clunkier code (repositories load one entity at a time); the return is upgrades that do not break your modules.
When the Rule Bends
Honesty requires the exceptions:
- Bulk operations: repositories are per-entity; importing 100k products through
ProductRepositoryInterface::saveis pathologically slow. Import paths use import models deliberately - Coverage gaps: some internals have no service contract. When no API exists, isolate the internal dependency behind your own interface so the fragility has one address
- Performance-critical reads: collection queries are sometimes the right tool - again, isolate them
Extending Data
Adding fields to core entities goes through extension attributes - etc/extension_attributes.xml declaring attributes attached to a data interface, populated via plugins. Extension attributes are the service-contract-safe answer to “I need orders to carry an ERP reference”; adding a getter to the core interface is not something you can do, and joining custom tables inside arbitrary model code is how upgrade conflicts happen.
The Audit Question
In code review, one question enforces the discipline: “Does this class depend on anything outside Api/?” If yes and unavoidable, it gets isolated and documented. That habit, applied consistently, is the difference between a codebase that upgrades in a week and one that upgrades in a quarter.