Services

Adobe Commerce development
Storefront design
Upgrades, migration & support
Performance & SEO
AI & agentic commerce
Custom application development
Book a free audit call

Plugins vs Observers vs Preferences in Magento 2: When to Use Each

Plugins vs observers: Development & integrations guide from Mistri
Loading...
Plugins vs Observers vs Preferences in Magento 2: When to Use Each

Use an observer when Magento already dispatches an event at the moment you need and you only have to react to it. Use a plugin when you need to change the arguments or result of a public method and no event fits. Use a preference only when nothing else can reach the behavior, because it replaces a whole class and carries the highest upgrade risk. That order, smallest footprint first, follows Adobe's own guidance on modifying core and third-party code.

The three mechanisms in one paragraph each

Observers

Magento dispatches named events through Magento\Framework\Event\ManagerInterface, for example after an order is placed. An observer is a class that implements Magento\Framework\Event\ObserverInterface, registered for an event in events.xml. It receives only the data the event passes. Adobe's playbook rates observers as the least invasive option: your code never takes ownership of the class.

Plugins (interceptors)

A plugin wraps a public method of a class or interface. Before methods change arguments, after methods change the result, and around methods run code on both sides and decide whether to call the original. Several plugins can wrap the same method, ordered by sortOrder. Magento generates interceptor classes for this during compilation.

Preferences

A preference in di.xml tells the object manager to create your class wherever another class or interface is requested. Preferences for interfaces are how Magento wires its own service contracts. A preference that replaces a concrete core class, however, makes your class responsible for everything that class does. Adobe describes this as effectively taking ownership of the PHP class.

Decision table with upgrade-risk rating

Ratings follow Adobe's implementation playbook for observers, plugins, preferences and patches, with the other rows rated on the same basis: how much of someone else's code you depend on or replace.

MechanismUse it whenCannot doUpgrade risk
ObserverAn event fires at the right moment and you only need to react or adjust passed objectsChange behavior where no event exists; see data not passed to the eventLow
After pluginYou need to change or extend a public method's return valueReach protected or private methodsLow to moderate: breaks if the return type or meaning changes
Before pluginYou need to validate or change arguments before a public method runsChange the result; reach non-public methodsLow to moderate: breaks if the method signature changes
Around pluginYou must decide whether the original method runs at allSame limits as other pluginsModerate: easy to break other plugins; Adobe advises avoiding when not required
DI argument or virtual typeYou need a different dependency, setting or list injected into an existing classChange the class's own logicLow to moderate
Preference for an interface you ownProviding your own implementation of your module's service contractNothing in particular; this is normal designLow
Preference replacing a core or third-party classYou must change protected or private behavior and nothing else worksCoexist with another module that prefers the same classHigh
Patch to vendor codeA temporary fix until the vendor releases oneSurvive changes to the target file reliablyHigh; Adobe suggests removing patches quickly

Before writing code at all, check whether a supported extension already solves the problem. Our build or buy framework covers that decision.

Writing an observer the documented way

Register the observer in etc/events.xml for all areas, or in etc/frontend/events.xml or etc/adminhtml/events.xml to limit it to one area. Adobe's best practices recommend the narrowest area that works, so the global configuration stays light.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="sales_order_place_after">
        <observer name="vendor_module_order_placed"
                  instance="Vendor\Module\Observer\OrderPlaced"/>
    </event>
</config>
<?php
declare(strict_types=1);

namespace Vendor\Module\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\MessageQueue\PublisherInterface;

class OrderPlaced implements ObserverInterface
{
    public function __construct(private PublisherInterface $publisher)
    {
    }

    public function execute(Observer $observer): void
    {
        $order = $observer->getEvent()->getOrder();
        $this->publisher->publish('vendor.order.placed', $order->getIncrementId());
    }
}

The observer hands work to a message queue instead of calling an external system during checkout. The topic name here is illustrative and needs its own communication.xml and queue configuration. Keep observers small and fast; Adobe's best practices also warn against cyclical event loops and against relying on observers running in a particular order.

Writing a plugin the documented way

Declare the plugin on the type you want to intercept in etc/di.xml (or an area-specific di.xml). Plugins on service contract interfaces are generally more stable than plugins on concrete classes.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Catalog\Api\ProductRepositoryInterface">
        <plugin name="vendor_module_normalize_sku"
                type="Vendor\Module\Plugin\NormalizeSku"
                sortOrder="10"/>
    </type>
</config>
<?php
declare(strict_types=1);

namespace Vendor\Module\Plugin;

use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;

class NormalizeSku
{
    public function beforeSave(
        ProductRepositoryInterface $subject,
        ProductInterface $product,
        $saveOptions = false
    ): array {
        $product->setSku(trim((string) $product->getSku()));
        return [$product, $saveOptions];
    }
}

Points from Adobe's plugin documentation to keep in mind:

  • Method names are the prefix plus the original method name with its first letter capitalized: beforeSave, afterSave, aroundSave.
  • A before method returns an array of arguments (or null to leave them unchanged). An after method receives $result and must return the result.
  • An around method receives callable $proceed and must pass the arguments on when it calls it. Adobe advises avoiding around plugins when they are not required because they increase stack traces and affect performance.
  • Plugins cannot be used on final methods or classes, non-public methods, static methods, __construct and __destruct, virtual types, objects created before the interception framework is bootstrapped, or classes implementing NoninterceptableInterface.
  • To switch off someone else's plugin, redeclare it with disabled="true" rather than editing their module.

Preferences: use sparingly

A preference looks like this:

<preference for="Magento\Catalog\Model\Product\Url"
            type="Vendor\Module\Model\Product\Url"/>

Only one preference wins for a given class, decided by module load order. If two extensions both prefer the same core class, one silently loses. Every core upgrade then needs your class checked against the new parent. Before reaching for a preference, check whether you can solve the problem with a plugin on a public method that calls the protected logic, an observer on a nearby event, or a DI argument change.

Upgrade and debugging tips

  • See what is wired to a class: bin/magento dev:di:info "Magento\Catalog\Model\Product" lists its preference, constructor arguments and plugins.
  • Recompile after DI changes: production mode needs bin/magento setup:di:compile so interceptors are regenerated.
  • Match signatures exactly. Plugins must accept the same argument types as the original, including nullable ones. Signature and type changes in core, or stricter PHP versions, are a common source of upgrade breakage. Our guide to moving Magento to PHP 8.5 covers the related issues.
  • Watch performance. Plugins on very frequently called methods, such as product getters, run thousands of times per page. Prefer higher-level methods.
  • Consider out-of-process options. For integrations, Adobe also offers App Builder and event-based extensibility that run outside the Magento codebase. Availability depends on edition and licence.

FAQ

Are plugins slower than observers?

Adobe's playbook says plugins have more overhead than observers but calls the difference trivial in most cases. Around plugins and plugins on hot methods are the ones to watch.

Can an observer change data?

It can modify objects passed to it, such as an order or quote, but it only sees what the event passes. When you need to change a method's arguments or return value, a plugin is the clearer tool.

Can I plug into a protected method?

No. Plugins only work on public methods. Look for the public method that calls it, an event nearby, or, as a last resort, a preference.

Choosing the lowest-risk mechanism is a large part of keeping a store upgradable. Mistri builds custom modules this way through our Magento extension development work.

Sources

Adobe Developer: Plugins (Interceptors); Adobe Developer: Events and observers; Adobe Experience League: Modifying core and third-party code; Adobe Developer: Observers best practices.