Specification and rule objects
Business rules as composable objects you can and, or and not.
01The intuition
The problem it solves
A course registration program decides whether a student may join a class. The rules: has the prerequisite, is in year 11 or above, has not failed the course twice, the class is not full. The obvious code puts all of them in one can_join method. Then the sports club needs "year 11 or above and not on the injured list", which shares one rule and adds another, and the scholarship form needs three of the four plus a new one. Each place rewrites the rules, and when the year rule changes to year 10, four files change.
def can_join(student: "Student", course: "Course") -> bool:
return (course.prereq in student.passed
and student.year >= 11
and student.fails.get(course.id, 0) < 2
and len(course.members) < course.capacity)
def can_join_club(student, club):
return student.year >= 11 and student.id not in injured # rule copied
Build it one piece at a time
Step one: make each rule a small object with one question: is this candidate satisfied? Programmers call such an object a specification.
from typing import Protocol
class Spec(Protocol):
def ok(self, student: "Student") -> bool: ...
class MinYear:
def __init__(self, year: int) -> None: self.year = year
def ok(self, s: "Student") -> bool: return s.year >= self.year
class HasPassed:
def __init__(self, course_id: str) -> None: self.course_id = course_id
def ok(self, s: "Student") -> bool: return self.course_id in s.passed
class NotInjured:
def ok(self, s: "Student") -> bool: return s.id not in injured
Step two: give specifications the three words you combine rules with: and, or, not. Each combination is itself a specification, so you can keep combining.
class And:
def __init__(self, *parts: Spec) -> None: self.parts = parts
def ok(self, s) -> bool: return all(p.ok(s) for p in self.parts)
class Or:
def __init__(self, *parts: Spec) -> None: self.parts = parts
def ok(self, s) -> bool: return any(p.ok(s) for p in self.parts)
class Not:
def __init__(self, part: Spec) -> None: self.part = part
def ok(self, s) -> bool: return not self.part.ok(s)
Step three: each place in the program builds its rule from the shared parts, as data. The year rule now exists once; the club and the course both use it.
senior = MinYear(11)
physics_entry = And(senior, HasPassed("maths-10"), Not(FailedTwice("physics-11")))
club_entry = And(senior, NotInjured())
eligible = [s for s in students if physics_entry.ok(s)]
Why it works
The principle: a business rule is a value, not a line of code, so it can be named, shared, combined and tested on its own. The invariant is that a specification answers only yes or no about one candidate and has no side effects, which is what makes combining safe. Where it breaks: a rule that needs two candidates (student and course together), where the specification should take a small pair object instead; rules that hit a database inside ok and are then evaluated ten thousand times in a loop; and the temptation to make every if in the program a specification. Interviewers test by asking you to add a rule and by asking why a student was rejected, which is where a specification that can explain itself (return the failing part) earns its place.
The trade-off you must say out loud
Four rules become four classes plus three combinators, and the final rule is harder to read as a tree than as one boolean line. Use it when rules are shared across places, built at runtime from configuration, or need to be reported back to the user. The follow-up is usually about explanation and performance: short-circuit order, and returning the list of unsatisfied rules rather than a bare False.
Where it shows up in the 100 Google problems
- Coupon and discount system: eligibility and stacking rules composed with and, or and not, and the failing rule shown to the shopper.
- Course registration system: the example above, with capacity and schedule clashes as further specifications.
- Role and permission system: allow and deny rules over user, role and resource combined into one decision.
- Rule and workflow engine: conditions are specifications; actions fire when they hold.
- Meeting room reservation system: room fits capacity, is free in the interval and has the equipment, each a rule.
02Used in these problems
- Design a streaming CSV importer with validation
- Design a meeting room reservation system
- Design calendar event conflict handling
- Design a hotel booking system
- Design a student-to-room allocation system
- Design a course registration system
- Design a coffee machine
- Design an inventory management system
- Design a coupon and discount system
- Design Stack Overflow
- Design a role and permission system
- Design a rule and workflow engine