
A Magento data patch is a PHP class in a module’s Setup/Patch/Data directory that implements DataPatchInterface and runs once when you execute bin/magento setup:upgrade. Data patches replace the deprecated InstallData and UpgradeData scripts, giving you ordered, dependency-aware data changes that Magento records in the patch_list table so they never run twice.
If you still have InstallData.php or UpgradeData.php files in a Magento 2 module, you are writing against a model Adobe has already moved past. Since Magento 2.3, schema changes belong in declarative db_schema.xml and data changes belong in patches. The old version-compare scripts still execute for backward compatibility, but new work should not use them, and once a module starts using data patches it cannot go back to upgrade scripts. This guide covers how patches actually behave, the methods that matter, the idempotency rule the official docs gloss over, and how to migrate a legacy setup script without breaking an existing install.
Bemeir has shipped this pattern across production Magento and Adobe Commerce builds, so the examples here reflect what holds up on a real store, not just the reference sample.
Why InstallData and UpgradeData had to go
The legacy model tied every data change to a module version string in module.xml. On setup:upgrade, Magento compared the declared version to the recorded version and ran UpgradeData::upgrade() with a big if (version_compare(...)) ladder inside. That design had three practical problems.
First, ordering was implicit. If module A needed data created by module B first, you managed it through module sequence and hoped the version ladders lined up. Second, the version ladder grew into an unreadable block of nested conditionals that no one wanted to touch. Third, there was no clean way to express that one specific change depended on another specific change; the only unit of ordering was the whole module version.
Patches fix all three. Each patch is its own class, ordering is explicit through getDependencies(), and Magento tracks each applied patch by class name rather than by a module version number. The unit of change is now a single, named, testable file.
What a data patch looks like
A data patch lives at <Vendor>/<Module>/Setup/Patch/Data/<PatchName>.php and implements \Magento\Framework\Setup\Patch\DataPatchInterface, as documented in Adobe’s data and schema patches reference. Here is the shape every patch follows.
<?php
namespace Bemeir\Catalog\Setup\Patch\Data;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Eav\Setup\EavSetupFactory;
class AddFabricCareAttribute implements DataPatchInterface
{
public function __construct(
private ModuleDataSetupInterface $moduleDataSetup,
private EavSetupFactory $eavSetupFactory
) {
}
public function apply()
{
$this->moduleDataSetup->getConnection()->startSetup();
$eavSetup = $this->eavSetupFactory->create(
['setup' => $this->moduleDataSetup]
);
if (!$eavSetup->getAttributeId(\Magento\Catalog\Model\Product::ENTITY, 'fabric_care')) {
$eavSetup->addAttribute(
\Magento\Catalog\Model\Product::ENTITY,
'fabric_care',
[
'type' => 'text',
'label' => 'Fabric Care',
'input' => 'textarea',
'required' => false,
'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE,
'group' => 'General',
]
);
}
$this->moduleDataSetup->getConnection()->endSetup();
return $this;
}
public static function getDependencies(): array
{
return [];
}
public function getAliases(): array
{
return [];
}
}
The constructor receives ModuleDataSetupInterface through dependency injection, plus whatever services the change needs (here EavSetupFactory). The real work sits in apply(), wrapped in startSetup() and endSetup() calls on the connection. Notice the getAttributeId() guard before addAttribute(). That guard is not in the minimal reference sample, and it is the single most important line in the file. More on why below.
apply(), getDependencies(), and getAliases()
Three methods carry the contract.
apply() holds the change. It runs inside a database transaction and should return $this. Everything the patch does, creating an attribute, seeding a lookup table, backfilling a column, belongs here.
getDependencies() is a static method that returns an array of patch class names that must run before this one. The patches can live in any module, which is the clean replacement for the old module-sequence juggling. If patch B needs the data patch A created, B returns [A::class] and Magento orders them for you. Patches with no dependencies run in no guaranteed order relative to each other, so never assume two independent patches run in file order.
getAliases() returns previous class names for this patch. When you rename a patch file, list the old name here so Magento recognizes the already-applied row in patch_list and does not run the renamed class again as if it were new. Skip this step on a rename and the patch re-runs on every environment that already had the old name recorded.
How Magento tracks and orders changes
Applied patches are recorded in the patch_list table by fully qualified class name. On setup:upgrade, Magento reads that table, skips anything already recorded, resolves the dependency graph for the rest, and runs the remainder once each. Declarative schema from db_schema.xml is applied first, then schema patches and data patches. That ordering matters: if your data patch writes to a column, the declarative schema that creates the column has already run by the time apply() fires.
The table is also why a patch can accidentally run again. If someone deletes the row, or the class is renamed without a getAliases() entry, Magento treats it as unapplied and executes it. That is the whole reason idempotency is not optional.
Here is how the three mechanisms compare.
| Mechanism | File / location | Runs | Tracked by | Use it for |
|---|---|---|---|---|
| Declarative schema | etc/db_schema.xml |
Every setup:upgrade, reconciled to desired state |
db_schema_whitelist.json |
Tables, columns, indexes, constraints |
| Schema patch | Setup/Patch/Schema/ implements SchemaPatchInterface |
Once | patch_list |
Triggers, renames, stored procedures, partitions |
| Data patch | Setup/Patch/Data/ implements DataPatchInterface |
Once | patch_list |
Seeding rows, EAV attributes, backfills, config values |
Schema structure is declarative. Data and the awkward schema operations that do not fit a declarative model are patches. If you are adding a plain column, do not write a patch for it; declare it in db_schema.xml and regenerate the whitelist with bin/magento setup:db-declaration:generate-whitelist. Adobe’s declarative schema overview explains how that reconciliation works.
Making apply() idempotent, the part the docs skip
The reference documentation presents apply() as if it always runs against a clean slate. Production is not clean. Patches get re-run when a patch_list row is removed during debugging, when a database is restored from a snapshot taken before the row existed, when a patch is renamed, or when the same codebase is deployed to an environment that was seeded by a database dump rather than by setup:upgrade. In every one of those cases, a patch that blindly inserts will throw a duplicate-key error or create a second copy of its data.
Write apply() so that running it twice is safe. The rules are simple and they pay for themselves the first time a deploy hiccups.
- Before adding an EAV attribute, check
getAttributeId()and skip if it exists. - Before inserting a config value, read it first and only write if absent or wrong.
- Before seeding rows, query for the natural key and insert only the missing ones, or use an insert-on-duplicate pattern.
- Before backfilling a column, scope the update to rows that still hold the old value.
An idempotent patch converts a frightening re-run into a no-op. A non-idempotent patch converts it into a production incident. This is the same defensive posture that makes a deploy safe to retry, which we cover in our guide to zero-downtime Magento deploys with Deployer.
Reverting: what you can and cannot undo
Reverting is where expectations outrun reality. Magento does not let you revert a single data patch on a running store. There is no bin/magento patch:revert command. If you implement PatchRevertableInterface and write a revert() method, that logic only fires during module uninstall:
bin/magento module:uninstall Vendor_Modulereverts all Composer-installed data patches for that module.bin/magento module:uninstall --non-composer Vendor_Modulereverts non-Composer modules.
That is the entire revert story. For day-to-day work, assume a data patch is forward-only. If a patch did the wrong thing, you fix it with a new corrective patch, not by editing the original and re-running it, because the original already has a patch_list row and will not fire again. Also heed Adobe’s warning that deleting data from a column used as a foreign key reference can trigger ON DELETE cascade behavior you did not intend, so test revert() against a realistic dataset before you rely on it.
Migrating legacy InstallData scripts
If a module still carries InstallData.php or UpgradeData.php, move the logic into patches deliberately. Adobe’s migration guide for install and upgrade scripts spells out the mechanics; the practical sequence is this.
- Create one data patch per logical change rather than one giant patch. Smaller patches are easier to read, order, and reason about.
- For a store that already ran the old
UpgradeDataladder, usePatchVersionInterfaceon the new patch so Magento knows the change was already applied at a given module version and does not repeat it. - Keep
apply()idempotent regardless, because some environments will have the data and some will not. - Remove the old script only after every environment has recorded the equivalent patch or version.
Once a module uses data patches, you cannot keep using upgrade scripts in it, so do the migration in full rather than mixing both models.
How this fits a disciplined Magento workflow
Patches are one piece of a larger discipline around safe database change. They pair with composer-managed dependencies and locked versions, which we break down in Magento 2 Composer and dependency management, and with the broader practice of extending the platform through its own extension points rather than core edits, the same principle behind custom GraphQL resolvers on Magento and Hyva. A module whose schema is declarative, whose data changes are small idempotent patches, and whose dependencies are explicit is a module that upgrades cleanly and deploys without drama.
Bemeir is the first US-based Hyva development partner and a full Magento and Adobe Commerce shop, and we build with the same standards on Shopify, Shopware, and BigCommerce. Our deep technology partner ecosystem means these modules have to interoperate with payments, ERP, and fraud systems in production, which is exactly why patch discipline matters. You can read more about the team on our about page.
FAQ
What is the difference between a data patch and a schema patch in Magento?
A data patch implements DataPatchInterface and changes data: seeding rows, adding EAV attributes, backfilling columns, setting config values. A schema patch implements SchemaPatchInterface and handles schema operations that a declarative db_schema.xml cannot express cleanly, such as triggers, renames, stored procedures, and partitions. Both run once and are recorded in patch_list. Plain tables and columns should be declarative, not patches.
Do Magento data patches run every time I run setup:upgrade?
No. Each data patch runs exactly once. Magento records the applied patch class name in the patch_list table and skips it on future setup:upgrade runs. A patch only runs again if its patch_list row is removed or the class is renamed without listing the old name in getAliases().
How do I control the order data patches run in?
Use the static getDependencies() method to return the class names of patches that must run first. Those patches can belong to any module. Patches with no dependencies have no guaranteed order relative to each other, so never rely on filename or alphabetical order between independent patches.
Can I undo or revert a Magento data patch?
Not individually on a live store. Magento has no command to revert a single data patch. A revert() method defined through PatchRevertableInterface only runs during bin/magento module:uninstall, which reverts all of that module’s data patches. In practice, treat data patches as forward-only and correct mistakes with a new patch.
Should I still use InstallData and UpgradeData scripts?
No. They are deprecated in favor of data patches and declarative schema. They still execute for backward compatibility, but new modules should use patches, and once a module adopts data patches it can no longer use upgrade scripts. Migrate legacy scripts into small idempotent patches, using PatchVersionInterface so stores that already ran the old scripts do not repeat the work.





