Resolving sequence, Composer and circular dependencies
Module dependencies in Magento exist on two separate layers: composer.json controls which package gets installed at all, and module.xml sequence controls in what order it gets loaded. Mixing the two builds circular dependencies that only surface at setup:upgrade and are hard to untangle without a clean refactoring.
Table of Contents
- 1. Two layers of dependencies in Magento
- 2. module.xml sequence in detail
- 3. composer.json require vs. suggest
- 4. Detecting circular dependencies
- 5. Resolving circular dependencies
- 6. Optional dependency instead of a hard sequence
- 7. Best practices for your own and third-party modules
- 8. Making the dependency graph visible
- 9. Comparison: sequence vs. composer require vs. soft dependency
- 10. Summary
- 11. FAQ
1. Two layers of dependencies in Magento
Anyone managing module dependencies in Magento 2 inevitably works on two entirely separate layers that are frequently confused. The first layer is the Composer package layer: composer.json decides which package gets downloaded at all via bin/composer install or bin/composer update and placed in the vendor directory. The second layer is Magento's internal module load order: module.xml with its <sequence> element decides in what order the already-present modules merge their etc configuration files, events, and layout handles.
The critical mistake that shows up in almost every project as the module count grows: a developer assumes that a require entry in composer.json automatically also controls the load order in Magento. That's wrong. Composer is only concerned with installing packages and has no relation whatsoever to the internal Magento module order. Without an explicit sequence declaration in module.xml, Magento loads modules in an order that is deterministic but not necessarily predictable for the developer, since it depends among other things on alphabetical order and other already-existing sequence declarations.
This confusion is the root of almost every problem around module dependencies, from unexpected load orders through hard-to-debug events to genuine circular dependencies. In this article we clearly separate both layers, show the correct syntax for sequence, the differences between require and suggest in composer.json, and a concrete approach to permanently avoid circular dependencies between your own modules.
2. module.xml sequence in detail
The <sequence> element in module.xml exclusively controls the order in which Magento merges the modules' etc directories during compilation: di.xml merges, events.xml registrations, acl.xml definitions, and layout handle ordering. A module that references another module in its sequence is guaranteed to load AFTER that other module. This matters when your own module, for example, wants to override a layout handle from the referenced module or register a plugin on a class defined there whose di.xml entry must exist first.
What sequence explicitly does NOT control is the Composer installation. A module can reference another module in its sequence without a corresponding Composer dependency existing. In that case setup:upgrade only works as long as the referenced module happens to also be installed. If it's missing, Magento reports an explicit error during compilation because the sequence points to a non-existent module. Hence the fixed rule: every sequence reference should be accompanied by a matching require entry in composer.json, unless it is intentionally an optional, soft dependency.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<!-- Vendor_PriceSync loads AFTER Magento_Catalog and Vendor_CustomerSync -->
<module name="Vendor_PriceSync">
<sequence>
<module name="Magento_Catalog"/>
<module name="Vendor_CustomerSync"/>
</sequence>
</module>
</config>
An important addition: sequence creates no hard runtime dependency in PHP code. Nothing stops a class in Vendor_PriceSync from directly referencing classes from Magento_Catalog, independent of the sequence declaration. The sequence exclusively regulates the order of the XML merge process, not the actual code coupling at runtime. This distinction is central to understanding the next sections about module dependencies at the Composer level.
3. composer.json require vs. suggest
In composer.json there are two relevant fields for module dependencies: require for hard, mandatory dependencies, and suggest for soft, recommended dependencies. A require entry ensures Composer mandatorily installs the referenced package before your own module is even functional. That's the right choice when your own code actually imports and directly uses classes from the other module, for example a repository interface or a service contract.
suggest, on the other hand, installs nothing automatically but merely shows a hint via composer show that a certain package would meaningfully complement the functionality. The critical mistake that happens frequently in practice: a developer uses suggest for a module whose classes are still directly referenced in their own code, for example in an optional feature branch. If the suggested module isn't installed, the autoloader crashes with a fatal error on the first access to the missing class, because PHP takes no notice of Composer metadata at runtime.
{
"name": "vendor/module-price-sync",
"require": {
"php": "~8.4.0",
"magento/module-catalog": "*",
"vendor/module-customer-sync": "*"
},
"suggest": {
"vendor/module-loyalty-points": "Enables loyalty point recalculation after price sync, if installed"
}
}
The only safe combination for a suggest dependency is a runtime check with Magento\Framework\Module\Manager::isEnabled() before any code from the suggested module is even referenced. This safeguard is covered in detail in section 6. Without this check, suggest isn't really soft in practice, but only an incompletely secured hard dependency that crashes at runtime instead of degrading gracefully when the package is missing.
4. Detecting circular dependencies
A circular dependency arises when module A references module B in its sequence, and module B, directly or through a chain of further modules, in turn references module A again. The typical symptom is a failing bin/magento setup:upgrade with an error message about an unresolvable module order, or a module that shows up as "invalid" in the admin panel without the actual reason being immediately apparent.
The first diagnostic step is bin/magento module:status, which shows the actually computed load order. It's also worth checking the generated app/etc/config.php: all active modules appear there in their final computed order as a numbered array. If two closely related, actually interdependent modules appear there in an unexpected order or with contradictory positions, that's a strong indication of a circular or at least contradictory sequence declaration.
# Check the computed module load order and any warnings
bin/magento module:status
# Typical circular dependency error during compilation:
# Sequence for module "Vendor_ModuleA" is invalid ->
# a circular reference to module "Vendor_ModuleB" was detected
# Inspect the final computed module order (sequence numbers reflect load order)
grep -A 200 "'modules' => \[" app/etc/config.php | head -60
# Composer-level check: does the dependency graph even make sense on that layer?
bin/composer why vendor/module-a
bin/composer depends vendor/module-b
It's important to distinguish between a genuine sequence circularity, which Magento explicitly detects at compile time and flags with an error, and a subtler variant: two modules without a direct sequence circularity that nevertheless reference each other in PHP code via constructor injection. The latter produces no Magento error at setup:upgrade, but leads to a dependency-injection circle that only surfaces as a "circular dependency" error from the Object Manager on the first attempt to instantiate the affected classes.
5. Resolving circular dependencies
The sustainable solution for a circular dependency between two functionally closely related modules follows the model that Magento itself consistently uses in its core architecture: splitting a module into an *Api module with pure interfaces and service contracts, and an implementation module that references this Api module. An example from the Magento core is the separation of Magento_CatalogApi and Magento_Catalog: other modules only reference the interfaces from CatalogApi, without building a direct dependency on the concrete implementation.
Applied to a circular dependency problem between Vendor_ModuleA and Vendor_ModuleB, this means: the jointly needed interfaces, for example a service contract that both modules want to call on each other, get extracted into a third, neutral module Vendor_SharedApi that itself has no dependency on A or B. Both Vendor_ModuleA and Vendor_ModuleB then only reference Vendor_SharedApi in their sequence and their composer.json, and the direct mutual dependency disappears entirely. This resolution is the only genuinely clean path, because it fixes the underlying architecture problem instead of just papering over a symptom by reshuffling sortOrder or sequence values.
<!-- Vendor_SharedApi/etc/module.xml: no dependency on ModuleA or ModuleB -->
<module name="Vendor_SharedApi"/>
<!-- Vendor_ModuleA/etc/module.xml: depends only on the shared API -->
<module name="Vendor_ModuleA">
<sequence>
<module name="Vendor_SharedApi"/>
</sequence>
</module>
<!-- Vendor_ModuleB/etc/module.xml: depends only on the shared API too -->
<module name="Vendor_ModuleB">
<sequence>
<module name="Vendor_SharedApi"/>
</sequence>
</module>
6. Optional dependency instead of a hard sequence
Not every relationship between two modules needs to be a hard module dependency. If module B should only react when module A happens to be installed, but remains fully self-sufficient without A, a soft dependency via Magento\Framework\Module\Manager::isEnabled() is the right approach instead of a hard sequence declaration with an accompanying composer require.
This technique lets you inject only interfaces that exist in every case into the constructor, and only conditionally carry out the actual interaction with the optional module at runtime. Important here: the PHP code must never type-hint a class from the optional module directly in the constructor, since otherwise the autoloader immediately throws a fatal error if the module is missing, regardless of whether isEnabled() would later correctly return false.
declare(strict_types=1);
namespace Vendor\PriceSync\Model;
use Magento\Framework\Module\Manager as ModuleManager;
use Psr\Log\LoggerInterface;
/**
* Notifies the optional loyalty module about a price change, if it is installed.
*/
final class LoyaltyNotifier
{
private const string LOYALTY_MODULE_NAME = 'Vendor_LoyaltyPoints';
/**
* @param ModuleManager $moduleManager Checks whether the optional module is enabled
* @param LoggerInterface $logger Logs a debug notice when the module is absent
*/
public function __construct(
private readonly ModuleManager $moduleManager,
private readonly LoggerInterface $logger,
) {
}
/**
* Recalculates loyalty points only if the optional module is present and enabled.
*
* @param int $productId Product entity ID whose price just changed
* @return void
*/
public function notifyPriceChanged(int $productId): void
{
if (!$this->moduleManager->isEnabled(self::LOYALTY_MODULE_NAME)) {
$this->logger->debug('Loyalty module not installed, skipping recalculation.');
return;
}
// Resolve the optional dependency lazily via ObjectManager,
// never via constructor type-hinting, to avoid a hard autoload dependency.
$loyaltyService = \Magento\Framework\App\ObjectManager::getInstance()
->get(\Vendor\LoyaltyPoints\Api\RecalculationServiceInterface::class);
$loyaltyService->recalculateForProduct($productId);
}
}
This construction deliberately uses the Object Manager as a known, documented exception, because a classic constructor injection here would undo the intended optional nature of the dependency. For all hard module dependencies, constructor injection naturally remains the standard; this exception applies exclusively to genuinely runtime-checked optional dependencies.
7. Best practices for your own and third-party modules
The most important rule for clean module dependencies is: minimal sequence declaration. A module should only reference in its sequence the modules whose etc merge order actually matters, not every remote, indirect dependency. Superfluous sequence entries needlessly increase the complexity of the dependency graph and make later refactorings harder without offering any functional benefit.
Equally important: code should never make implicit assumptions about load order that aren't secured by an explicit sequence declaration. Anyone who, for example, assumes in an observer that a certain other event handler has already run, without ensuring this via a genuine sequence dependency, is building a fragile, implicit module dependency that silently breaks with the smallest change to one of the involved modules. For third-party modules the additional rule is: never patch vendor files directly to add a missing sequence; always use your own small extension module with a correct sequence reference to the foreign module.
8. Making the dependency graph visible
bin/magento module:status delivers the computed load order but not the full dependency graph with every relationship between modules. On the Composer layer, bin/composer why <package> and bin/composer depends <package> provide an overview of which other packages reference a given package, and what it itself depends on. These two commands together give a complete picture of both the Composer package layer and Magento's internal load order.
On more complex projects with twenty or more custom modules, it's worth doing a one-time, documented visualization of the dependency graph, for example as a simple diagram showing each module's sequence targets and its Composer require targets side by side. This documentation makes it visible at a glance where a module dependency exists on only one of the two layers, a common sign of an incomplete or inconsistent declaration.
# Which installed packages depend on this one?
bin/composer why vendor/module-shared-api
# What does this package itself depend on?
bin/composer depends vendor/module-a
# Cross-check: does every composer require have a matching module.xml sequence entry?
grep -r "vendor/module" app/code/Vendor/ModuleA/composer.json
grep -r "Vendor_" app/code/Vendor/ModuleA/etc/module.xml
9. Comparison: sequence vs. composer require vs. soft dependency
The three mechanisms presented solve different tasks and should not be swapped for one another. The overview below summarizes when each mechanism is the right choice for a module dependency.
| Mechanism | Controls | Typical mistake | Correct use |
|---|---|---|---|
| module.xml sequence | Load order of etc merges | Reference without an accompanying composer require | Always combine with composer require |
| composer.json require | Package installation | Without sequence, even though load order matters | When directly using classes in code |
| composer.json suggest | Recommendation without installation | Direct class reference without an isEnabled check | Always secure with Module\Manager::isEnabled() |
| Shared Api module | Resolving circular dependencies | Interfaces remain inside the implementation module | Extract shared interfaces into a neutral third module |
This table shows: every unclean combination of the three mechanisms produces a typical, recurring mistake. The consistent, correct combination of sequence, require, and, where fitting, a runtime-checked soft dependency, prevents practically all the problems described in this article around module dependencies.
10. Summary
Clean module dependencies in Magento require a clear separation of two layers: composer.json controls package installation, module.xml sequence controls the load order of configuration files. Every sequence reference should be accompanied by a matching composer require, except for deliberately soft, runtime-checked dependencies via Module\Manager::isEnabled(). Circular dependencies arise when two modules reference each other directly or through a chain, and can only be reliably resolved permanently by extracting shared interfaces into a neutral, third Api module.
The long-term benefit of a clean module dependency strategy shows especially on growing projects with many custom extension modules: instead of risking a circular reference with every new module, a clear architecture with shared Api modules, minimal sequence declarations, and documented Composer dependencies ensures that new modules fit predictably and without surprises into the existing dependency graph.
Magento Module Dependencies: The Essentials at a Glance
Separate the two layers
composer.json controls installation, module.xml sequence controls load order. Both belong together, not apart.
Secure soft dependencies
Never use suggest without a Module\Manager::isEnabled() check, or risk a fatal error when the module is missing.
Resolve circular dependencies
Extract shared interfaces into a neutral shared Api module instead of using sortOrder tricks.
Diagnostics
module:status, composer why and composer depends together give the complete dependency graph.
11. FAQ: Magento Module Dependencies
1require vs. sequence, what's the difference?
2Is composer require enough for load order?
3require vs. suggest?
4Detect a circular dependency?
5Resolve a circular dependency?
6When to use an optional dependency?
7Allowed to patch vendor files?
8Find the complete dependency graph?
9Name reference in constructor for optional module?
10How many sequence entries make sense?
Mironsoft
Magento module architecture, refactoring and long-term maintainable extensions
Circular dependencies in your module stack?
We analyze existing Magento modules, uncover circular and inconsistent dependencies, and refactor them into a clean, long-term maintainable module architecture.
Architecture review
Document the complete dependency graph of your modules
Refactoring
Permanently resolve circular dependencies with shared Api modules
Module development
Build new extension modules with clean dependency structure from day one