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.
Keep going
- Learning paths Backend, Hyvä, DevOps, architect: pick the path that matches your role.
- Magento playground Safe sandbox recipes for plugins, observers, GraphQL, and UI components.
- Glossary 80+ Magento and Claude Code terms, defined without jargon.
- Recipes cookbook Copy-paste recipes that turn one prompt into shippable Magento code.
- Prompt engineering Write prompts that produce Marketplace-pass Magento code.
- Anti-patterns Twelve ways AI-paired Magento work goes wrong, and the fixes.
- CLI sandbox Run Claude Code on Magento safely with Docker, hooks, and allowlists.
- Slash-commands Production slash-commands for Magento 2 workflows.
- 30-day roadmap Day-by-day plan to ship a Magento module with Claude Code.
- Code reviewer agent A drop-in Claude sub-agent that reviews Magento PRs for shippability.
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/ 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.