Imagine you’re building a quality-inspection pipeline for a factory. A product must go through a sequence of checks. At first, you hard-code those checks in a single linear block of code. Over time new checks are added, some must be reordered, and special cases appear. The single large if/else or monolithic function becomes hard to change, and adding or reordering checks turns into a risky operation.
This is where the “Chain of Responsibility” pattern shines. In this blog post, let’s take a look at how the pattern is used through two practical examples.
Overview of the Pattern
The Chain of Responsibility (CoR) pattern solves this by letting multiple objects get a chance to handle a request. The sender of the request doesn’t need to know which handler will process it. The request simply travels along the chain until some handler deals with it.
So from technical perspectives, it helps:
Replaces large, brittle if/else blocks with small, focused handler objects.
Makes adding new behavior as simple as adding a new handler and placing it in the chain.
Keeps responsibilities separated: the client triggers the request and handlers know how to deal with specific cases.
How to Implement
A typical implementation in Python has:
An abstract base Handler that stores a reference to the next handler and defines the interface.
Concrete handlers that implement the actual processing logic.
Client code that wires the chain and sends requests.
Example 1: odoo-addons-path (detector chain)
A real-world example is odoo-addons-path (a CLI tool developed by Trobz) which computes the final Odoo addons_path by detecting different project layouts. A naive approach would be a giant if/else that tries every layout. Instead, each layout becomes a detector (a handler) that checks whether the provided project matches that layout and returns the resulting paths.
A concrete handle looks like:
class TrobzDetector(CodeBaseDetector):
def detect(self, codebase: Path) -> Optional[tuple[str, dict[str, Any]]]:
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)
Then in main application, the wiring is followed:
trobz = TrobzDetector()
# other handlers if available
# another_detector = AnotherDetector()
# Chain them in order of preference
trobz.set_next(another_detector)
# return result
result = trobz.detect(codebase_path)
Each detector either returns a result or forwards the detection request to the next detector in the chain.
Example 2: Odoo Quality Checks (MRP work orders)
In Odoo MRP, producing an item can trigger one or more quality checks on a work order.
Note that it is an enterprise feature
Rather than hard-coding a fixed sequence, each check is linked to the next (and previous) check, forming a chain of checks stored in the database (via next_check_id / previous_check_id field), like a doubly linked list.
A check can decide to:
Perform its logic and proceed production, or
Pass control to the next check in the sequence.
There’s an internal _next()-like mechanism which performs the current check and then advances the workorder to the next linked check.
For example:
def _next(self, continue_production=False):
#perform current check logic...
# if finished, move to the next check in the chain:
self.workorder_id._change_quality_check(position='next')
Example 3: Multi-tier validation
The OCA base_tier_validation module implements a multi-tier validation pattern inspired by CoR. It managed approval workflows where a record must pass through multiple validation tiers in sequence before being approved.
Each tier is a “handler” defined in the tier.definition model with:
A sequence number defining the order of processing.
Reviewer assignments.
An optional domain filter to determine if the tier applies.
Optional approval ordering.
class TierDefinition(models.Model):
_name = "tier.definition"
_description = "Tier Definition"
# Handler properties
sequence = fields.Integer(default=30) # Order in chain
definition_domain = fields.Char() # Condition to apply this handler
# Reviewer assignments (polymorphic - individual, group, or field-based)
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) # True = sequential, False = parallel
When validation is requested via request_validation:
Creates tier reviews for all matching tier definitions.
Moves reviews through states.
With approve_sequence=True, each tier must be approved in order before the next becomes reviewable.
With approve_sequence=False, multiple tiers can be reviewed in parallel
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:
# Step 1: Find all tier definitions (handlers) for this model
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:
# Step 2: Evaluate domain filter - does this handler apply?
if rec.evaluate_tier(td):
sequence += 1
# Step 3: Create tier review
vals_list.append(rec._prepare_tier_review_vals(td, sequence))
# Step 4: Instantiate handlers
created_trs = tr_obj.create(vals_list)
self._notify_review_requested(created_trs)
return created_trs
Module purchase_tier_validation provides a concrete implementation of this concept.
From three above examples, we can see that the CoR pattern fits well when
Your program must process requests that could be of several different types, and the exact handler is not known at compile time.
You want to allow multiple handlers to inspect or attempt to handle a request in a specific order.
You want to make the set and order of handlers configurable or extensible without changing client code.
Pros and Cons
Pros:
Respects the Open/Closed Principle: add new handlers without modifying clients.
Respects Single Responsibility Principle: callers and processors are decoupled.
Provides an easy way to control or change the order of handling.
Cons:
In the worst case, a request may traverse the whole chain, which can be less efficient.
Debugging control flow can be harder because logic is decentralized across handlers.
If no handler processes a request, requests may silently fail unless a fallback handler or explicit error is provided.
Conclusion
Chain of Responsibility is a simple but powerful pattern to avoid monolithic branching logic and make request processing flexible and extensible. From CLI detectors to database-driven quality checks in Odoo, CoR maps cleanly to cases where the sequence of handling should be configurable, reorderable, or extended over time.