
In Magento and Adobe Commerce, use an observer when an event already fires at the point you need, a plugin to change the arguments or return value of a public method when no event fits, and a preference only as a last resort to replace an entire class. Plugins are the usual choice.
Every Magento developer hits the same fork: you need to change how core behaves, and you have three tools that can do it. Pick the wrong one and you get a module that conflicts with every other extension, slows down a hot code path, or quietly stops working after an upgrade. This guide explains how plugins, observers, and preferences actually work, what each one cannot do, and a decision rule that holds up on a real Magento and Adobe Commerce store with dozens of third-party modules installed.
The three customization points at a glance
All three are configured through dependency injection or event configuration, and none of them require editing core files. That shared principle, extend without touching core, is what keeps a store upgradeable. Where they differ is what they hook and how they coexist with other modules.
| Mechanism | Hooks | Can change | Coexists with other modules | Typical use |
|---|---|---|---|---|
| Observer | A dispatched event | Data passed on the event object; side effects | Yes, all observers of an event run | React to something that happened |
| Plugin (interceptor) | A public method of a class | Method arguments and return value | Yes, chained by sortOrder |
Modify input or output of a method |
| Preference | A class or interface, globally | Everything, by replacing the class | No, last declaration wins | Replace a whole implementation |
Read that “coexists” column carefully. It is the single biggest reason to reach for a plugin over a preference.
Observers: react to events that already fire
An observer listens for an event that Magento dispatches and runs your code when it fires. You declare it in events.xml, and the observer class implements \Magento\Framework\Event\ObserverInterface with a single execute(Observer $observer) method. Adobe’s events and observers guide lists the mechanics.
<!-- etc/events.xml -->
<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="bemeir_notify_fulfilment"
instance="Bemeir\Orders\Observer\NotifyFulfilment" />
</event>
</config>
Observers are the right tool when the thing you care about is an event that Magento already emits, such as an order being placed or a customer logging in, and you want to run a side effect or adjust data that the event carries. They do not intercept method calls, so you cannot use an observer to change what a method returns to its caller. You can only work with whatever the dispatching code chose to put on the event object.
Two practical limits matter. First, an event has to exist at the point you need; you cannot observe a method that never dispatches one. Second, observers have no reliable ordering control between modules, so never write two observers of the same event that depend on running in a particular sequence.
Plugins: wrap the public methods you need to change
A plugin, also called an interceptor, wraps a public method so your code runs before it, after it, or around it. This is the workhorse of Magento customization and the right answer most of the time. You declare it in di.xml under a <type> element. Adobe’s plugins reference is the authoritative source for the behavior below.
<!-- etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Catalog\Model\Product">
<plugin name="bemeir_product_name"
type="Bemeir\Catalog\Plugin\ProductNamePlugin"
sortOrder="10" />
</type>
</config>
The plugin class names methods by prefixing the target public method with before, after, or around, capitalizing the first letter. For getName() you write afterGetName. Each type behaves differently:
- before runs ahead of the method. Signature
beforeGetName(Product $subject, ...$args). Return an array to change the arguments passed into the original method, or return nothing to leave them alone. - after runs once the method returns. Signature
afterGetName(Product $subject, $result, ...$args). It receives the result and must return a value. This is how you modify output, and it is the lightest, safest plugin type. - around wraps the whole call. Signature
aroundGetName(Product $subject, callable $proceed, ...$args). You call$proceed()to run the next plugin or the original method. If you do not call$proceed, everything downstream is skipped.
Prefer after to change a result and before to change arguments. Reach for around only when you genuinely need to conditionally skip the original method, because around plugins add stack depth and carry a performance cost. Adobe’s own documentation advises avoiding them unless termination is required. A common bug: an around plugin that forgets to forward arguments to $proceed, or does not match the original method’s default values and type hints, which throws a fatal error when a null is passed.
Multiple plugins on the same method are chained by sortOrder, lowest first for the before and around phases, and the after phase unwinds afterward. With three plugins at sortOrder 10, 20, and 30 using only before methods, the order is A before, B before, C before, original, then A after, B after, C after. Ties fall back to module load order from module.xml.
When several modules stack plugins on one method and the behavior looks wrong, inspect the merged configuration rather than guessing. The generated interceptor classes and the resolved plugin list live under the generated directory, and clearing it with a recompile often explains an ordering surprise that no single di.xml file reveals on its own. Keep each plugin focused on a single method, so when a third-party extension fights yours the conflict stays easy to isolate and the fix is a sortOrder change rather than a rewrite.
What plugins cannot intercept
Plugins work by generating an Interceptor subclass of the target, so anything that cannot be overridden by a subclass is off limits. You cannot plugin:
- Final methods or final classes
- Non-public methods (protected or private)
- Static methods
__constructand__destruct- Virtual types
- Objects instantiated before the interception framework is bootstrapped
- Classes implementing
Magento\Framework\ObjectManager\NoninterceptableInterface
If you need to change one of those, a plugin will silently do nothing, which is a frustrating way to lose an afternoon. That is the narrow case where a preference may be your only option.
Preferences: replace a class, and inherit its problems
A preference tells the object manager to use your class wherever another class or interface is requested. It is declared in di.xml and it is global.
<!-- etc/di.xml -->
<preference for="Magento\Catalog\Model\Product"
type="Bemeir\Catalog\Model\Product" />
Your class should extend the original and override only what you need. The problem is coexistence. Preferences do not chain. If two modules declare a preference for the same class, the one Magento loads last wins and the other is silently ignored. On a store running many third-party extensions, that is a real and hard-to-debug source of conflict. Preferences also make upgrades heavier, because your subclass is pinned to the parent’s method signatures, so every core change to that class is a compatibility risk you have to retest.
Use a preference only when you truly need to replace an entire implementation, or when you must intercept something plugins cannot reach, such as a protected method, and no event exists. For almost everything else, a plugin does the job without claiming exclusive ownership of the class. This is the same out-of-core philosophy behind Adobe Commerce webhooks and out-of-process extensibility, where the goal is always to extend the platform through its seams rather than fork it.
A decision rule that holds up in production
Work down this list and stop at the first match:
- Is there an event dispatched at exactly the point you need, and do you only need a side effect or to adjust data already on the event? Use an observer.
- Do you need to change the arguments or the return value of a public method? Use a plugin. Use
afterfor output,beforefor input,aroundonly to conditionally skip the call. - Do you need to replace a whole class, or change a method a plugin cannot intercept, with no event available? Use a preference, and extend the original class rather than copying it.
The ordering is deliberate. Observers and plugins both let other modules do their own thing alongside yours. Preferences do not, so they sit last. The same reasoning extends to how you structure data and schema changes, which we cover in writing Magento data patches, and to how Hyva moves presentation logic into view models instead of block rewrites, covered in Hyva view models explained. The thread running through all of it is the same: extend through the platform’s seams so the next upgrade does not undo your work. Adobe’s dependency injection guide is the reference for how di.xml resolves all of this.
Bemeir is the first US-based Hyva development partner and a full Magento and Adobe Commerce shop, and the same engineering standards carry across our work on Shopify, Shopware, and BigCommerce. Because our technology partner ecosystem means a client store often runs dozens of extensions at once, picking the customization point that coexists cleanly is not a style preference, it is what keeps those stores stable. You can learn more about Bemeir and how we work.
FAQ
Should I use a plugin or an observer in Magento?
Use an observer when Magento already dispatches an event at the exact point you care about and you only need to run a side effect or adjust data carried on the event. Use a plugin when you need to change the arguments going into a public method or the value it returns, which an observer cannot do. If both could work, a plugin gives you more control over ordering through sortOrder.
When should I use an around plugin instead of before or after?
Only when you must conditionally prevent the original method from running, or wrap logic around both its input and output together. Around plugins increase stack depth and cost more than before or after plugins, and Adobe recommends avoiding them unless termination is required. Use after to change a return value and before to change arguments whenever those suffice.
Why are preferences discouraged in Magento?
Because preferences do not coexist. When two modules declare a preference for the same class, only the last one Magento loads takes effect and the others are silently ignored, which causes hard-to-trace conflicts on stores with many extensions. Preferences also pin your subclass to the parent’s signatures, making every core upgrade a compatibility risk. Use a plugin unless you genuinely must replace the whole class.
What methods can a Magento plugin not intercept?
Plugins cannot intercept final methods or classes, non-public (protected or private) methods, static methods, __construct and __destruct, virtual types, objects created before the interception framework loads, or classes implementing NoninterceptableInterface. Plugins are generated as subclasses, so anything a subclass cannot override is out of reach. For those cases you need a preference or a different approach, and observers also run in no guaranteed order between modules, so never make two observers of one event depend on a sequence.





