Magento gives you three ways to touch data - repositories, models, and collections - and they are not interchangeable. Each sits at a different altitude: stability, convenience, or power. Picking the wrong altitude is behind both fragile modules and slow ones. Here is the map.
The Three Layers
Repository (ProductRepositoryInterface): the stable API. get/getById/save/delete/getList. Per-entity, deliberate, backward-compatible. This is the default choice for business logic.
Model (Product::load, ->save()): the active-record legacy layer. Still everywhere in core, but deprecated for new code in favour of repositories - and model save() triggers a waterfall of legacy events.
Collection (ProductCollection): the power tool. SQL-level control: joins, conditions, field selection, pagination. The right tool for reading many records efficiently - and the wrong tool for saving (collections do not save; iterate repositories or use mass actions).
getList and SearchCriteria
Repository list methods take a SearchCriteriaInterface:
$criteria = $this->searchCriteriaBuilder
->addFilter('status', 1)
->addFilter('price', 50, 'gt')
->setPageSize(25)
->setCurrentPage(2)
->create();
$result = $this->productRepository->getList($criteria);
Filters, or groups, sorting and paging - the same shape the REST API exposes. For standard filtered lists, getList is the right altitude: portable, cache-friendly, API-consistent.
When Repositories Are Too Slow
Repositories load full entities: EAV joins, extension attribute resolution, event dispatch. Per-entity overhead is real:
- Reading 5,000 products for a feed? Use a collection with
addAttributeToSelectof exactly the fields needed - an order of magnitude faster - Updating one attribute on 10,000 products?
updateAttributesmass action, not repository saves - Writing in a loop at import scale? You are in import-engine territory, not repository territory (see our import workflows post)
The rule: repositories for transactional, per-entity business logic; collections and mass actions for bulk reads and batch updates; the import engine for imports.
Data Models and Extension Attributes
Your own modules should follow the same shape: a data interface (Api/Data), a repository interface, a model/resource/collection implementing it. It looks ceremonial for a small module; it pays off the first time another system consumes your entity via the API for free.
Attaching custom data to core entities: extension attributes, registered in extension_attributes.xml and populated via plugins on the repository. Never join your table into core collections ad hoc in five places - extension attributes give the join one owner.
The One-Line Version
Repositories are for correctness, collections are for speed, models are for legacy. Know which you need before you write the constructor - switching altitudes later is a refactor, not a tweak.