Custom modules: extending Magento where it is designed to be extended
Magento is built from modules, and custom functionality belongs in custom modules, not in the core. There are two intended ways to change existing behaviour. Plugins, also called interceptors, modify public methods before, after or around the call and are declared in di.xml; Magento generates the interceptor classes itself. Observers react to events dispatched by Magento or another module and are registered in events.xml, either globally or per area such as the storefront or the admin. We choose whichever touches least: an after plugin rather than an around plugin, an observer whenever a suitable event exists. The smaller the intervention, the smaller the check at the next upgrade.
A module receives its dependencies through dependency injection: the object manager supplies whatever is configured in di.xml. We do not inject the object manager itself, even though legacy code often does — it hides dependencies and makes testing harder. A preference, meaning a custom default implementation for an interface or class, is used only where no plugin will do. It replaces the original completely, and of several preferences for the same class only one wins. Towards the outside, a good module communicates through service contracts: PHP interfaces split into data interfaces for the data structures and service interfaces for the operations. Other modules, integrations and later extensions then depend on a contract, not on an implementation.
A module describes its tables and columns with declarative schema instead of the old upgrade scripts. Content that ships with the module — attributes, configuration, master data — is created by data patches; changes that cannot be expressed declaratively go into schema patches, and both can be made revertible where needed. Every module is checked against the Magento Coding Standard, a rule set for PHP_CodeSniffer. Deployment runs through Composer and Git, with bin/magento setup:upgrade, setup:di:compile and setup:static-content:deploy in production mode, never by uploading files by hand. The code sits in a Git repository that belongs to you, and every change can be traced. A module is finished only when another developer can read it without having to ask.
Third-party extensions and legacy code: reviewed and made upgrade-safe
Most Magento shops that come to us have a history: extensions from the Adobe Commerce Marketplace, free or paid, modules from previous agencies, small changes made directly in the code. So we start with an inventory of every module. Where does it come from, does anyone still maintain it, and which Magento and PHP versions is it built for? Which plugins, observers and preferences does it bring, which classes does it replace, where does it clash with other modules? Are there changes in vendor/ or copied core files? On top of that, the Coding Standard runs over the custom code, and setup:di:compile shows whether the whole thing compiles cleanly. The outcome is a list, not an opinion.
Each module then gets a decision: keep it, update it, replace it with a maintained alternative, rebuild it properly as a custom module, or remove it because nobody uses the feature any more. Core changes are moved out into custom modules, as plugins or observers, so that Composer has a free hand again at upgrade time. On tooling: according to Adobe, the Upgrade Compatibility Tool is available for Adobe Commerce instances only. For Magento Open Source we therefore check with the Coding Standard, the compiler, existing tests and a full run on a staging system with a copy of the live data. Marketplace access keys are tied to the edition, too; a key for Open Source does not work for Adobe Commerce. The cheapest upgrade check is the one for an extension that is no longer installed.
How much work an upgrade can mean for your extensions is clear from the step to 2.4.9. Adobe replaced Zend_Cache with symfony/cache and Laminas MVC with its own MVC implementation, raised the Symfony dependencies to 7.4 LTS and swapped TinyMCE for HugeRTE in the WYSIWYG editor. According to Adobe, extensions that use Zend_Cache must be adapted; anyone who has extended the editor needs to test against HugeRTE. For 2.4.9 only PHP 8.5 is approved. We adapt our own and inherited modules and check with the vendors of purchased extensions when compatible versions will be available. How upgrades, security patches and operation work as a whole is covered on our Magento Open Source page. This page is about the code that has to come along.
Integrations with ERP, PIM and shipping
An integration is decided by the question of direction: which system holds the master data for products, prices, stock, customers and orders, and which one only reads? Once that is settled field by field, the technology is craft. Magento provides the Commerce Web API for this, as a REST and a GraphQL interface, both available in Magento Open Source. We mostly use REST for data exchange between systems and GraphQL where a frontend or an app queries specific data. If the ERP offers no interface but exports files, a custom module imports those files, following the same rules for logging and error handling. Which ERP or PIM sits behind it is secondary. The effort lies in the state of the data, not in the protocol.
Large volumes of data do not belong in a single request that runs until something breaks. Magento handles bulk operations through message queues: the REST bulk endpoints, for example, place jobs in a queue that consumers work through, started with bin/magento queue:consumers:start. Magento supports RabbitMQ and ActiveMQ Artemis as brokers; in the 2.4.9 release notes Adobe recommends ActiveMQ Artemis for the long term. Regular synchronisation runs as scheduled jobs via the Magento cron. Every run writes a log, a faulty record ends up on an error list instead of halting the whole sync, and an aborted run can be repeated without creating duplicate data. If the consumers stop, monitoring should be the one to notice, not a customer.
One example from our work is the Magento shop of Manotura GmbH & Co. KG for irrigation technology: sprinkler systems, pipe systems and accessories, a range that can only be filtered sensibly by technical attributes. Product master data, prices and stock come from the ERP, and orders go back to it. The transfers run as scheduled jobs with logging and retry logic, so a single failure does not block the whole sync. More than 12,000 products sit in the synchronised catalogue, prices and stock are reconciled every hour, and maintenance effort is around 40 per cent lower than with double data entry. For shipping, 2.4.9 includes a DHL connection via the MyDHL REST API; other carriers we connect through their own interfaces. Data is maintained in one place, and the shop follows.
B2B in Magento Open Source: custom modules instead of a B2B licence
Adobe's B2B module belongs to Adobe Commerce and, according to Adobe, requires a separate licence. Customer Segments and Content Staging, among others, are also available in Adobe Commerce only. Anyone using Magento Open Source therefore builds B2B features as custom modules — on a foundation that Open Source already provides: customer groups, tier prices, catalogue and cart price rules, multiple websites, stores and store views with their own languages and currencies, plus Inventory Management, which is installed by default. That is more than many expect, and for some B2B shops it is already enough. The rest is development that fits your sales process rather than a ready-made feature bundle. Where an Adobe Commerce licence is already in place, we use the B2B module and extend it where it falls short.
Typical modules we build for B2B on Open Source: prices per customer from the ERP instead of a separate customer group for each customer, company accounts with several buyers and roles, order approvals above a certain value or for specific product groups, budgets per cost centre, quick order by part number and quantity, quote requests from the basket, and a catalogue that shows prices only after a verified registration. If buyers order from a procurement system, an OCI punch-out is added; the basket then goes back into the customer's own system. Each of these features uses the intended extension points — plugins, observers and service contracts — and gets its own tables via declarative schema. We build what your sales team actually needs, not everything a B2B package might contain.
Customer-specific prices have a technical side that is often underestimated. A price that applies to one customer only must not end up in the HTTP cache for everyone, has to be taken into account during indexing and must apply in search, filters and search suggestions just as on the product page. With many customers and frequent price changes, whether prices are calculated in advance or determined on request is an architectural decision with consequences for indexers and page load times. We make it before development starts, based on your numbers of customers, products and price changes. How visibility rules and customer prices apply in shop search is covered on our page about shop search with OpenSearch. A business customer will forgive a slow page sooner than a price meant for someone else.
Frontend and performance: theme, Hyvä, indexers, cache
A custom theme inherits from an existing one and overrides specific layout files and templates rather than copying them wholesale. Every overridden file has to be read against the new original at upgrade time, so the number of overrides determines the maintenance cost, not the look. Since 10 November 2025 the Hyvä Theme has been free and open source, under the same licences as Magento Open Source, OSL-3.0 and AFL-3.0. Hyvä UI, Hyvä Checkout and the Hyvä Commerce and Hyvä Enterprise packages remain paid products. Whether Hyvä suits your shop depends less on the theme than on your extensions: every module that brings its own frontend has to work with the chosen theme. We check that before the decision, module by module.
Performance starts with measurement. When category and product pages are slow, the cause is rarely the hardware and usually indexers, cache and modules. Indexers keep calculated data such as prices, category assignments and stock up to date; bin/magento indexer:reindex rebuilds them once, but they stay current only with a working cron. In front sits Varnish as the HTTP cache, serving finished pages without starting PHP — as long as no module needlessly marks a page as uncacheable. Valkey handles cache and sessions, and message queue consumers process background tasks. If one of these components fails or is misconfigured, the whole shop slows down, and the symptom rarely points to the cause.
So we first look for the place where the time is lost: in the database, in a plugin that makes an external request for every product, in a template, in an indexer that lags behind, or in search. Search and filters with OpenSearch have a page of their own; here the subject is the code. In production mode, set with bin/magento deploy:mode:set production, compilation and static content deployment run as part of the switch; a live shop left in developer mode wastes time on every request. Running the services behind it — servers, Varnish, Valkey, OpenSearch, staging — is described on our Magento Open Source page. An optimisation nobody has measured is a guess with an invoice.