Blog
A rule engine is a software system that separates business logic from application code, allowing you to define, manage, and execute business rules without modifying your core application. Think of it as a specialized decision-making component that evaluates conditions and takes actions based on those evaluations.
The key advantage of rule engines is that they allow you to express complex business logic in a declarative way, focusing on what should happen rather than how it should be implemented. This makes your business rules more accessible, maintainable, and adaptable to changing requirements.
In traditional programming, business logic is often embedded within application code, making it difficult to modify without risking unintended consequences. A rule engine solves this problem by providing a clear separation between:
This separation enables business analysts and domain experts to understand and even modify rules without needing deep programming knowledge, while developers can focus on building robust application infrastructure.
So why else does it make sense to use a rule engine?
Centralized business logic
Rules are defined and stored in a central location, making them easier to manage, update, and reuse across different parts of your application. This centralization ensures consistency in how rules are applied throughout your systems.
Reduced complexity
By extracting business rules from your application code, you simplify both components. Your application code becomes cleaner and more focused on technical concerns, while your business rules become more explicit and easier to understand.
Making your APIs more flexible
When business requirements change (and they always do), you can update rules without modifying and redeploying your application code. This significantly reduces the time and risk associated with implementing changes.
Easier collaboration between devs and business
Rule engines bridge the gap between business and technical teams. Your business people can easily understand, review, validate, or contribute to rule definitions, while you, as a developer, can focus on the technical implementation.
Stronger oversight
With rules explicitly defined and centrally managed, it's easier to track what rules exist, when they were changed, and why. This is of particular importance for compliance and governance in regulated industries.
Rule engines are most useful in scenarios where complex decision-making logic needs to be applied consistently across many transactions or processes. Here are three real-world examples from different industries:
A telecom provider needs to route customer service calls to the appropriate department based on multiple factors.
Implementing these rules in code would create a series of if-else statements that would be difficult to maintain as routing policies evolve. With a rule engine, these policies can be defined declaratively and modified without changing the underlying call routing system.
An airport operations system needs to determine what actions to take when flights are delayed.
These rules involve multiple conditions and actions that may change seasonally or based on airport policies. Instead of keeping them in a configuration database that will be difficult to understand by business people, the rules can be kept in a human-readable form now.
A municipal government needs to determine whether building permit applications can be automatically approved or require manual review.
These rules involve combinations of conditions that would be cumbersome to implement and maintain in regular code. A rule engine allows the government to easily update approval criteria as regulations change.
Here are the same airport rules that we talked about earlier.
You'll immediately note that it's pretty much regular English all throughout. It's also easy to recognize that it sort of reminds one of the Python syntax, and that's on purpose too.
# #########################################################################################
rule
Flight_Delay_Handling_International
docs
""" Manages passenger compensation and rebooking for delayed flights.
Used for ensuring proper customer service during flight disruptions.
"""
defaults
international_voucher_threshold_hours = 3
when
flight_delay_hours > international_voucher_threshold_hours and
flight_type == 'international'
then
offer_vouchers = True
voucher_amount = 50
notify_all_passengers = True
# #########################################################################################
rule
Flight_Delay_Handling_Last_Flight_Of_Day
docs
""" Manages passenger compensation and rebooking for delayed flights.
Used for ensuring proper customer service during flight disruptions.
"""
defaults
hotel_accommodation_threshold_hours = 5
when
flight_delay_hours > hotel_accommodation_threshold_hours and
is_last_flight_of_day == True
then
offer_hotel_accommodation = True
offer_meal_vouchers = True
arrange_transportation = True
# #########################################################################################
rule
Flight_Delay_Handling_Connecting_Fligts
docs
""" Manages passenger compensation and rebooking for delayed flights.
Used for ensuring proper customer service during flight disruptions.
"""
when
affects_connecting_flight == True and
(is_baggage_checked is False or next_flight_within_hours <= 24)
then
auto_rebook_next_available = True
notify_passenger = True
update_baggage_routing = True
# #########################################################################################
It's designed for people like you, who:
You can install a VS Code plugin for syntax highlighting - go to Extensions, search for "Zato Rules" and click "Install".
/opt/zato/env/qs-1/server1/config/repo/user-conf
directory and you can map it to your host (e.g. your WSL2 Ubuntu server under Windows) - the best way to automate it is use the blueprint project.zrules
, e.g. telco_bss.zrules, gov.zrules or airport.zrulesLet's create a simple rule file for our flight delay scenario. Create or modify the file named airport.zrules
so it reads as below:
# #########################################################################################
rule
01_Flight_Delays
docs
""" Determines what actions to take when flights are delayed based on delay duration
and passenger status. Used by the flight management system to automate
passenger notifications and service offerings.
"""
defaults
flight_delay = 0
passenger_type = 'regular'
is_international = False
when
flight_delay > 120 and
passenger_type == 'diamond' and
is_international == True
then
send_notification = True
offer_accomodation = True
template = 'DelayAlert'
# #########################################################################################
rule
02_Flight_Cancellations
docs
""" Determines what actions to take when flights are cancelled.
Provides different handling based on passenger status and flight type.
"""
when
is_cancelled == True and
(passenger_type == 'diamond' or passenger_type == 'platinum')
then
send_notification = True
offer_rebooking = True
offer_accomodation = True
template = 'CancellationVIP'
# #########################################################################################
The primary interface for using rules is through the self.rules.match
method of your Zato services, which allows you to match data against defined rules and take actions based on the results.
Here's a simple example of using the rule engine in a service:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class FlightDelayHandler(Service):
def handle(self) -> 'None':
# Input data about the flight delay situation
data = {
'flight_delay': 150, # Delay in minutes
'passenger_type': 'diamond', # Frequent flyer status
'is_international': True, # International flight flag
}
# Evaluate a specific rule
if result := self.rules.airport_01_Flight_Delays.match(data):
# Access the 'then' values to determine actions
send_notification = result.then['send_notification']
offer_accomodation = result.then['offer_accomodation']
template = result.then['template']
self.logger.info('Sending notification: {send_notification}')
self.logger.info('Offering accommodation: {offer_accomodation}')
self.logger.info('Template to use: {template}')
This service checks if the current flight delay situation matches the conditions defined in the airport_01_Flight_Delays
rule. If there's a match, it accesses the rule's then
values to determine what actions to take.
Each rule in a .zrules
file follows a specific structure with several sections:
The rule
section defines the name of the rule. This should be a unique identifier within the container (derived from the filename).
All rules are always evaluated alphabetically so if you'd like to be specific about what has priority, you can use a prefix number for ordering (e.g., 01_
, 02_
) followed by a descriptive name.
Note that in your Python code you refer to rules using their full name, which is a concatenation of the .zrules file name plus the actual rule name. If the rule above is in an airport.zrules name then its full name is airport_01_Flight_Delays.
The docs
section provides human-readable documentation about the rule's purpose and usage. This is important for maintainability, especially when rules are managed by multiple people.
docs
""" Determines what actions to take when flights are delayed based on delay duration
and passenger status. Used by the flight management system to automate passenger
notifications and service offerings.
"""
The defaults
section defines default values for variables that might not be present in the input data. This makes rules more robust by handling missing data gracefully.
The when
section defines the conditions that must be met for the rule to match. This is where you specify the business logic using comparison operators, logical operators, and variable references.
The then
section defines the actions or values that should be returned when the rule matches. These can be simple values or complex objects, such as dicts or lists.
then
send_notification = True
offer_accomodation = True
template = 'DelayAlert'
status = {'status_type':'alert', 'status_subtype':'delay'}
notify_list = [123, 456, 789]
Zato's rule engine supports a variety of operators and expressions for creating sophisticated rule conditions. Here are some advanced techniques:
You can combine multiple conditions using logical operators:
and
: Both conditions must be trueor
: At least one condition must be truenot
: Negates a conditionThe rule engine supports all standard comparison operators:
==
: Equal to!=
: Not equal to>
, >=
: Greater than, greater than or equal to<
, <=
: Less than, less than or equal toYou can check if a value is in a list using the in
operator:
For pattern matching, you can use the =~
operator with regular expressions:
This matches flight numbers that consist of two uppercase letters followed by 3-4 digits.
Remember that the rule engine is a built-in capability of Zato, and you access your rules via Python services.
It means that you can continue to use your services to access any kind of APIs, read or update data in databases and so on, while your rules make decisions only.
It's by design that the syntax of rules is kept very simple and it's used to express decision logic only. Any kind of interactions with the outside world is done with your Zato services in Python, which gives you a very clean separation between the decision making and acting on the decisions made.
Zato provides several ways to use rules in your services, depending on your specific needs. Let's explore the different patterns:
The most straightforward approach is to match data against a specific rule:
class TelecomCustomerService(Service):
def handle(self) -> 'None':
# Customer data
data = {
'customer_tier': 'premium',
'open_tickets': 2,
'account_age_days': 365,
}
# Match against a specific rule
if self.rules.telecom_01_Premium_Support.match(data):
# Rule matched, take appropriate action
self.logger.info('Routing customer to premium support')
When you need to use the values defined in the rule's then
section, capture the result object:
class AirportGateAssignment(Service):
def handle(self) -> 'None':
# Aircraft data
data = {
'aircraft_type': 'B787',
'gate_width_meters': 50,
'jetbridge_length_meters': 15,
'gate_currently_available': True,
}
# Match and capture the result
if result := self.rules.airport_09_GateAssignment_WidebodyAircraft.match(data):
# Access the 'then' values
assigned_gate = result.then['assigned_gate']
priority_boarding = result.then['priority_boarding']
self.logger.info('Gate {assigned_gate} with priority boarding: {priority_boarding}')
You can match data against all rules in a file and use the first matching rule:
class ResponseCoordinator(Service):
def handle(self) -> 'None':
# Incident data
data = {
'severity': 9,
'unit_affected': 'ABC',
}
# Match against all rules in the 'emergency.zrules' file
if result := self.rules.emergency.match(data):
# The first matching rule's results are returned
priority_level = result.then['priority_level']
response_units = result.then['response_units']
self.logger.info('Priority: {priority_level}, dispatching: {response_units}')
You can also match against a specific set of rules:
class TelecomBillingService(Service):
def handle(self) -> 'None':
# Customer billing data
data = {
'account_type': 'business',
'usage_gb': 250,
'contract_months': 24,
}
# Rules to check
rules = [
'telecom_03_Business_Discount',
'telecom_05_Volume_Discount',
'telecom_08_Loyalty_Program'
]
# Match against the specified rules
if result := self.rules.match(data, rules=rules):
discount_percentage = result.then['discount_percentage']
self.logger.info('Applied discount: {discount_percentage}%')
Sometimes you may need to determine which rule to use at runtime:
class PermitProcessor(Service):
def handle(self) -> 'None':
# Permit application data
data = {
'zoning_code': 'residential',
'proposed_height_feet': 30,
'proposed_setback_feet': 20,
}
# Determine which rule to use based on our input
permit_type = self.request.payload.permit_type
permit_type = permit_type.capitalize()
# Build a full name of the rule
rule_name = f'government_07_PermitApproval_{permit_type}'
# Match against the dynamically selected rule
if result := self.rules[rule_name].match(data):
permit_status = result.then['permit_status']
self.logger.info('Permit status: {permit_status}')
When you match data against rules, there are several patterns for handling the results, depending on your business requirements.
The simplest approach is to use the match result as a boolean condition:
if self.rules.telecom_01_Premium_Support.match(data):
# Rule matched, take appropriate action
self.route_to_premium_support()
When you need to use the values defined in the rule's then
section, capture the result object:
if result := self.rules.airport_01_Flight_Delays.match(data):
# Access the 'then' values
send_notification = result.then['send_notification']
offer_accommodation = result.then['offer_accomodation']
template = result.then['template']
# Use these values to drive your business logic
if send_notification:
self.send_customer_notification(template)
if offer_accommodation:
self.arrange_hotel_accommodation()
It's important to handle cases where no rules match:
# A rule matched, use its results
if result := self.rules.government.match(data):
self.process_according_to_rule(result)
# No rules matched, use default handling
else:
self.apply_default_processing()
Sometimes you need to apply multiple rules in sequence, with each rule potentially modifying the data:
class ComplexRuleProcessor(Service):
def handle(self) -> 'None':
# Initial data
data = {
'customer_id': '12345',
'transaction_amount': 500,
}
# Apply eligibility rules
if eligibility_result := self.rules.eligibility.match(data):
# Update data based on eligibility result
data['eligible_for_discount'] = eligibility_result.then['is_eligible']
# Now apply pricing rules with the updated data
if pricing_result := self.rules.pricing.match(data):
final_amount = pricing_result.then['final_amount']
self.logger.info('Final transaction amount: {final_amount}')
As your rule-based system grows, following these best practices will help maintain clarity and efficiency:
Group related rules in the same file: Rules that belong to the same business domain should be kept together.
Use consistent naming conventions: Prefix rule names with numbers (e.g., 01_
, 02_
) to control evaluation order within a container.
Create separate containers for different domains: For example, keep telecom_rules.zrules
, airport_rules.zrules
, and government_rules.zrules
as separate files.
Always include detailed docs: Each rule should have clear documentation explaining its purpose, when it applies, and what actions it triggers.
Document the business context: Include information about why the rule exists and who is responsible for its business logic.
Update documentation when rules change: Keep the documentation synchronized with the actual rule logic.
Keep rules focused - each rule should address a specific business scenario rather than trying to handle multiple cases.
Use defaults for robustness - define default values for variables that might be missing from input data.
Order conditions by complexity - put simpler, more frequently false conditions first to take advantage of short-circuit evaluation, and take advantage of other performance tuning opportunities
Create test cases for each rule - ensure that each rule works correctly with various input data. You'll be invoking your rules from Python services so you can use the Zato's API testing in pure English framework for that.
Test rule interactions - verify that rules work correctly when applied in sequence or combination.
Validate rule changes - when modifying rules, test both the new behavior and ensure existing scenarios still work correctly.
Version control your rule files - keep rule files in a version control system alongside your application code (e.g. in git).
Deploy rules with care - treat rule changes like code changes, with proper review and testing before deployment.
The Zato Rule Engine provides a clean way to implement business rules in your integration services in a manner that is easy to understand by both technical and business people.
By separating business logic from application code, you gain flexibility, maintainability, and clarity in your systems, so you can make them more adaptable to changing business requirements while keeping your code clean and focused on technical concerns.