A lightweight, zero-dependency rule engine with visual tree-based orchestration
Documentation · Live Demo · Getting Started · Changelog
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.
| 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 |
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
| 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.
| 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 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/ Gosync.RWMutex/ Pythonthreading.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.,@uidreadsroam.get("uid"))
┌──────────────────────────────────────────────────────────────┐
│ 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
| 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 |
docker run -d --name ice-server -p 8121:8121 \
-v ./ice-data:/app/ice-data waitmoon/ice-server:latestOpen http://localhost:8121 to access the Web UI.
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/goPython
pip install ice-rulesLeaf 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.scoreJava
// 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.
| 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 |
| 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 |
| 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 |
| 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 |
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
- Node Types — All relation and leaf node types in detail
- Client Configuration — Full client parameter reference
- Server Configuration — Full server parameter reference
- Roam API — Data context API across all SDKs
SDK Guides
- GitHub Issues — Bug reports and feature requests
- GitHub Discussions — Questions and community discussions
- Live Demo — Try Ice without installing anything
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.