Skip to main content
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

Performance note

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.