# Design Patterns in Odoo: Chain of Responsibility

> How the Chain of Responsibility pattern replaces long if/else logic with a chain of handlers, shown in odoo-addons-path, Odoo quality checks and OCA multi-tier validation.

**Source:** <https://trobz.com/insights/design-patterns-odoo-chain-of-responsibility/>

---


Imagine building a quality-inspection pipeline for a factory, where every product goes through a sequence of checks. At first, you hard-code the checks in one linear block of code. Over time, new checks are added, some need to be reordered and special cases appear. The large `if/else` block or monolithic function becomes hard to change, and adding or reordering checks becomes risky.

This is where the Chain of Responsibility pattern helps. In this post, we look at how the pattern works through three practical examples.

## Overview of the Pattern

The Chain of Responsibility (CoR) pattern lets several objects take a turn at handling a request. The sender does not need to know which handler will process it: the request travels along the chain until a handler deals with it.

From a technical point of view, the pattern:

- replaces large, brittle `if/else` blocks with small, focused handler objects;
- makes adding behavior as simple as writing a new handler and placing it in the chain;
- keeps responsibilities separate: the client triggers the request, and each handler knows how to deal with specific cases.

## How to Implement It

A typical Python implementation has:

- an abstract base handler that stores a reference to the next handler and defines the interface;
- concrete handlers that implement the actual processing;
- client code that wires the chain together and sends requests.

### Example 1: Detector Chain in odoo-addons-path

