← All articles >
magento 2observerseventsevent-driven architecturemagento developmentphptutorial

Magento 2 Observers & Events: Complete Developer Guide

Share: LinkedIn X Facebook

Magento 2’s event-driven architecture lets you react to almost anything that happens in the system — order placement, customer registration, product save, admin actions — without modifying core code or extending classes. Observers listen for named events and execute logic when those events fire. This guide covers how to declare observers, what built-in events are available, how to dispatch your own custom events, and when to choose observers over plugins .

In short: An observer is a class that listens for a named event and runs code when that event is dispatched. Declare it in events.xml (global) or events.xml scoped to an area (frontend, adminhtml, webapi_rest, graphql). Use observers when you need to react to an event that has no specific method you can plugin to — for example, “after order is placed” (sales_order_place_after) isn’t tied to a single method call.

Table of Contents

  1. Events vs Plugins: When to Use What
  2. Declaring an Observer in events.xml
  3. The Observer Class Signature
  4. Area-Scoped Events: Frontend, Adminhtml, Web API, GraphQL
  5. Most Useful Built-in Events in Magento 2
  6. Dispatching Custom Events
  7. Passing Data to Observers via the Event
  8. Observer Execution Order and Stopping Propagation
  9. Best Practices and Common Pitfalls
  10. FAQ

Events vs Plugins: When to Use What

Before writing an observer, check if a plugin would be cleaner. Here’s a rule of thumb:

ScenarioBest approach
Modify arguments or return value of a specific methodPlugin
React to “something happened” (order placed, customer logged in)Observer
Execute logic that spans multiple unrelated classesObserver
Need to wrap the entire method executionPlugin (around)
The event you need doesn’t exist yetPlugin, or dispatch a custom event

The key difference: plugins hook into methods, observers hook into events. Events can be dispatched from anywhere — models, controllers, helpers, even other observers — and multiple observers can listen to the same event without knowing about each other.

Declaring an Observer in events.xml

Create etc/events.xml in your module (for global events) or scope it to an area:

<!-- app/code/Vendor/Module/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="vendor_module_order_placed"
                  instance="Vendor\Module\Observer\OrderPlaced"
                  shared="false"/>
    </event>
</config>

The name attribute on <observer> must be unique within the event scope. Use a prefix (your vendor/module name) to avoid conflicts. shared="false" creates a new instance each time — use this unless the observer is stateless.

The Observer Class Signature

Every observer implements \Magento\Framework\Event\ObserverInterface with a single execute() method:

<?php
declare(strict_types=1);

namespace Vendor\Module\Observer;

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

class OrderPlaced implements ObserverInterface
{
    public function __construct(
        private readonly \Psr\Log\LoggerInterface $logger
    ) {}

    public function execute(Observer $observer): void
    {
        $order = $observer->getEvent()->getOrder();
        $this->logger->info('Order placed: ' . $order->getIncrementId());
    }
}

The Observer object gives you access to getEvent(), which returns the event data object. From there, you retrieve the specific data using getters named after the event’s data keys. Common getters: getOrder(), getProduct(), getCustomer(), getQuote().

Area-Scoped Events: Frontend, Adminhtml, Web API, GraphQL

Global events.xml fires in all areas. Scope it to a specific area by placing the file in the area’s etc/ directory:

app/code/Vendor/Module/
├── etc/
│   └── events.xml                    ← global (all areas)
├── etc/frontend/
│   └── events.xml                    ← frontend only
├── etc/adminhtml/
│   └── events.xml                    ← admin panel only
├── etc/webapi_rest/
│   └── events.xml                    ← REST API only
└── etc/graphql/
    └── events.xml                    ← GraphQL only

This is useful when the same module needs different behaviour in different areas. For example, log an order in frontend but send an admin notification in adminhtml.

Most Useful Built-in Events in Magento 2

Checkout & Sales Events

Event nameData available
sales_order_place_afterorder
sales_order_save_afterorder
checkout_cart_add_product_completeproduct, request
checkout_onepage_controller_success_actionorder_ids
sales_quote_collect_totals_afterquote

Customer Events

Event nameData available
customer_register_successcustomer, account_controller
customer_logincustomer
customer_logoutcustomer
customer_address_save_aftercustomer_address, customer

