Skip to content
5.0 Upwork, 100+ reviews Adobe Certified since 2021 New: become your own Magento developer Learn more →
Magento glossary

What is Magento di.xml?

Short answer di.xml is Magento 2’s dependency injection configuration file. Every class receives its dependencies through __construct(Dep1 $a, Dep2 $b), and di.xml tells the framework which classes to pass in. It sits at etc/di.xml (global), etc/frontend/di.xml (storefront) or etc/adminhtml/di.xml (admin). I use it for five things: preferences (swap an interface implementation), plugins (wrap a method), type arguments (set constructor parameters), virtualTypes (reuse a class under a new name) and array arguments (pass a list of config into a service).

How it works
  1. Pick the dependency wiring you need 01
  2. Write the di.xml 02
  3. Magento merges every module’s di.xml 03
  4. Compile in production, setup:di:compile 04
  5. Resolved at runtime by the ObjectManager 05
Each step is explained below

Most common mistake Two modules preferring the same interface without <sequence>

How it works

Five steps from XML node to running interceptor

di.xml is read from every enabled module, merged and cached, and setup:di:compile turns it into generated classes and compiled config. This is the path from an XML node to a running interceptor.

  1. 01

    Pick the dependency wiring you need

    There are five jobs di.xml can do, and I decide which one I need before I write any XML. To replace an interface implementation: <preference for="..." type="..."/>. To wrap a method on an existing class: a plugin inside <type><plugin/></type>. To pass specific constructor arguments: <type><arguments><argument name="..."/></arguments></type>. To reuse a class under a new name with different arguments: <virtualType>. To hand a service a list of things (filters, mappings, allowed types): <argument xsi:type="array">.

  2. 02

    Write the di.xml

    The root element is <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">, and its children are <preference>, <type> and <virtualType>. The file’s location sets the scope. etc/di.xml applies everywhere, etc/frontend/di.xml only on the storefront, etc/adminhtml/di.xml only in the admin, and there are also webapi_rest, webapi_soap, graphql and crontab areas. An area file is merged on top of the global one, so I put a plugin in etc/frontend/di.xml when it should only run on the storefront.

  3. 03

    Magento merges every module’s di.xml

    Magento reads the di.xml of every enabled module, merges them into one configuration per area and caches the result. The merge order follows the module list in app/etc/config.php, which setup:upgrade sorts using each module’s <sequence> in module.xml. When two modules set a preference for the same interface, the one merged last wins. That is why I always add a <sequence> on the module I am overriding.

  4. 04

    Compile in production, setup:di:compile

    bin/magento setup:di:compile reads the merged config and writes three kinds of classes under generated/code/. Interceptors for every class with a plugin (Foo\Bar\Interceptor extends Foo\Bar and routes the plugged methods through the plugin list). Factories for every \Foo\BarFactory reference. Proxies for every \Foo\Bar\Proxy reference. In default and developer mode Magento creates these on the first request that needs them, which is slow. In production I compile them once on deploy.

  5. 05

    Resolved at runtime by the ObjectManager

    When a class is requested, through constructor injection or ObjectManagerInterface::create() / get(), the ObjectManager checks the merged config. A preference decides which class is built. If that class has plugins, its Interceptor is built instead. Type arguments fill in constructor parameters, and a virtual type resolves to its real class with its own arguments. Inside your class none of this is visible: you just write __construct(Dep1 $a, Dep2 $b).

When to use

Four scenarios, pick the right di.xml primitive for the job

Most di.xml problems I clean up come from picking the wrong tool for the job. Each of these four jobs has one clear answer.

  • Replacing a Magento or third-party class entirely

    Use a <preference>. For example: “for Magento\Catalog\Api\ProductRepositoryInterface, use Vendor\Module\Model\MyProductRepository”. Magento’s own di.xml files are mostly preferences that map each public interface to its default class, and your module points that mapping at your class instead. I make the replacement extend the original or implement the same interface, and I only do this when a plugin can’t reach the logic.

  • Wrapping a method without replacing the class

    Use a plugin: <type name="Vendor\Target"><plugin name="my_plugin" type="Vendor\Module\Plugin\MyPlugin"/></type>. A plugin runs before, around or after a public method without subclassing, and it can live next to other modules’ plugins on the same class. When a client asks me to change how Magento behaves, a plugin is usually my first answer. A preference is heavier and clashes more easily with other modules.

  • Reusing a generic class with different configuration

    Use a <virtualType>. Magento core uses them all over, in collection processors, UI data providers and logger handlers. For example, two filter managers based on Magento\Framework\Filter\FilterManager but registered as MyCatalogFilters and MyCustomerFilters, each with different arguments. You get a new name in DI without writing a PHP class, and you can use that name anywhere di.xml expects a class name.

  • Injecting a config array into a service class

    Use <type name="Vendor\Service"><arguments><argument name="rules" xsi:type="array">...</argument></arguments></type>. The array can hold strings, numbers, nested arrays or xsi:type="object" references to other classes. Magento uses this wherever a service needs a list of validators, processors or formatters. It is also how other modules add their own entry to your list later, just by adding an item in their own di.xml.

Common mistakes

Three di.xml mistakes that break production silently

