Every merchant wants a different checkout: an extra field, a delivery date picker, a reordered step. Every Magento upgrade season, agencies inherit checkouts that cannot be upgraded because a previous developer took shortcuts. The difference between the two outcomes is not budget - it is technique. These are the customisation patterns that survive.
Rule One: Merge, Never Override
Magento merges every module’s checkout_index_index.xml into one tree. The safe pattern is to declare only the nodes you change:
<item name="sidebar" xsi:type="array">
<item name="children" xsi:type="array">
<item name="summary" xsi:type="array">
<item name="config" xsi:type="array">
<item name="template" xsi:type="string">Acme_Checkout/summary/custom</item>
</item>
</item>
</item>
</item>
Copying the entire 2,000-line core file into your theme to change one node freezes that file at today’s version - every future core checkout improvement is silently lost, and security patches stop applying cleanly.
Rule Two: Extend JS with Mixins, Not File Copies
Need to change the behaviour of a core JS component? Use a RequireJS mixin:
// requirejs-config.js
var config = {
config: {
mixins: {
'Magento_Checkout/js/action/set-shipping-information': {
'Acme_Checkout/js/action/set-shipping-information-mixin': true
}
}
}
};
define(['mage/utils/wrapper'], function (wrapper) {
'use strict';
return function (originalAction) {
return wrapper.wrap(originalAction, function (proceed, payload) {
// your logic before/after
return proceed(payload);
});
};
});
The mixin wraps the original function rather than replacing the file. Core updates flow through; your wrapper stays on top. The unsafe alternative - copying the whole JS file into your theme - is the single most common cause of checkout regressions after upgrades.
Rule Three: Use Layout Processors for Dynamic Changes
When field configuration must depend on runtime data (store config, customer group), a layout processor plugin modifies the jsLayout array in PHP before it is sent to the browser:
<type name="Magento\Checkout\Block\Checkout\LayoutProcessor">
<plugin name="acme_checkout_fields" type="Acme\Checkout\Plugin\LayoutProcessorPlugin"/>
</type>
This is the correct home for “hide company field for B2C customers” and “make telephone required” - not JS hacks on rendered DOM.
Rule Four: Keep Custom Fields in Extension Attributes
Adding a field that must reach the order? Do not stuff it into a random column. Declare it as an extension attribute on the order or address interface (etc/extension_attributes.xml), populate it via a plugin on the save path, and read it via the API. Extension attributes are the upgrade-stable contract for exactly this use case.
Rule Five: Test the Upgrade Path, Not Just the Feature
Before launch, and before every Magento upgrade: place test orders through every payment method, as guest and registered, on mobile. Automate it if the checkout is heavily customised - a checkout regression costs more per hour than almost any other bug.
Or skip the whole problem: Hyva Checkout replaces the Knockout stack with Alpine and templates you actually own. For heavily customised checkouts, the migration cost is often lower than the third year of fighting jsLayout. Either way, the rule is the same: extend through the seams Magento provides, and upgrades stay boring.