Magento 2 Module Anatomy Explained, File by File

Anatomy of a Magento module

Click any file to learn what it does, when you need it, and which Magento subsystem it talks to.

folder config / code docs / data

Pick any node from the tree to see when you need it, what it does, and a Claude Code prompt that scaffolds it for you.

Common questions

What is the difference between registration.php and etc/module.xml?

registration.php tells Composer's autoloader where the module lives on disk, it runs at bootstrap and registers the namespace + path with ComponentRegistrar. Without it, Magento literally cannot find your code. etc/module.xml tells Magento's module manager the module exists, what its setup_version is, and its load-order dependencies via . You need both: registration.php for the autoloader, module.xml for module:enable. A common bug is a working registration.php with a missing module.xml, the class loads, but DI etc/di.xml is silently ignored because the module is not enabled.

Why does my plugin in etc/di.xml not fire on the storefront?

You almost certainly put it in etc/frontend/di.xml when you needed it in etc/di.xml, or vice-versa. Magento has four DI scopes: etc/di.xml (global), etc/frontend/di.xml (storefront only), etc/adminhtml/di.xml (admin only), etc/webapi_rest/di.xml + webapi_soap/di.xml (REST/SOAP). Plugins on storefront-only services (e.g. CustomerSession) live under etc/frontend/. Plugins on universal services (e.g. OrderRepository) live in global etc/di.xml. Run bin/magento setup:di:compile after every move, the compiled interceptors are area-scoped and won't pick up the change otherwise.

Should I use db_schema.xml or InstallData / UpgradeData scripts?

db_schema.xml, always, for table structure. The legacy InstallSchema / UpgradeSchema classes are deprecated since 2.3 and removed-in-spirit since 2.4. Declarative schema lets you diff via setup:db-declaration:generate-whitelist, supports rollback, and survives setup:upgrade --safe-mode. For data seeds (config rows, EAV attributes, sample products), use Setup/Patch/Data/.php implementing DataPatchInterface. Patches run once, are tracked in patch_list, and are idempotent if you write them right. Don't mix the two, declarative schema for structure, data patches for content.

How do ACL resources in etc/acl.xml actually gate admin access?

An ACL resource is just a string ID like Vendor_Module::manage_widgets. You declare it in etc/acl.xml under the Magento_Backend::admin tree, then reference it in three places: (1) etc/adminhtml/menu.xml, the resource attribute hides the menu item from users without the role; (2) the controller's _isAllowed() method, gates the page itself; (3) etc/adminhtml/system.xml, gates config sections. ACL is permission-only, it doesn't authenticate, it doesn't audit, it just answers yes/no. Always implement _isAllowed(); menu hiding is cosmetic, anyone with the URL can hit the controller without it.

Do I need both etc/webapi.xml and etc/schema.graphqls for one feature?

Only if you want both REST and GraphQL clients. The good pattern: build a Service Contract (Api/ + Api/Data/ + Repository) once, then expose it through whichever transport(s) you need. etc/webapi.xml maps an HTTP route + method to a service interface method, pure declarative. etc/schema.graphqls defines a GraphQL type and field, and a Resolver class translates the GraphQL args into a service-contract call. Both transports re-use the same Repository, so business logic lives in one place and you avoid the classic Magento 2 trap of REST and GraphQL drifting out of sync.

Send a brief. Get a written quote in 24 hours.

Two paragraphs is enough: scope, price and timeline come back in writing.