An Odoo upgrade is a verdict. It reads back, all at once, every customization decision made since the last one, and tells you which were disciplined and which were shortcuts. The teams whose upgrades take a week and the teams whose upgrades take a quarter are usually running the same Odoo version. The difference is in how the custom modules were written.
Five habits separate code that survives a version jump from code that has to be rewritten.
1. Extend, never replace
Odoo's inheritance system exists so you can add behaviour without owning the thing you're adding to. Python overrides should call super() and contribute a delta. View customizations should use targeted xpath to insert or modify specific nodes, not replace a whole arch block with an edited copy.
The reason is mechanical. When you extend, the upgrade only has to reconcile your delta with the new core. When you replace, you have silently forked core code, and every improvement Odoo ships to that view or method for the next three years is now your manual merge problem.
2. Never hardcode a module name you don't own
Module names are not stable across versions. Odoo 19 alone renamed a dozen-plus Enterprise modules: account_auto_transfer became account_transfer, account_disallowed_expenses became account_fiscal_categories, and the entire hr_work_entry_contract_* family dropped its contract infix. Every depends entry, every auto_install bridge, every XML ref pointing at an old name breaks on upgrade.
You cannot prevent the renames, but you can minimize your exposure: depend on the smallest set of modules that actually delivers what you need, and treat every external dependency as a thing to re-verify each version rather than assume.
3. Use the public API, and nothing below it
Odoo's restructuring in 19, splitting the ORM into the odoo/orm/ package, broke essentially no code that imported the documented way and a fair amount of code that reached into internals like odoo.api.Meta or odoo.conf. Private API has no compatibility promise. If an import path isn't in the documentation, treat using it as borrowing against the next upgrade.
4. Don't fight the front-end framework
The most expensive front-end customizations to carry forward are the ones that monkey-patch core JavaScript or manipulate the DOM directly. Odoo 19 makes this concrete: the patch() function now throws if you pass it the old string-name argument. Code that swapped out core component internals, or kept jQuery widgets reaching into rendered markup, is fragile by construction. It depends on the exact shape of core code that the next version will reshape.
The durable pattern is to add, not alter: register your own components and services, extend via documented hooks, and let the framework own its own internals.
5. Make XPath selectors specific, not positional
An xpath that targets //field[@name='partner_id'] survives a core view being reordered. An xpath that targets the third group by position does not. Anchor view customizations to stable, named landmarks (field names, explicit element IDs), never to structure that core is free to rearrange.
What this buys you
Follow these five and an upgrade becomes what it should be: fix the genuine breaking changes, clear the deprecation warnings, test, ship. Ignore them and the upgrade becomes an archaeology project: reconstructing why a forked view exists, what a monkey-patch was working around, which renamed module a dependency meant to point at.
The upgrade tax is unavoidable. Its size is not. It is set, quietly, every time someone chooses between extending the framework and overpowering it.