Directly using Magento 2 models in your own code is a tempting shortcut. The model is right there, it has the method you need, it works. The problem appears when Magento is updated and the internal implementation changes. Service Contracts are Magento’s answer – a stable, public API separated from the implementation.
What are Service Contracts?
A Service Contract is a set of PHP interfaces defining the public API of a module. Magento splits them into two types:
- Data Interfaces – describe data structures (e.g.
ProductInterface,OrderInterface) - Service Interfaces – describe business operations (e.g.
ProductRepositoryInterface)
Interfaces annotated with @api are covered by Magento’s backward-compatibility guarantee.
Model vs Repository – a concrete example
<?php
// The "shortcut" way - direct model usage
class MyClass
{
public function getProduct(int $id): \Magento\Catalog\Model\Product
{
$product = $this->productFactory->create();
$product->load($id); // load() deprecated since Magento 2.1!
return $product;
}
}
<?php
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Api\Data\ProductInterface;
class MyClass
{
public function __construct(
private ProductRepositoryInterface $productRepository
) {}
public function getProduct(int $id): ProductInterface
{
return $this->productRepository->getById($id);
}
}
Data Interfaces – SearchCriteria example
<?php
class ProductService
{
public function __construct(
private ProductRepositoryInterface $productRepository,
private SearchCriteriaBuilder $searchCriteriaBuilder
) {}
public function getActiveProducts(): array
{
$searchCriteria = $this->searchCriteriaBuilder
->addFilter('status', 1)
->addFilter('visibility', [2, 3, 4], 'in')
->setPageSize(20)
->create();
$result = $this->productRepository->getList($searchCriteria);
return array_map(
fn(ProductInterface $p) => [
'sku' => $p->getSku(),
'name' => $p->getName(),
'price' => $p->getPrice(),
],
$result->getItems()
);
}
}
Defining your own Service Contracts
<?php
namespace Vendor\Module\Api\Data;
/**
* @api
*/
interface SubscriptionInterface
{
public function getId(): ?int;
public function getEmail(): string;
public function setEmail(string $email): self;
public function getCreatedAt(): string;
}
<!-- di.xml - bind interface to implementation -->
<preference for="Vendor\Module\Api\Data\SubscriptionInterface"
type="Vendor\Module\Model\Subscription"/>
<preference for="Vendor\Module\Api\SubscriptionRepositoryInterface"
type="Vendor\Module\Model\SubscriptionRepository"/>
Summary
Service Contracts are real protection against problems during Magento upgrades. Code based on @api interfaces is more resilient to internal platform changes and easier to test – you can replace the repository implementation with a mock without changing the calling code.