Every “my plugin isn’t firing” ticket I have ever debugged came from one of these three. Check them in this order before reaching for xdebug.

  • Two modules preferring the same interface without <sequence>

    Both modules declare <preference for="Some\Interface" type="Their\Class"/>. The last-loaded module wins, and load order with no explicit sequence falls back to alphabetical, meaning Vendor_A beats Vendor_Z… until a third module ships and re-shuffles everything. Always declare <sequence> in your module’s etc/module.xml listing every module whose classes you intend to override. Make load order explicit, not accidental.

  • Using <preference> when a plugin would do

    Preferences fully replace the class. Every other module’s plugins on the original class get bypassed unless your replacement extends the original. Worse, two modules can’t both prefer the same interface to different classes, one loses. Plugins compose: ten modules can all wrap the same method without conflict. The default answer for “change one method” is a plugin. Reach for a preference only when you really need to swap the entire class (different constructor signature, different inheritance) and accept that you become the new base.

  • Forgetting setup:di:compile after a di.xml edit in production

    Production mode serves pre-compiled interceptors from generated/code/. Edit di.xml, push to live, restart php-fpm, nothing happens, because Magento is still serving the old interceptor classes that were generated before your plugin existed. The fix is bin/magento setup:upgrade && bin/magento setup:di:compile && bin/magento cache:flush. Bake this into the deploy script. Symptom: your plugin / preference doesn’t fire and there’s no error in the log because Magento doesn’t know your wiring exists yet.

FAQ

Magento di.xml, frequently asked questions

  • di.xml vs config.xml, what’s the difference?
    Different files for different jobs. di.xml is dependency-injection wiring, it tells the ObjectManager how to instantiate classes, which plugins wrap which methods, and which preferences override which interfaces. config.xml in Magento 2 is largely a legacy carry-over from Magento 1; the vast majority of default-value configuration in M2 lives in system.xml (for admin-editable settings), default_etc.xml fragments, or other area-specific files. If you’re wiring constructor dependencies or registering interceptors, you want di.xml. If you’re setting default values for store-scope config or registering an admin config field, you want system.xml.
  • Can I have multiple di.xml files in one module?
    Yes, and you usually should, one per area. etc/di.xml applies globally (every area). etc/frontend/di.xml applies only when Magento is running in the frontend area. etc/adminhtml/di.xml applies only in admin. etc/webapi_rest/di.xml and etc/webapi_soap/di.xml apply only to REST and SOAP API requests respectively. Magento loads the global di.xml first, then merges the area-specific one on top when bootstrapping that area. This lets you, for example, replace a class on the storefront but keep the stock behaviour in admin.
  • Why is my preference being ignored?
    Almost always module load order. Another module declared a <preference> for the same interface and loads after yours, so its preference wins. Open app/etc/config.php and look at the modules array order; the further down a module sits, the later it loads. To fix, add a <sequence> block to your module’s etc/module.xml listing every module whose preference you want to override, this forces Magento to load your module after them. Less commonly: the area scope is wrong (you put the preference in etc/adminhtml/di.xml but the code runs on the storefront), or your replacement class has a syntax error and Magento silently falls back. Check var/log/exception.log.
  • Can I do conditional DI based on store or website?
    No, di.xml is parsed once at boot and applies globally to every request. There’s no <if store="..."> primitive, and adding one would defeat the purpose of pre-compilation. The standard answer is to put the conditional logic inside the class itself: inject Magento\Framework\App\Config\ScopeConfigInterface via the constructor, read the store-scoped flag, and branch on it at method-call time. For more elaborate strategies, inject an array of strategy objects via di.xml and have the class pick the right one based on store-scope config at runtime, same idea, cleaner separation.
  • What’s a virtualType actually for?
    Cloning a generic class under a unique name with specific arguments. Say you have a generic FilterManager that accepts an array of filter rules in its constructor. You need two configured instances, one for catalog filters, one for customer filters. Without virtualType you’d write two near-identical PHP classes. With virtualType you write <virtualType name="MyCatalogFilters" type="Vendor\FilterManager"><arguments>...</arguments></virtualType> and another for customer filters, both resolve to the same PHP class but with their own DI arg map. Reference them by their virtual name anywhere a class name is expected. Magento core uses this heavily for collection processors, factories, and configurable services.
  • Does Hyvä use di.xml?
    Yes, di.xml is a framework-level mechanism, not a theme-level one. Hyvä is a frontend theme replacing Luma’s heavy KnockoutJS / RequireJS stack with Tailwind + Alpine.js, but the PHP layer underneath, the ObjectManager, the merged di.xml, the interceptor pipeline, is identical to a Luma site. Plugins and preferences registered in any module’s di.xml fire the same way regardless of which theme is active. The only Hyvä-specific consideration is that frontend-only di.xml (etc/frontend/di.xml) targeting Luma block classes (e.g. Magento\Catalog\Block\Product\View) may not run on Hyvä storefronts simply because Hyvä renders the page through different block classes, but that’s a block-selection issue, not a di.xml limitation.
di.xml audit

Want a di.xml architecture review on your Magento module?

Send your module repo, I will audit every preference, plugin, virtualType, and type-arg injection, flag the sequence collisions and preference-vs-plugin smells, then reply with a written refactor plan, fixed-price quote, and earliest start date. 24-business-hour turnaround.

Kishan Savaliya Online 10 AM to 7 PM IST Chat on WhatsApp