<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Boga</title>
    <description>The latest articles on DEV Community by Boga (@bogaboga1).</description>
    <link>https://dev.to/bogaboga1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3707473%2F0e62e303-83d0-4b1e-b2c1-86451919f1fd.png</url>
      <title>DEV Community: Boga</title>
      <link>https://dev.to/bogaboga1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bogaboga1"/>
    <language>en</language>
    <item>
      <title>Reinventing a Solved Problem: An Architectural Review of Odoo OWL Frontend Framework</title>
      <dc:creator>Boga</dc:creator>
      <pubDate>Sun, 18 Jan 2026 19:39:25 +0000</pubDate>
      <link>https://dev.to/bogaboga1/reinventing-a-solved-problem-an-architectural-review-of-odoo-owl-frontend-framework-2113</link>
      <guid>https://dev.to/bogaboga1/reinventing-a-solved-problem-an-architectural-review-of-odoo-owl-frontend-framework-2113</guid>
      <description>&lt;p&gt;In this post, I want to focus on Odoo’s OWL framework — the first major layer of frontend complexity in Odoo’s web stack — and question whether building it was truly necessary, or whether it was an avoidable source of long-term complexity justified by the familiar argument: “it’s an ERP, so it must be complex.”&lt;/p&gt;

&lt;p&gt;For context, OWL (Odoo Web Library) is the JavaScript framework used to power Odoo’s frontend components, including the dashboard, backend UI, and parts of the website.&lt;/p&gt;

&lt;p&gt;According to Odoo, OWL was built from scratch to solve a specific problem: allowing third-party developers to override and extend frontend components defined by other modules without modifying core files or losing changes during upgrades.&lt;/p&gt;

&lt;p&gt;On paper, this goal is reasonable — even admirable. However, the conclusion that this &lt;em&gt;required&lt;/em&gt; building an entirely new frontend framework is far more questionable.&lt;/p&gt;

&lt;p&gt;The same goal is already achievable in all major modern frontend frameworks (React, Vue, Angular) through well-established mechanisms such as component composition, slots, higher-order components, dependency injection, extension APIs, and schema-driven rendering.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a Mature Frontend Framework Would Have Provided To Odoo ?
&lt;/h3&gt;

&lt;p&gt;By using a &lt;strong&gt;mature frontend framework&lt;/strong&gt; would have provided &lt;strong&gt;several major advantages&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Clear, evolving documentation&lt;/strong&gt;
Mature frameworks have continuously updated documentation that closely tracks real-world usage and features. In contrast, Odoo’s documentation is often incomplete, outdated, or misleading — a problem significant enough to warrant a dedicated post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security responsiveness&lt;/strong&gt;
Modern frontend ecosystems respond rapidly to security disclosures, issuing patches without requiring a full application upgrade. In Odoo, frontend fixes are tightly coupled to backend releases, making security patching significantly more disruptive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A vast ecosystem&lt;/strong&gt;
From form builders and schema validators to accessibility tooling, testing frameworks, and UI component libraries — modern ecosystems provide solutions that Odoo either reimplements partially or lacks entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer familiarity&lt;/strong&gt;
Frontend developers today are already fluent in React, Vue, or Angular. OWL introduces a proprietary mental model that developers must learn on top of Odoo’s already complex backend abstractions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead, Odoo now maintains a hybrid frontend stack where OWL coexists with legacy code — including multiple versions of jQuery (2.x and 3.x) still present in parts of the system. This alone should raise questions about long-term maintainability.&lt;/p&gt;

&lt;p&gt;Before comparing OWL directly to React or Vue, it’s important to understand how OWL actually works.&lt;/p&gt;

&lt;p&gt;At its core, OWL consumes XML definitions sent from the backend and dynamically builds a component tree on the frontend. These XML views are parsed, interpreted, and translated into JavaScript-rendered UI components.&lt;/p&gt;

&lt;p&gt;For example, consider a simple OWL form definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;field&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"my_model_field_name"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend receives this XML and uses the OWL runtime to translate it into rendered UI components.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parses the XML into a component tree&lt;/li&gt;
&lt;li&gt;Issues additional requests to fetch model field metadata&lt;/li&gt;
&lt;li&gt;Decides which component to render based on field definitions&lt;/li&gt;
&lt;li&gt;Optionally uses the &lt;code&gt;widget&lt;/code&gt; attribute to select a custom renderer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, OWL acts as a runtime XML interpreter that generates UI behavior dynamically.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Hard Truth About OWL
&lt;/h3&gt;

