Design Principles in Odoo: A Practical Look at Dependency Injection

Trong OCA

Dependency Injection (DI) is a core software design pattern where a component receives its dependencies from an external source rather than creating them itself. If you've worked with Odoo's OWL framework, you've already seen this concept in action with its Services. This approach is preferred because it promotes loose coupling, which makes code more modular, easier to test, and reduces repetition.

In this post, we will explore how Odoo developers are leveraging this powerful pattern within the OCA rest-framework through its modern FastAPI integration.

Why is loose coupling good?

Coupling refers to the degree of interdependence between different parts of code, like modules or classes. Loose coupling (or low coupling) is the gold standard. It means components can operate independently, which brings huge advantages:

  • More Flexibility: when components aren't tightly linked, developers can modify or replace one part without causing a ripple effect that breaks the entire system. This makes it much easier to adapt and evolve the application on the fly.

  • Improved modularity: changes are localized, reducing the time and resources needed for development, testing, and maintenance. Ultimately, this lowers the overall cost and complexity of the project.

Dependency Injection in the OCA FastAPI Framework

FastAPI integration was introduced into Odoo world thanks to this PR. It brought FastAPI's modern features, including its native and elegant support for Dependency Injection to Odoo. This concept allows functions to declare the dependencies they need, and the framework takes care of providing them.

Example implementation

One of the most useful dependencies is odoo_env. It automatically provides the current Odoo environment (odoo.api.Environment) to API endpoint, which is essential for interacting with Odoo models.
For example, in order to create a CRM lead from an API call, Depends (odoo_env) is defined in the route handler as following:

@lead_router.post("/leads", status_code=201)
def create(
   data: LeadInput,
   env: Annotated[api.Environment, Depends(odoo_env)],
) -> Lead | None:
   lead = env["crm.lead"].create(data.to_crm_lead_vals())
   return Lead.from_crm_lead(lead)

How it works: This is made possible by a ContextVar variable (odoo_env_ctx) that is initialized with the correct Odoo environment at the beginning of each request.

odoo_env_ctx: ContextVar[Environment] = ContextVar("odoo_env_ctx")

Other useful dependencies, such as authentication, are clearly explained in here.

                                Related concept

                                A powerful extension of this idea is dependency overriding, introduced in FastAPI and relevant for testing or customizing behavior dynamically. This concept was brought into FastAPI with PR and clearly showcase the advantages of this pattern. With dependency_override, common logic (like authentication or data validation) are defined once and reused across multiple endpoints.
                                For example, based on field authentication method, FastAPI can decide which authentication mechanism should be used.

                                def _get_app(self):
                                    app = super()._get_app()
                                    if self.app == "demo":
                                    # Here we add the overrides to the authenticated_partner_impl method
                                    # according to the authentication method configured on the demo app
                                        if self.demo_auth_method == "http_basic":
                                            authenticated_partner_impl_override = (authenticated_partner_from_basic_auth_user)
                                        else:
                                            authenticated_partner_impl_override = (api_key_based_authenticated_partner_impl)
                                            app.dependency_overrides[authenticated_partner_impl] = (authenticated_partner_impl_override)
                                     return app