Ice
A lightweight, zero-dependency rule engine with visual tree-based orchestration
Documentation ·
Live Demo ·
Getting Started ·
Changelog
中文
What is Ice?
Ice is a rule engine that takes a fundamentally different approach: rules are organized as trees, not chains or tables. Each node in the tree handles its own logic independently — modifying one node never creates cascading effects on others. Nodes communicate exclusively through a shared, thread-safe data context called Roam, rather than referencing each other directly.
The result is a system where business rules can be visually configured, hot-reloaded in seconds, and executed in-memory with sub-millisecond latency — all without requiring any database or middleware.
Key Features
| Feature |
Description |
| Tree-Based Orchestration |
Rules organized as trees — nodes are independent, changes to one node never cascade to others |
| Visual Web Editor |
Configure rules through an intuitive web UI with a tree editor, no DSL to learn |
| Zero Dependencies |
No database, no message queue, no service registry — just files on disk |
| Multi-Language SDKs |
Java, Go, and Python with full feature parity |
| Hot Reload |
Changes take effect in seconds via automatic version polling — no restart needed |
| High Performance |
Pure in-memory execution with sub-millisecond latency, zero network overhead |
| Node Reuse |
Same node can be shared across multiple rule trees |
| Parallel Execution |
Built-in parallel relation nodes for concurrent child execution |
| Mock Debugging |
Trigger rule execution remotely from the Web UI for real-time debugging |
| Lane / Traffic Isolation |
Branch-based rule isolation for A/B testing, canary releases, and gradual rollouts |
How It Works
Rule Trees
Every rule in Ice is a tree composed of two types of nodes:
- Relation Nodes — control the execution flow (similar to logical operators)
- Leaf Nodes — contain your actual business logic
Relation Nodes (Control Flow)
| Type |
Behavior |
Short-Circuit |
| AND |
All children must return true |
Yes |
| ANY |
At least one child returns true |
Yes |
| ALL |
All children must return true |
No |
| NONE |
No child returns true |
No |
| TRUE |
Always returns true (execute all children for side effects) |
No |
Each type has a parallel variant (P_AND, P_ANY, P_ALL, P_NONE, P_TRUE) that executes children concurrently.
Leaf Nodes (Business Logic)
| Type |
Return |
Purpose |
| Flow |
boolean |
Conditional checks — “Is the user eligible?” |
| Result |
boolean |
Business operations — “Issue the coupon” |
| None |
void |
Side effects — queries, logging, metrics, notifications |
Roam (Data Context)
Roam is the thread-safe data container that flows through the entire rule tree. Nodes read input from and write output to Roam — they never reference each other directly. This is the key to node isolation.
Roam
├── put("uid", 12345) // flat key-value
├── putDeep("user.level", "gold") // nested key
├── get("uid") // → 12345
└── getDeep("user.level") // → "gold"
- Thread-safe: Java
ConcurrentHashMap / Go sync.RWMutex / Python threading.RLock
- Deep keys:
putDeep("a.b.c", value) auto-creates nested structures
- Dynamic references: prefix a value with
@ to resolve it from Roam at runtime (e.g., @uid reads roam.get("uid"))
Architecture
┌──────────────────────────────────────────────────────────────┐
│ Shared Storage (ice-data/) │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌──────────┐ │
│ │ apps/ │ │ bases/ │ │ confs/ │ │ versions/│ │
│ └────────┘ └────────┘ └────────┘ └──────────┘ │
└──────────────────────────────────────────────────────────────┘
▲ ▲
│ Write │ Read (Poll)
│ │
┌─────────┴─────────┐ ┌───────────┴───────────┐
│ Ice Server │ │ Ice Client │
│ │ │ │
│ • Web UI │ │ • Version polling │
│ • Tree editor │ │ • Hot reload │
│ • Apply & publish │ │ • In-memory exec │
│ • Mock debugging │ │ • Mock execution │
│ │ │ • Fault tolerant │
└────────────────────┘ └───────────────────────┘
Key design decisions:
- Server and Client are fully decoupled — if the Server goes down, Clients continue executing rules from their in-memory cache
- File-based communication — no network protocol between Server and Client, just a shared directory (
ice-data/)
- Incremental updates — Client polls
versions/ for changes and applies only the delta; falls back to full reload if incremental files are missing
Deployment Options
| Mode |
Description |
| Single Machine |
Server + Client on the same host, ice-data/ is a local directory |
| Docker Compose |
Containerized deployment with volume mounts |
| Distributed |
Multiple Servers/Clients sharing storage via NFS, AWS EFS, or GCP Filestore |
Quick Start
1. Deploy the Server
docker run -d --name ice-server -p 8121:8121 \
-v ./ice-data:/app/ice-data waitmoon/ice-server:latest
Open http://localhost:8121 to access the Web UI.
2. Install the Client SDK
Java
<dependency>
<groupId>com.waitmoon.ice</groupId>
<artifactId>ice-core</artifactId>
<version>4.0.9</version>
</dependency>
Go
go get github.com/zjn-zjn/ice/sdks/go
Python
pip install ice-rules
3. Define Your Leaf Nodes
Leaf nodes are where your business logic lives. Here’s a simple ScoreFlow that checks if a value meets a threshold:
Java
@Data
@EqualsAndHashCode(callSuper = true)
public class ScoreFlow extends BaseLeafFlow {
private double score;
private String key;
@Override
protected boolean doFlow(IceRoam roam) {
Number value = roam.getDeep(key);
if (value == null) {
return false;
}
return value.doubleValue() >= score;
}
}
Go
type ScoreFlow struct {
Score float64 `json:"score" ice:"name:Score Threshold,desc:Minimum score to pass"`
Key string `json:"key" ice:"name:Roam Key,desc:Key to read from Roam"`
}
func (s *ScoreFlow) DoFlow(ctx context.Context, roam *icecontext.Roam) bool {
value := roam.ValueDeep(s.Key).Float64Or(0)
return value >= s.Score
}
Register it:
ice.RegisterLeaf("com.example.ScoreFlow",
&ice.LeafMeta{Name: "Score Check", Desc: "Check if score meets threshold"},
func() any { return &ScoreFlow{} })
Python
@ice.leaf("com.example.ScoreFlow", name="Score Check", desc="Check if score meets threshold")
class ScoreFlow:
score: Annotated[float, IceField(name="Score Threshold")] = 0.0
key: Annotated[str, IceField(name="Roam Key")] = ""
def do_flow(self, roam: Roam) -> bool:
value = roam.get_deep(self.key)
if value is None:
return False
return float(value) >= self.score
4. Initialize the Client and Execute
Java
// Initialize
IceFileClient client = new IceFileClient(1, "./ice-data", "com.your.package");
client.start();
// Execute
IceRoam roam = IceRoam.create();
roam.setId(1L);
roam.put("uid", 12345);
roam.put("score", 95.5);
Ice.syncProcess(roam);
Go
// Initialize
client, _ := ice.NewClient(1, "./ice-data")
client.Start()
defer client.Destroy()
// Execute
roam := ice.NewRoam()
roam.SetId(1)
roam.Put("uid", 12345)
roam.Put("score", 95.5)
ice.SyncProcess(context.Background(), roam)
Python
# Initialize
client = ice.FileClient(app=1, storage_path="./ice-data")
client.start()
# Execute
roam = ice.Roam.create()
roam.set_id(1)
roam.put("uid", 12345)
roam.put("score", 95.5)
ice.sync_process(roam)
Open the Web UI → create a rule tree → arrange your nodes → click Apply. Changes take effect within seconds — no restart required.
Comparison with Traditional Rule Engines
|
Ice |
Traditional (Drools, etc.) |
| Learning Curve |
5 minutes — visual configuration, no DSL |
Requires learning rule language syntax |
| Deployment |
Docker one-click, zero external dependencies |
Requires database + middleware setup |
| Configuration |
Web UI tree editor |
Text/code-based rule files |
| Performance |
In-memory, sub-millisecond latency |
Compilation and interpretation overhead |
| Hot Reload |
Seconds, automatic — no restart |
Often requires application restart |
| Change Impact |
Node-isolated — changes affect only that node |
Cascading effects across rule chains |
| Ops Complexity |
Single binary + file directory |
Multi-component infrastructure |
Use Cases
| Scenario |
Examples |
| Marketing Campaigns |
Coupons, discounts, promotions, group deals, flash sales |
| Risk Control |
Credit assessment, anti-fraud detection, real-time risk evaluation |
| Dynamic Pricing |
Price strategies, discount rules, tiered pricing, surge pricing |
| Access Control |
Permission management, feature flags, role-based access |
| Process Orchestration |
Approval workflows, order processing, ticket routing, state machines |
Configuration Reference
Client Configuration
| Parameter |
Required |
Default |
Description |
app |
Yes |
— |
Application ID |
storagePath |
Yes |
— |
Path to shared ice-data/ directory |
scan |
Java only |
— |
Package path for automatic node scanning |
pollInterval |
No |
2s |
How often to check for version changes |
heartbeatInterval |
No |
10s |
Heartbeat reporting interval |
parallelism |
No |
ForkJoinPool |
Thread pool size for parallel nodes |
lane |
No |
— |
Lane name for traffic isolation / branch testing |
Server Configuration
| Parameter |
Default |
Description |
port |
8121 |
Server HTTP port |
storage-path |
./ice-data |
File storage directory |
mode |
open |
open (normal) or controlled (read-only UI) |
client-timeout |
30s |
Client inactivity timeout |
version-retention |
1000 |
Number of version files to retain |
publish-targets |
— |
Remote server addresses for multi-instance publishing |
Documentation
Guide
- Getting Started — Deploy, integrate, and run your first rule
- Core Concepts — Trees, nodes, Roam, and execution model
- Architecture — Design decisions and deployment patterns
- FAQ — Common questions and troubleshooting
Reference
SDK Guides
Contributing
Contributions are welcome! Whether it’s bug reports, feature requests, documentation improvements, or code contributions — feel free to open an issue or submit a pull request.
License
Apache License 2.0
评论