Catalog & Product Events

Event nameData available
catalog_product_save_afterproduct
catalog_product_load_afterproduct
catalog_category_save_aftercategory
catalog_product_is_salable_beforeproduct

You can find the full list in vendor/magento/framework/Event/etc/events.xsd and by searching for ->dispatch( in the Magento codebase.

Dispatching Custom Events

Dispatching your own events makes your module extensible — other developers can hook into it without modifying your code.

use Magento\Framework\Event\ManagerInterface;

class SomeService
{
    public function __construct(
        private readonly ManagerInterface $eventManager
    ) {}

    public function doSomething(string $sku, array $data): void
    {
        // ... business logic ...

        $this->eventManager->dispatch(
            'vendor_module_something_done',
            ['sku' => $sku, 'result' => $data]
        );
    }
}

Convention for event names: lowercase, vendor prefix, underscore-separated segments. The second argument is an associative array — each key becomes available via getSku(), getResult(), etc. on the observer’s event object.

Passing Data to Observers via the Event

When dispatching, the array keys become the getter names on the event data:

$this->eventManager->dispatch('custom_event', [
    'order' => $order,
    'items' => $items,
    'source' => 'cron'
]);

In the observer:

public function execute(Observer $observer): void
{
    $order = $observer->getEvent()->getOrder();
    $items = $observer->getEvent()->getItems();
    $source = $observer->getEvent()->getSource();
}

The data keys follow camelCase convention. Magento’s built-in events use singular nouns for single objects (order, product, customer) and plural for collections (order_ids, items).

Observer Execution Order and Stopping Propagation

Observers for the same event execute in the order they appear in events.xml. If you need to control ordering across modules, use the sort_order parameter on the <observer> element (lower numbers run first):

<event name="sales_order_place_after">
    <observer name="first_module" instance="Vendor\First\Observer" sort_order="10"/>
    <observer name="second_module" instance="Vendor\Second\Observer" sort_order="20"/>
</event>

To stop subsequent observers from executing, call stopPropagation():

public function execute(Observer $observer): void
{
    if (!$this->config->isEnabled()) {
        $observer->stopPropagation();
        return;
    }
    // ... process ...
}

Use this sparingly — stopping propagation silently breaks other modules that depend on the same event.

Best Practices and Common Pitfalls

  1. Keep observers lightweight. An observer should not contain complex business logic. Delegate to services or models. Observers are called synchronously — a slow observer blocks the entire request.

  2. Never inject a concrete class that triggers events in its constructor. This creates infinite loops. For example, don’t inject a repository in an observer that listens to the save event of that same entity.

  3. Use shared="false" for observers that hold state. If an observer has dependencies that change between calls (like a registry value), creating a new instance each time prevents stale data.

  4. Prefer plugins over observers when you need to modify method arguments or return values. Plugins are type-safe and more predictable. Use observers only for “reaction” scenarios.

  5. Don’t rely on observer execution order across modules. Only control order within your own module. If your observer must run before or after another module’s observer, consider using a plugin instead.

  6. Test with event profiling enabled. Add ?debug=events to your URL (with developer mode) to see which events fire on a page. Verify your observer is called when expected and not called when it shouldn’t be.

FAQ

Q: Can I use dependency injection in observers?
Yes. All observer dependencies are injected via the constructor. Magento’s DI container resolves them automatically.

Q: What is the difference between events.xml and frontend/events.xml?
Global events.xml fires in all areas. Area-scoped files only fire when Magento runs in that area (frontend store, admin panel, REST API, or GraphQL).

Q: How do I find what data an event provides?
Check the dispatch() call in the source code. The array keys passed to dispatch() become the getter names. For example, if the code dispatches ['order' => $order], your observer calls $observer->getEvent()->getOrder().

Q: Can one observer listen to multiple events?
Not directly. Each observer class needs a separate declaration in events.xml. However, you can create a single service class and call it from multiple observer execute() methods.

Q: Are observers available in GraphQL?
Yes. GraphQL is its own area (graphql). Place your events.xml in etc/graphql/ to scope observers to GraphQL requests only.


Need a custom module with observers and events wired correctly? Check my custom module development service or read the plugins guide for the companion topic on interceptors.