Install the ZEN Engine and evaluate your first decision in Java.
Installation
<dependency>
<groupId>io.gorules</groupId>
<artifactId>zen-engine</artifactId>
<version>0.4.7</version>
</dependency>
Basic usage
import io.gorules.zen_engine.ZenEngine;
import io.gorules.zen_engine.ZenDecision;
import io.gorules.zen_engine.JsonBuffer;
public class Main {
public static void main(String[] args) throws Exception {
var ruleJson = Main.class.getResourceAsStream("/rules/pricing.json").readAllBytes();
try (var engine = new ZenEngine(null, null)) {
var decision = engine.createDecision(new JsonBuffer(ruleJson));
var input = new JsonBuffer("""
{
"customer": { "tier": "gold", "yearsActive": 3 },
"order": { "subtotal": 150, "items": 5 }
}
""");
var response = decision.evaluate(input, null).join();
System.out.println(response.result());
// => {"discount":0.15,"freeShipping":true}
}
}
}
Loader
The loader pattern enables dynamic decision loading from any storage backend. The loader function returns CompletableFuture<JsonBuffer>. Use ConcurrentHashMap to cache decisions for optimal performance.
File system
import io.gorules.zen_engine.ZenEngine;
import io.gorules.zen_engine.JsonBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
var cache = new ConcurrentHashMap<String, byte[]>();
var engine = new ZenEngine(key -> {
var bytes = cache.computeIfAbsent(key, k -> {
try {
return Files.readAllBytes(Path.of("./rules", k));
} catch (Exception e) {
throw new RuntimeException(e);
}
});
return CompletableFuture.completedFuture(new JsonBuffer(bytes));
}, null);
var response = engine.evaluate("pricing.json", new JsonBuffer("{}"), null).join();
System.out.println(response.result());
AWS S3
import io.gorules.zen_engine.ZenEngine;
import io.gorules.zen_engine.JsonBuffer;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.regions.Region;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipEntry;
import java.io.ByteArrayInputStream;
var s3 = S3Client.builder().region(Region.US_EAST_1).build();
var rules = new ConcurrentHashMap<String, byte[]>();
// Download and extract all decisions at startup
var request = GetObjectRequest.builder()
.bucket("my-rules-bucket")
.key("decisions.zip")
.build();
var zipBytes = s3.getObjectAsBytes(request).asByteArray();
try (var zis = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (!entry.isDirectory()) {
rules.put(entry.getName(), zis.readAllBytes());
}
}
}
var engine = new ZenEngine(key -> {
var bytes = rules.get(key);
if (bytes == null) {
return CompletableFuture.failedFuture(new RuntimeException("Decision not found: " + key));
}
return CompletableFuture.completedFuture(new JsonBuffer(bytes));
}, null);
var response = engine.evaluate("pricing.json", new JsonBuffer("{}"), null).join();
System.out.println(response.result());
Azure Blob Storage
import io.gorules.zen_engine.ZenEngine;
import io.gorules.zen_engine.JsonBuffer;
import com.azure.storage.blob.BlobServiceClientBuilder;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipEntry;
import java.io.ByteArrayInputStream;
var blobService = new BlobServiceClientBuilder()
.connectionString(System.getenv("AZURE_STORAGE_CONNECTION"))
.buildClient();
var container = blobService.getBlobContainerClient("rules");
var rules = new ConcurrentHashMap<String, byte[]>();
// Download and extract all decisions at startup
var blob = container.getBlobClient("decisions.zip");
var zipBytes = blob.downloadContent().toBytes();
try (var zis = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (!entry.isDirectory()) {
rules.put(entry.getName(), zis.readAllBytes());
}
}
}
var engine = new ZenEngine(key -> {
var bytes = rules.get(key);
if (bytes == null) {
return CompletableFuture.failedFuture(new RuntimeException("Decision not found: " + key));
}
return CompletableFuture.completedFuture(new JsonBuffer(bytes));
}, null);
var response = engine.evaluate("pricing.json", new JsonBuffer("{}"), null).join();
System.out.println(response.result());
Google Cloud Storage
import io.gorules.zen_engine.ZenEngine;
import io.gorules.zen_engine.JsonBuffer;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipEntry;
import java.io.ByteArrayInputStream;
var storage = StorageOptions.getDefaultInstance().getService();
var rules = new ConcurrentHashMap<String, byte[]>();
// Download and extract all decisions at startup
var blob = storage.get("my-rules-bucket", "decisions.zip");
var zipBytes = blob.getContent();
try (var zis = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (!entry.isDirectory()) {
rules.put(entry.getName(), zis.readAllBytes());
}
}
}
var engine = new ZenEngine(key -> {
var bytes = rules.get(key);
if (bytes == null) {
return CompletableFuture.failedFuture(new RuntimeException("Decision not found: " + key));
}
return CompletableFuture.completedFuture(new JsonBuffer(bytes));
}, null);
var response = engine.evaluate("pricing.json", new JsonBuffer("{}"), null).join();
System.out.println(response.result());
Async evaluation
Evaluation returns CompletableFuture for non-blocking execution:
import io.gorules.zen_engine.ZenEngineResponse;
import java.util.concurrent.CompletableFuture;
import java.util.List;
import java.util.ArrayList;
var futures = new ArrayList<CompletableFuture<ZenEngineResponse>>();
for (var input : inputs) {
futures.add(decision.evaluate(new JsonBuffer(input), null));
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
for (var future : futures) {
System.out.println(future.get().result());
}
Error handling
import io.gorules.zen_engine.ZenException;
try {
var response = decision.evaluate(input, null).join();
System.out.println(response.result());
} catch (Exception e) {
if (e.getCause() instanceof ZenException zenEx) {
System.err.println("Evaluation failed: " + zenEx.getMessage());
} else {
throw e;
}
}
Tracing
Enable tracing to inspect decision execution:
import io.gorules.zen_engine.ZenEvaluateOptions;
var options = new ZenEvaluateOptions((byte) 1, null); // (trace, maxDepth) - use (byte) 1 to enable tracing
var response = decision.evaluate(input, options).join();
System.out.println(response.trace());
// Each node's input, output, and performance timing
System.out.println(response.performance());
// Total evaluation time
Expression utilities
Evaluate ZEN expressions outside of a decision context:
import io.gorules.zen_engine.ZenUniffi;
import io.gorules.zen_engine.JsonBuffer;
// Standard expressions
var context = new JsonBuffer("""
{ "a": 5, "b": 3 }
""");
var result = ZenUniffi.evaluateExpression("a + b", context);
// => 8
var itemsContext = new JsonBuffer("""
{ "items": [1, 2, 3, 4] }
""");
var total = ZenUniffi.evaluateExpression("sum(items)", itemsContext);
// => 10
// Unary expressions (comparison against $)
var unaryContext = new JsonBuffer("""
{ "$": 10 }
""");
var isValid = ZenUniffi.evaluateUnaryExpression(">= 5", unaryContext);
// => true
var listContext = new JsonBuffer("""
{ "$": "US" }
""");
var inList = ZenUniffi.evaluateUnaryExpression("'US', 'CA', 'MX'", listContext);
// => true
The Java bindings use JNA (Java Native Access) for interoperability with the native Rust engine. This introduces some overhead compared to native Rust or direct bindings. We plan to revisit this when the FFM (Foreign Function & Memory) API becomes more widely adopted.
Best practices
Use try-with-resources. ZenEngine implements AutoCloseable to release native resources.
try (var engine = new ZenEngine(null, null)) {
// use engine
}
Initialize the engine once. Create a single ZenEngine instance at application startup and reuse it for all evaluations.
Implement a loader for dynamic decisions. The loader pattern centralizes decision loading logic and enables caching with ConcurrentHashMap.
Use CompletableFuture composition. Chain async operations or use allOf for parallel evaluation of multiple decisions.