[odoo-addons-path](https://github.com/trobz/odoo-addons-path) is a command-line tool developed by Trobz that computes the final Odoo `addons_path` by detecting how a project is laid out. A naive approach would be one giant `if/else` that tries every layout. Instead, each layout has its own detector, a handler that checks whether the project matches that layout and returns the resulting paths.

The base class stores the next detector and forwards the request when a detector does not match:

```python
class CodeBaseDetector(ABC):
    _next_detector: Optional["CodeBaseDetector"] = None

    def set_next(self, detector: "CodeBaseDetector") -> "CodeBaseDetector":
        self._next_detector = detector
        return detector

    @abstractmethod
    def detect(self, codebase: Path) -> tuple[str, dict[str, Any]] | None:
        if self._next_detector:
            return self._next_detector.detect(codebase)
        return None
```

A concrete handler, here for the Trobz project layout, looks like this:

```python
class TrobzDetector(CodeBaseDetector):
    def detect(self, codebase: Path) -> tuple[str, dict[str, Any]] | None:
        if (codebase / ".trobz").is_dir():
            addons_dirs = []
            for item in (codebase / "addons").iterdir():
                if item.is_dir():
                    addons_dirs.append(item)
            return (
                "Trobz",
                {
                    "addons_dirs": addons_dirs,
                    "addons_dir": [codebase / "project"],
                    "odoo_dir": [
                        codebase / "odoo/addons",
                        codebase / "odoo/odoo/addons",
                    ],
                },
            )
        return super().detect(codebase)
```

The tool then wires the chain in order of preference:

```python
trobz = TrobzDetector()
c2c = C2CDetector()
odoo_sh = OdooShDetector()
doodba = DoodbaDetector()
fallback = GenericDetector()
trobz.set_next(c2c).set_next(odoo_sh).set_next(doodba).set_next(fallback)
res = trobz.detect(codebase)
```

Each detector either returns a result or passes the request on to the next detector, with a generic detector at the end as a fallback.

### Example 2: Quality Checks on Work Orders in Odoo MRP

In Odoo Manufacturing, producing an item can trigger one or more [quality checks on a work order](https://www.odoo.com/documentation/19.0/applications/inventory_and_mrp/quality/quality_management/quality_checks.html#quality-check-on-work-order). Quality is an Odoo Enterprise feature.

Rather than hard-coding a fixed sequence, each check is linked to the next and previous checks through the `next_check_id` and `previous_check_id` fields, so the checks form a chain stored in the database, much like a [doubly linked list](https://en.wikipedia.org/wiki/Doubly_linked_list).

At each step, a check either performs its logic and lets production continue, or passes control to the next check in the sequence. An internal `_next()`-style method performs the current check and then moves the work order on to the next linked check. A simplified sketch:

```python
def _next(self, continue_production=False):
    # perform the current check logic...
    # when finished, move to the next check in the chain:
    self.workorder_id._change_quality_check(position='next')
```

### Example 3: Multi-Tier Validation in OCA

The OCA [base_tier_validation](https://github.com/OCA/server-ux/tree/18.0/base_tier_validation) module implements multi-tier validation inspired by the Chain of Responsibility. It manages approval workflows in which a record must pass through several validation tiers before it is approved.

Each tier is a handler defined in the `tier.definition` model, with:

- a sequence number that sets the order of processing;
- reviewer assignments;
- an optional domain filter that decides whether the tier applies;
- an option to approve the tiers in sequence.

A simplified excerpt of the model:

```python
class TierDefinition(models.Model):
    _name = "tier.definition"

    # Handler properties
    sequence = fields.Integer(default=30)  # order in the chain
    definition_domain = fields.Char()  # condition for applying this handler

    # Reviewer assignment: a user, a group or a field on the record
    review_type = fields.Selection([
        ("individual", "Specific user"),
        ("group", "Any user in a specific group"),
        ("field", "Field in related record"),
    ])
    reviewer_id = fields.Many2one(comodel_name="res.users")
    reviewer_group_id = fields.Many2one(comodel_name="res.groups")
    reviewer_field_id = fields.Many2one(comodel_name="ir.model.fields")

    # Chain ordering
    approve_sequence = fields.Boolean(default=False)  # approve tiers in sequence order
```

When validation is requested, `request_validation` finds the tier definitions for the record's model and company, keeps only the tiers whose domain matches the record, creates a tier review for each of them and notifies the reviewers:

```python
def request_validation(self):
    td_obj = self.env["tier.definition"]
    tr_obj = self.env["tier.review"]
    vals_list = []
    for rec in self:
        if rec._check_state_from_condition() and rec.need_validation:
            tier_definitions = td_obj.search(
                [
                    ("model", "=", self._name),
                    ("company_id", "in", [False] + rec._get_company().ids),
                ],
                order="sequence desc",
            )
            sequence = 0
            for td in tier_definitions:
                if rec.evaluate_tier(td):
                    sequence += 1
                    vals_list.append(rec._prepare_tier_review_vals(td, sequence))
    created_trs = tr_obj.create(vals_list)
    if any(self.mapped("can_review")):
        self._update_counter({"review_created": True})
    self._notify_review_requested(created_trs)
    return created_trs
```

With `approve_sequence` enabled, each tier must be approved in order before the next one can be reviewed. Without it, several tiers can be reviewed in parallel. The [purchase_tier_validation](https://github.com/OCA/purchase-workflow/tree/18.0/purchase_tier_validation) module applies this mechanism to purchase orders.

## When to Use It

These three examples show that the pattern fits well when:

- your program must process several kinds of requests, and the exact handler is not known in advance;
- several handlers should be able to inspect or handle a request in a specific order;
- the set and order of handlers should be configurable or extensible without changing client code.

## Pros and Cons

**Pros:**

- **Open/Closed Principle**: new handlers can be added without modifying clients.
- **Single Responsibility Principle**: the code that sends requests is decoupled from the code that processes them.
- **Flexible ordering**: the order of handling is easy to control or change.

**Cons:**

- **Performance**: in the worst case, a request travels through the whole chain.
- **Harder debugging**: logic is spread across handlers, so control flow is harder to follow.
- **Unhandled requests**: if no handler processes a request, it can fail silently unless there is a fallback handler or an explicit error.

## Conclusion

Chain of Responsibility is a simple pattern that avoids monolithic branching logic and makes request processing flexible and extensible. From command-line detectors to quality checks and approval tiers stored in the Odoo database, it fits cases where the sequence of handling should be configurable, reorderable or extended over time.