&lt;p&gt;OWL’s flexibility does not come from a fundamentally new idea — it comes from &lt;strong&gt;deferring structure and behavior decisions to runtime&lt;/strong&gt;. This is not unique, nor does it require a custom framework.&lt;/p&gt;

&lt;p&gt;The same level of flexibility can be achieved in React for example by using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schema-driven rendering&lt;/li&gt;
&lt;li&gt;Plugin registries&lt;/li&gt;
&lt;li&gt;Declarative extension points&lt;/li&gt;
&lt;li&gt;Controlled overrides via dependency injection&lt;/li&gt;
&lt;li&gt;Permissioned component replacement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many systems already parse XML, JSON, or DSLs and render them safely within mature frameworks — without reinventing rendering lifecycles, state management, reactivity, or tooling.&lt;/p&gt;

&lt;p&gt;By choosing to build OWL, Odoo accepted the burden of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintaining a proprietary framework&lt;/li&gt;
&lt;li&gt;Rebuilding tooling that already exists elsewhere&lt;/li&gt;
&lt;li&gt;Fragmenting frontend knowledge&lt;/li&gt;
&lt;li&gt;Coupling frontend evolution to backend architectural constraints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a follow-up section, I’ll demonstrate how Odoo’s XML-based UI model could be rendered and overridden cleanly using React.js, achieving the same extensibility without introducing a custom framework. The goal isn’t to claim OWL is unusable — but to show that building it was an unnecessary architectural choice that added long-term cost without solving a novel problem.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A Simple OWL Component vs a React Component
To make the discussion concrete, let’s compare a minimal OWL component with an equivalent React component.
&lt;strong&gt;OWL Component (Simplified)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** @odoo-module **/&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;xml&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@odoo/owl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Hello&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;xml&lt;/span&gt;&lt;span class="s2"&gt;`
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;Hello &amp;lt;t t-esc="props.name"/&amp;gt;&amp;lt;/h1&amp;gt;
    &amp;lt;/div&amp;gt;
  `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usage is typically tied to XML view definitions sent from the backend, and the component lifecycle, state handling, and rendering behavior are governed by OWL’s custom runtime.&lt;/p&gt;




&lt;h3&gt;
  
  
  React Component (Equivalent)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Hello&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Hello &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At a surface level, both components are trivial. The key difference isn’t syntax — it’s &lt;strong&gt;ecosystem gravity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In React:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Component composition is standard&lt;/li&gt;
&lt;li&gt;Tooling (linting, testing, profiling) is mature&lt;/li&gt;
&lt;li&gt;State, effects, and error boundaries are well-defined&lt;/li&gt;
&lt;li&gt;Integration with schema-driven rendering is commonplace&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OWL must reimplement or approximate much of this — while also introducing a proprietary mental model that developers must learn &lt;em&gt;in addition to&lt;/em&gt; Odoo’s backend abstractions.&lt;/p&gt;




&lt;ol&gt;
&lt;li&gt;React Pseudo-Implementation That Mirrors OWL Overrides
A common defense of OWL is that it allows &lt;em&gt;runtime UI overrides&lt;/em&gt; — the ability for modules to replace or extend UI behavior dynamically without editing core code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not unique to OWL.&lt;/p&gt;

&lt;p&gt;Below is a simplified &lt;strong&gt;React-based architecture&lt;/strong&gt; that mirrors the same capability.&lt;/p&gt;




