How Registry Works Architecture & Implementation Guide
What is the Registry?
The Odoo Registry (@web/core/registry) is a central place where frontend modules "announce" what they provide like components, services, views, or field widgets.
Instead of modules directly depending on each other, they simply register themselves in the Registry. Other parts of Odoo can then find and use them when needed.
How the Registry Works
Register Features
Services, views, components, field widgets, and actions register themselves in the Registry using unique keys.
Runtime Lookup
Whenever Odoo needs a feature, it queries the Registry to retrieve the registered implementation.
Load Implementation
The correct implementation is returned without modules knowing where it was defined.
Why It Matters
Modular
Features remain independent, making the frontend easier to organize and maintain.
Flexible
Existing implementations can be replaced or extended without changing dependent modules.
Extensible
New modules can plug into the system without modifying existing frontend code.
The Registry Stores References, Not Business Logic
The Registry acts as a central lookup table. It never executes business logic, validates data, or performs application tasks. Its sole responsibility is to maintain references between keys and their registered implementations, allowing modules to remain independent while enabling dynamic extension and replacement.
Three Core Properties
2.1 Flexible Coordination
The Odoo Registry removes hard JavaScript dependencies between modules. Instead of importing components directly, applications simply request the required implementation from the registry, enabling a clean, loosely coupled plugin architecture.
2.2 Reactive Store
Every registry extends EventBus. Whenever a new item is registered, an UPDATE event is emitted, allowing subscribed Owl components to react automatically without requiring a page reload.
2.3 Validated Contracts
Categories using addValidation() verify every registry entry against the expected structure. Invalid registrations fail immediately, helping developers detect integration issues before they reach the user interface.
addValidation() is not available for every registry category. Only categories that define a validation schema enforce structural checks and throw an error when invalid data is registered. Categories without validation accept any object, making it the developer's responsibility to register entries that follow the expected contract.
3. Runtime Lifecycle
The Registry transitions through four phases from static definitions to a live runtime environment:
| Phase | Name | What Happens | Registry Role |
|---|---|---|---|
| I | Static Registration | ES modules execute their top-level code as the asset bundle loads | Modules call registry.category(...).add(...) to publish items |
| II | Contract Enforcement | Categories with addValidation() verify the shape of registered items | Malformed registrations throw immediately at this point |
| III | Service Graph Resolution | startServices(env) resolves the dependency graph and boots services | Service registry is read; dependency order is computed |
| IV | Dynamic Discovery | OWL components render and look up implementations as needed | Components call registry.get() to find widgets, views, actions at runtime |
Static registration (Phase I) completes before any OWL component is mounted. As a result, all statically imported registry entries are already available when the UI starts rendering. Additional entries from plugins or lazy-loaded modules can be registered later, automatically notifying subscribed components through the EventBus UPDATE event and triggering the required UI updates.
4. Registry Categories
Registries are organized into named categories. Each category is an independent store for a specific type of item. The global registry object is the entry point: registry.category(name) returns the category store for that name, creating it if it does not yet exist.
| Category Name | Registered Items | Primary Consumer |
|---|---|---|
fields |
Field widget components (CharField, IntegerField, etc.) | Form view and List view renderers |
views |
View implementations (Form, List, Kanban, etc.) | ActionManager and WebClient |
services |
Service factories (Action, Notification, RPC, etc.) | startServices() and useService() |
actions |
Client action handlers | ActionManager |
systray |
Top navigation bar icon components | NavBar and WebClient |
main_components |
Root-level mounted Owl components | WebClient mounting loop |
5. Item Resolution Model
Direct Key Lookup
The simplest and fastest lookup mechanism is a direct key lookup.
Calling registry.category(...).get(...) retrieves the
component registered under the specified key. Since registry entries
are internally stored as key-value mappings, this lookup executes in
constant time (O(1)), making it extremely efficient
even when a category contains many registered items.
O(1) Performance
Registry lookups use direct key access, allowing extremely fast retrieval regardless of registry size.
Exact Match
The Registry returns the component mapped to the requested key. If no matching entry exists, an error is raised.
Used Everywhere
This lookup pattern powers field widgets, services, actions, views, menus, and most Registry-based extensions.
Scoped / Priority Lookup
While direct key lookups are sufficient in many cases, field widgets require
a more intelligent resolution strategy. Odoo's
getFieldFromRegistry() searches multiple registry keys in a
predefined priority order, always selecting the most specific widget
before falling back to more generic implementations.
Highest Priority
Odoo first searches for a widget registered specifically for the current view and widget combination, ensuring the most context-aware implementation is selected.
Automatic Fallback
If no view-specific widget exists, the Registry automatically falls back to a generic widget before finally using the default field type implementation.
Flexible Customization
Developers can override widgets for a single view without affecting every other view, making customizations cleaner and easier to maintain.
| Priority | Registry Key | Resolution Behaviour |
|---|---|---|
| 1 |
viewType.widgetName
|
Searches for a widget registered specifically for the current view type and widget combination. |
| 2 |
widgetName
|
Falls back to a generic widget that works across every view. |
| 3 |
fieldType
|
Uses the default widget associated with the field type such
as char, integer, or
boolean.
|
This layered lookup strategy enables highly targeted customizations. You can override a widget for a specific view without replacing the global implementation, allowing custom modules to coexist while preserving Odoo's default behavior.
Sequence-Based Ordering
Some registry categories contain multiple items that must appear in a
predictable order. Instead of relying on registration order, Odoo uses
a sequence value to determine the final ordering. Items
with lower sequence numbers are processed before items with higher
values.
Ordered Execution
Registry entries are returned in ascending sequence order, ensuring deterministic rendering and execution.
UI Positioning
Categories such as systray use sequence values to determine the visual order of components.
Easy Customization
Developers can reposition components simply by changing their sequence value without modifying existing code.
| Sequence | Position | Result |
|---|---|---|
| 10 | First | Rendered before all default items. |
| 50 | Default | Standard ordering used by most registry entries. |
| 100 | Last | Appears after entries with lower sequence values. |
Avoid using the same sequence value for multiple custom components whenever possible. Meaningful spacing (10, 20, 30...) makes future insertions easier without renumbering existing entries.
Conflict Resolution
Every registry key must be unique. If a module attempts to register an
item using a key that already exists, Odoo immediately raises a
DuplicatedKeyError. This prevents accidental overrides and
keeps registry contents predictable. When replacing an existing entry is
intentional, developers can pass the { force: true } option.
Unique Keys
Every registry entry must use a unique key. Duplicate registrations are rejected immediately to prevent conflicts.
Force Replacement
Passing { force: true } intentionally replaces
an existing registry entry instead of throwing an exception.
Extension First
Whenever possible, extend existing functionality using
patch() instead of replacing the entire
registry entry.
| Action | Result | Recommended? |
|---|---|---|
| Normal Registration | Creates a new registry entry. | ✅ Yes |
| Duplicate Key | Throws DuplicatedKeyError. |
❌ No |
| { force: true } | Replaces the existing registry item. | ⚠ Only when required |
| patch() | Extends existing behavior without replacing it. | ⭐ Best Practice |
Avoid overusing { force: true }. When multiple modules replace the same registry key, only the last registration survives. If your goal is to extend existing behavior, prefer patch() because it preserves compatibility with other custom modules and future Odoo upgrades.
6. Service Orchestration
Services registered in the "services" category are not simple objects they are factory functions that the Service Launcher resolves, orders, and instantiates during startup.
Lazy Initialization
Services start only when their declared dependencies are ready.
The launcher does not simply iterate the registry in registration
order—it builds a dependency graph and resolves it. A service
that depends on rpc will not start until
rpc has finished its start() call.
Parallel Boot
Services with no mutual dependencies can boot concurrently. The launcher only enforces dependency relationships, allowing independent services to initialize in parallel and reduce application startup time.
Async Readiness
Service start() functions can return a
Promise. The launcher waits for that promise to
resolve before starting dependent services, ensuring asynchronous
initialization completes before the service becomes available.
Circular Dependency Detection
If two services declare each other as dependencies, the Service Launcher detects the cycle during startup and throws an error. Circular dependencies are intentionally blocked and must be resolved by redesigning the dependency graph.
| Mechanism | Runtime Behavior | Developer Consideration |
|---|---|---|
| Lazy Initialization | Services start only after every declared dependency is fully initialized. |
Declare all required services in the
dependencies array.
|
| Parallel Boot | Independent services initialize simultaneously whenever possible. | Never rely on the startup order of unrelated services. |
| Async Readiness |
The launcher waits for the start() Promise before starting dependent services.
|
Return a Promise whenever initialization performs asynchronous work.
|
| Circular Dependency Check | Cyclic dependencies are detected during startup and immediately raise an error. | Break dependency cycles by redesigning service relationships. |
7. OWL Integration
useRegistry Hook
Components that need to react to registry changes use the
useRegistry() hook. It subscribes the component to the
UPDATE event of the specified registry category.
Whenever new items are registered, the component automatically
re-renders to reflect the latest Registry state.
import { useRegistry } from "@web/core/registry";
setup() {
super.setup();
const fieldRegistry = useRegistry(
registry.category("fields")
);
}
Event throttling is built in. Multiple registrations occurring within the same JavaScript tick trigger only a single re-render, preventing unnecessary rendering work during bulk registrations.
useService Hook
The useService() hook provides access to running
service singletons through the OWL environment. It returns a
reactive proxy, allowing components to automatically update when
the service's reactive state changes.
import { useService } from "@web/core/utils/hooks";
setup() {
super.setup();
this.notification = useService("notification");
this.actionService = useService("action");
}
IMPORTANT
useService() can only be called during
setup(). It accesses env.services,
which becomes available only after
startServices() completes. Calling the hook before
services are initialized—or outside setup()—throws
an error.
8. Registry vs patch() — When to Use Which
The most common architectural decision in Odoo module development is choosing between extending via the Registry or via patch(). They solve different problems.
| Area | Registry | patch() | Recommended Usage |
|---|---|---|---|
| Primary Goal | Register or replace a named item within a registry category. | Extend or modify the behavior of an existing class or object. | Use Registry for new implementations and patch() for behavior changes. |
| Modification Scope | Replaces the entire registered item. | Overrides individual methods without replacing the whole object. | Choose patch() for targeted modifications; use Registry only for full replacements. |
| Original Logic | Not applicable. |
Supports super to invoke the original implementation.
|
If the original behavior must be preserved, prefer patch(). |
| Debugging | Easy to inspect through Registry categories and keys. | Requires inspecting the prototype chain and patched methods. | Registry-based extensions are generally easier to trace and maintain. |
| Maintenance Risk | Low — explicit, key-based registration. | Moderate — depends on prototype structure and implementation details. | Prefer Registry whenever an extension point already exists. |
Preferred Extensibility Order
Always follow this order when deciding how to extend Odoo:
Registry
Add or replace a named item (view, field widget, service, action).
Hooks (useX)
Encapsulate reusable stateful logic.
Services
Provide cross-component logic through env.services.
patch()
Last resort for modifying existing behavior when no Registry extension point exists.
9. Anti-patterns & Constraints
Don't Over-use { force: true }
Using { force: true } is a silent win-by-last-write. If two modules both force the same key, the second silently erases the first. There is no warning, no merge, and no super chain. For reusable modules, prefer patch() whenever possible.
No Circular Service Dependencies
The Service Launcher builds a directed dependency graph. Circular dependencies are detected during startup and immediately throw an error. Resolve them by refactoring services or introducing an intermediate communication layer.
Don't Register in setup()
Registry entries should be registered exactly once during module loading. Registering items inside setup() creates duplicate registrations whenever a component is mounted.
Validate Your Schema
When creating a Registry category that accepts third-party contributions, define an addValidation() schema. Invalid registrations are then rejected immediately instead of causing runtime failures.
10. Version Evolution
| Capability | Odoo 16 / 17 | Odoo 18 / 19 |
|---|---|---|
| Registry Architecture |
Basic object store with add() and get().
|
EventBus-powered reactive Registry with automatic update notifications. |
| Validation | Limited or no built-in schema validation. |
Strong registration validation through addValidation().
|
| Reactivity | Manual subscriptions and update handling. |
Reactive updates via useRegistry() and useService().
|
| Service Startup | Sequential startup with basic dependency checks. | Dependency graph resolution, parallel boot, async readiness, and circular dependency detection. |
| Module System |
Legacy odoo.define() modules.
|
Native ES Modules using @odoo-module.
|
11. Debugging the Registry
Global Inspection
In a browser console on any Odoo page, the running service instances are accessible at:
odoo.__WOWL_DEBUG__.root.env.services
// Returns an object with all running service instances keyed by name
To inspect a specific Registry category:
odoo.__WOWL_DEBUG__.root.env.services.registry.category("fields").getAll()
// Returns all registered field widget entries
// including their sequence and value.
Service Startup Tracepoint
To debug when and in what order a service starts, place a
debugger; statement inside its
start() function. The browser call stack reveals the
Service Launcher's dependency resolution path, making it easy to
identify which dependency triggered the service startup.
const myService = {
dependencies: ["rpc", "notification"],
start(env, { rpc, notification }) {
debugger; // Inspect the call stack here
return {
// Service API
};
}
};
Service Startup Tracepoint
To understand when and why a service starts,
place a debugger; statement inside its
start() method. When execution pauses, inspect the
browser call stack to identify which dependency triggered the service
and the order in which the Service Launcher resolved it.
Diagnosing DuplicatedKeyError
| Symptom | Likely Cause | Recommended Fix |
|---|---|---|
| DuplicatedKeyError during module load | Two modules register the same key within one registry category. | Rename the key or intentionally replace it using { force: true }. |
{ force: true } entry disappears |
Another module force-registers the same key later. | Search all .add() calls and verify asset loading order. |
useService() reports "not found" |
Service failed to register or never completed startup. | Verify service name and inspect dependency startup failures. |
| UPDATE event without UI refresh | Component isn't subscribed through useRegistry(). |
Replace manual listeners with the reactive useRegistry() hook. |
Asset Debug Mode
Append ?debug=assets to the Odoo URL to load individual,
unminified JavaScript files instead of bundled assets. This makes it
easy to inspect source files, trace Registry registrations, locate
.add() calls, and understand the exact asset loading order.