&lt;h3&gt;
  
  
  Component Registry (Core)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ComponentRegistry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;registerComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ComponentRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ComponentRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Schema-Driven Renderer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Renderer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{...&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Default Registration (Core Module)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;registerComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;field:text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;registerComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;field:number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NumberField&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Override by Addon / Third-Party Module
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;registerComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;field:text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CustomTextField&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No core files edited. No fork. No rebuild of a framework just to justify ERP is complex and it needs to have complex UI framework with no ecosystem no tooling no documentation.&lt;/p&gt;

&lt;p&gt;by using this simple structure at least someone can achieve almost everything in OWL.js while also if needed they can leverage existing ecosystem nice tooling.&lt;br&gt;
As well as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runtime replacement&lt;/li&gt;
&lt;li&gt;Controlled extension points&lt;/li&gt;
&lt;li&gt;Clear override ownership&lt;/li&gt;
&lt;li&gt;Predictable behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same pattern scales to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Permissions&lt;/li&gt;
&lt;li&gt;Feature flags&lt;/li&gt;
&lt;li&gt;User-specific overrides&lt;/li&gt;
&lt;li&gt;Context-based rendering&lt;/li&gt;
&lt;li&gt;A better implementation that handles menus and UI translations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is effectively what OWL does — but OWL bundles it with a custom rendering engine, lifecycle model, and tooling stack.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. “But React Can’t Do Runtime UI Overrides”
&lt;/h3&gt;

&lt;p&gt;This is the most common objection — and it’s based on a misconception.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;React absolutely supports runtime UI overrides.&lt;/strong&gt;&lt;br&gt;
What it does &lt;em&gt;not&lt;/em&gt; support is &lt;em&gt;implicit, unstructured mutation&lt;/em&gt; — and that’s a feature, not a limitation.&lt;/p&gt;

&lt;p&gt;Runtime overrides in React are achieved via:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Registries&lt;/li&gt;
&lt;li&gt;Context providers&lt;/li&gt;
&lt;li&gt;Dependency injection patterns&lt;/li&gt;
&lt;li&gt;Plugin systems&lt;/li&gt;
&lt;li&gt;Schema-driven rendering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of which are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explicit&lt;/li&gt;
&lt;li&gt;Traceable&lt;/li&gt;
&lt;li&gt;Testable&lt;/li&gt;
&lt;li&gt;Tooling-friendly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OWL’s approach relies heavily on runtime interpretation of XML combined with implicit behavior resolution. This provides flexibility — but at the cost of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debuggability&lt;/li&gt;
&lt;li&gt;Static analysis&lt;/li&gt;
&lt;li&gt;Predictable failure modes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;React’s ecosystem favors &lt;strong&gt;controlled extensibility&lt;/strong&gt; over unrestricted mutation. That makes large systems &lt;em&gt;more maintainable over time&lt;/em&gt;, especially when multiple teams and third-party developers are involved.&lt;/p&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OWL optimizes for &lt;em&gt;maximum runtime freedom&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;React optimizes for &lt;em&gt;sustainable extensibility&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Closing Clarification
&lt;/h2&gt;

&lt;p&gt;The argument here is not that OWL is unusable, nor that Odoo developers are unaware of existing frameworks.&lt;/p&gt;

&lt;p&gt;The argument is that &lt;strong&gt;the problem OWL solves was already solved&lt;/strong&gt;, and rebuilding a framework to solve it again introduced long-term cost without introducing a fundamentally new capability.&lt;/p&gt;

&lt;p&gt;Odoo didn’t just choose flexibility — it chose to own the entire frontend stack. And owning the stack means owning every limitation, bug, security issue, and ecosystem gap that comes with it.&lt;/p&gt;

&lt;p&gt;That is the real cost of reinventing the web stack. This does not make Odoo unusable — but it does make its long-term evolution far more expensive than it needed to be.&lt;/p&gt;




</description>
      <category>python</category>
      <category>odoo</category>
      <category>architecture</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Odoo Core and the Cost of Reinventing the Web Stack</title>
      <dc:creator>Boga</dc:creator>
      <pubDate>Tue, 13 Jan 2026 20:09:36 +0000</pubDate>
      <link>https://dev.to/bogaboga1/odoo-core-and-the-cost-of-reinventing-the-web-stack-4537</link>
      <guid>https://dev.to/bogaboga1/odoo-core-and-the-cost-of-reinventing-the-web-stack-4537</guid>
      <description>&lt;p&gt;Hello everyone 👋&lt;br&gt;&lt;br&gt;
If you enjoyed my previous post, thank you — and if you didn’t read it, that’s totally fine. You can find it here: &lt;a href="https://dev.to/bogaboga1/odoo-core-and-the-cost-of-reinventing-everything-15n1"&gt;Odoo Core and the Cost of Reinventing Everything&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this post, I want to highlight some &lt;strong&gt;quirks and questionable architectural decisions&lt;/strong&gt; in the Odoo codebase, specifically around &lt;strong&gt;validation, data handling, and error management&lt;/strong&gt;. These issues significantly increase debugging time and cognitive load, and most of them are problems that mature web frameworks solved decades ago.&lt;/p&gt;

&lt;p&gt;As mentioned in my previous post, Odoo chose to be a &lt;strong&gt;custom-built framework that reimplements every layer of a modern web stack instead of building on an existing one, despite mature frameworks having solved these problems decades ago.&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. The Validation Layer (or Lack Thereof)
&lt;/h2&gt;

&lt;p&gt;Validation is a foundational concept in any serious web framework. In mature ecosystems, validation is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Declarative
&lt;/li&gt;
&lt;li&gt;Centralized&lt;/li&gt;
&lt;li&gt;Separated from authorization and business logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Odoo, however:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Has &lt;strong&gt;no dedicated validation layer&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Does not use any standard validation library (e.g. Pydantic, Marshmallow)&lt;/li&gt;
&lt;li&gt;Spreads validation logic across &lt;strong&gt;models, controllers, and JavaScript&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Frequently mixes &lt;strong&gt;validation, authorization, and state transitions&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is deeply nested, brittle code that is difficult to reason about.&lt;/p&gt;

&lt;p&gt;Here’s a real example from the Odoo codebase that validates whether a user can change the state of a leave request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://github.com/odoo/odoo/blob/19.0/addons/hr_holidays/models/hr_leave.py#L1361
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_check_approval_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raise_if_not_possible&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt; Check if target state is achievable. &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_superuser&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

        &lt;span class="n"&gt;is_officer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hr_holidays.group_hr_holidays_user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;holiday&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;is_time_off_manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;holiday&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;employee_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;leave_manager_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;
            &lt;span class="n"&gt;dict_all_possible_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;holiday&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_get_next_states_by_state&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;validation_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;holiday&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;validation_type&lt;/span&gt;
            &lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
            &lt;span class="c1"&gt;# Standard Check
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;holiday&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;You can&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s"&gt;t do the same action twice.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;validate1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;validation_type&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;both&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Not possible state. State Approve is only used for leave needed 2 approvals&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;holiday&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cancel&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;A cancelled leave cannot be modified.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dict_all_possible_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;holiday&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}):&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cancel&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;You can only cancel your own leave. You can cancel a leave only if this leave &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="s"&gt;is approved, validated or refused.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;confirm&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;You can&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s"&gt;t reset a leave. Cancel/delete this one and create an other&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;validate1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;is_time_off_manager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Only a Time Off Officer/Manager can approve a leave.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;You can&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s"&gt;t approve a validated leave.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;validate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;is_time_off_manager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Only a Time Off Officer/Manager can validate a leave.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;holiday&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refuse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;You can&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s"&gt;t approve this refused leave.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;You can only validate a leave with validation by Time Off Manager.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refuse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;is_time_off_manager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Only a Time Off Officer/Manager can refuse a leave.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;You can&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s"&gt;t refuse a leave with validation by Time Off Officer.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;holiday&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_access&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;write&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;UserError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;raise_if_not_possible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;UserError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
                &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;raise_if_not_possible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;UserError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validates state transitions&lt;/li&gt;
&lt;li&gt;Performs authorization checks&lt;/li&gt;
&lt;li&gt;Constructs user-facing error messages&lt;/li&gt;
&lt;li&gt;Raises HTTP-facing exceptions&lt;/li&gt;
&lt;li&gt;Depends on implicit global context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this happens in a &lt;strong&gt;single method&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In a modern framework, this logic would be split into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A state machine&lt;/li&gt;
&lt;li&gt;A validation schema&lt;/li&gt;
&lt;li&gt;A permission layer&lt;/li&gt;
&lt;li&gt;A controller-level response mapper&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because Odoo lacks these abstractions, developers are forced to manually stitch everything together — leading to massive functions like this one and endless debugging sessions.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Data Type Handling (or the Absence of Normalization)
&lt;/h2&gt;

&lt;p&gt;Data normalization is another area where mature frameworks excel. Incoming HTTP data is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parsed&lt;/li&gt;
&lt;li&gt;Typed&lt;/li&gt;
&lt;li&gt;Validated&lt;/li&gt;
&lt;li&gt;Normalized &lt;em&gt;before&lt;/em&gt; business logic runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Odoo does none of this.&lt;br&gt;
Consider a simple HTTP controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;odoo.http&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@http.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/hello/user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;csrf&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;say_hello_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;request_body&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;httprequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request_body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Boga&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my_module.say_hello&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a simple form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"enter your name"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Say Hi&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the user clicks &lt;strong&gt;“Say Hi”&lt;/strong&gt; without entering a name, the value of &lt;code&gt;name&lt;/code&gt; will be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not &lt;code&gt;False&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So now we’re forced to write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Boga&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why does this matter?&lt;/p&gt;

&lt;p&gt;Because Odoo never normalizes request data. The same field can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing (&lt;code&gt;False&lt;/code&gt;)
&lt;/li&gt;
&lt;li&gt;Present but empty (&lt;code&gt;""&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Present with a value&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three cases must be handled manually, everywhere.&lt;/p&gt;

&lt;p&gt;And yes — checking for &lt;code&gt;False&lt;/code&gt; is still necessary, because a client could send an empty POST body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/hello/user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;request_body.get("name", False)&lt;/code&gt; returns &lt;code&gt;False&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In frameworks like Django, FastAPI, or Flask + WTForms, this problem simply does not exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Error Handling: The Most Fragile Part
&lt;/h2&gt;

&lt;p&gt;Error handling in Odoo HTTP is arguably its weakest point.&lt;/p&gt;

&lt;p&gt;Errors are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Globally intercepted&lt;/li&gt;
&lt;li&gt;Tightly coupled to translation logic&lt;/li&gt;
&lt;li&gt;Often returned with HTTP 200 responses&lt;/li&gt;
&lt;li&gt;Extremely difficult to override or customize correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This leads to unpredictable behavior where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A response &lt;em&gt;looks&lt;/em&gt; successful&lt;/li&gt;
&lt;li&gt;But actually contains an error payload&lt;/li&gt;
&lt;li&gt;Or silently redirects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consider this simplified example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# my_service.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyCustomService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_balance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;send_http_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/some/other/service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;not_found&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# my_controller.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@http.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/check/balance&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;csrf&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_user_balance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;request_body&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;
            &lt;span class="n"&gt;my_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_balance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;InvalidBalance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/invalid/balance&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my_module.success&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code &lt;strong&gt;will not behave as expected&lt;/strong&gt;, because Odoo’s global HTTP error handling will intercept exceptions &lt;em&gt;before&lt;/em&gt; your controller logic can respond properly.&lt;/p&gt;

&lt;p&gt;Once again, the lack of a clean separation between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transport errors&lt;/li&gt;
&lt;li&gt;Business exceptions&lt;/li&gt;
&lt;li&gt;HTTP responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Makes even simple flows unreliable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Odoo tries to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An ORM&lt;/li&gt;
&lt;li&gt;A web framework&lt;/li&gt;
&lt;li&gt;A frontend framework&lt;/li&gt;
&lt;li&gt;A business platform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it lacks the architectural discipline required to do any of these well.&lt;/p&gt;

&lt;p&gt;The absence of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A real validation layer&lt;/li&gt;
&lt;li&gt;Request data normalization&lt;/li&gt;
&lt;li&gt;Predictable error handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Means developers spend far more time debugging &lt;em&gt;framework behavior&lt;/em&gt; than implementing business logic.&lt;/p&gt;

&lt;p&gt;None of these problems are unsolved — they were simply &lt;strong&gt;re-solved poorly&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>odoo</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>Odoo Core and the Cost of Reinventing Everything</title>
      <dc:creator>Boga</dc:creator>
      <pubDate>Mon, 12 Jan 2026 18:47:41 +0000</pubDate>
      <link>https://dev.to/bogaboga1/odoo-core-and-the-cost-of-reinventing-everything-15n1</link>
      <guid>https://dev.to/bogaboga1/odoo-core-and-the-cost-of-reinventing-everything-15n1</guid>
      <description>&lt;p&gt;Hello, this is my first blog post ever. I’d like to share my experience working with &lt;strong&gt;Odoo&lt;/strong&gt;, an open-source Enterprise Resource Planning (ERP) system, and explain why I believe many of its architectural choices cause unnecessary complexity.&lt;/p&gt;

&lt;p&gt;Odoo is a single platform that provides many prebuilt modules (mini-applications) that most companies need. For example, almost every company requires a Human Resources system to manage employee details, leaves, attendance, contracts, resignations, and more. Beyond HR, companies also need purchasing, inventory, accounting, authentication, authorization, and other systems.&lt;/p&gt;

&lt;p&gt;Odoo bundles all of these tightly coupled systems into a single installation. On paper, this sounds great — and from a business perspective, it often is. From a &lt;strong&gt;technical perspective&lt;/strong&gt;, however, things get complicated very quickly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Odoo Core Components
&lt;/h2&gt;

&lt;p&gt;Below are the main Odoo components, ranked from least complex to most complex, and all largely developed in-house instead of relying on existing mature frameworks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Odoo HTTP Layer&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;JSON-RPC&lt;/li&gt;
&lt;li&gt;Website routing&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Odoo Views&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;XML transformed into Python and JavaScript&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Odoo ORM&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Custom inheritance system&lt;/li&gt;
&lt;li&gt;Query builder&lt;/li&gt;
&lt;li&gt;Dependency injection&lt;/li&gt;
&lt;li&gt;Caching layers&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Cache System&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Implemented from scratch&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;WebSocket Implementation&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Very low-level handling&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  Odoo HTTP Layer
&lt;/h2&gt;

&lt;p&gt;Odoo is &lt;strong&gt;not built on a standard Python web framework&lt;/strong&gt; like Django or Flask. Instead, it implements its own HTTP framework on top of Werkzeug (a WSGI utility library).&lt;/p&gt;

&lt;p&gt;This HTTP layer introduces its own abstractions, request lifecycle, routing, and serialization logic, including JSON-RPC and website controllers. While technically impressive, it reinvents many problems that have already been solved — and battle-tested — by existing frameworks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Odoo Views
&lt;/h2&gt;

&lt;p&gt;In my opinion, this is one of the most problematic parts of Odoo.&lt;br&gt;
Instead of using standard frontend technologies, Odoo relies heavily on &lt;strong&gt;XML-based views&lt;/strong&gt;. These XML files are sent to the browser and then transformed using Abstract Syntax Tree (AST) analysis into JavaScript. In other contexts (like the website), the XML may be converted into Python code and sometimes back into JavaScript again.&lt;/p&gt;

&lt;p&gt;This creates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High cognitive overhead&lt;/li&gt;
&lt;li&gt;Difficult debugging&lt;/li&gt;
&lt;li&gt;Tight coupling between backend and frontend&lt;/li&gt;
&lt;li&gt;Poor tooling support compared to modern frontend stacks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It feels like building a car from raw metal just to drive from point A to point B.&lt;/p&gt;




&lt;h2&gt;
  
  
  Odoo ORM
&lt;/h2&gt;

&lt;p&gt;Odoo’s ORM is not a typical ORM.&lt;/p&gt;

&lt;p&gt;It implements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A custom inheritance system (instead of using Python’s built-in one)&lt;/li&gt;
&lt;li&gt;Its own dependency injection mechanism&lt;/li&gt;
&lt;li&gt;A query builder&lt;/li&gt;
&lt;li&gt;Caching layers (LRU)&lt;/li&gt;
&lt;li&gt;Model extension via monkey-patching&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While powerful, this system is extremely complex and hard to reason about. Debugging model behavior often feels like navigating invisible layers of magic.&lt;/p&gt;




&lt;h2&gt;
  
  
  WebSocket Implementation
&lt;/h2&gt;

&lt;p&gt;Instead of using a mature real-time framework, Odoo implements its WebSocket handling with very low-level logic, sometimes in surprisingly small and dense files.&lt;/p&gt;

&lt;p&gt;A single comment from the codebase summarizes this approach better than words ever could:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxk75oq7dikb3prto9cof.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxk75oq7dikb3prto9cof.png" alt="Odoo websocket comment" width="800" height="523"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"This diagram shows Odoo is parsing WebSocket frames manually at the bit level—implementing the RFC specification from scratch rather than using battle-tested libraries like websockets or ws4py that already handle this complexity."&lt;/p&gt;




&lt;h2&gt;
  
  
  The “Odoo Is Old” Argument
&lt;/h2&gt;

&lt;p&gt;A common defense of Odoo’s architecture is that &lt;em&gt;“it’s an old system”&lt;/em&gt; — originally developed around 2005 using Python 2.&lt;/p&gt;

&lt;p&gt;However, this argument no longer holds.&lt;/p&gt;

&lt;p&gt;Odoo was &lt;strong&gt;largely rewritten from scratch around 2017&lt;/strong&gt; to support Python 3. At that time, many excellent frameworks already existed and had solved the same problems more cleanly, while continuing to evolve without breaking their ecosystems.&lt;/p&gt;

&lt;p&gt;Today, even small changes in Odoo’s core can break custom modules unless they are limited to simple CRUD models with minimal dependencies on core behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Odoo is a powerful product and a successful business platform. But from a software engineering perspective, many of its design decisions prioritize &lt;strong&gt;control and internal consistency&lt;/strong&gt; over &lt;strong&gt;maintainability, clarity, and developer experience&lt;/strong&gt;. If you work with Odoo long enough, you stop asking &lt;em&gt;“why does it work this way?”&lt;/em&gt; and start asking &lt;em&gt;“how do I survive this upgrade?”&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>odoo</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
