> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gorules.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Java Rules Engine

> Integrate GoRules into your Java application.

Install the ZEN Engine and evaluate your first decision in Java.

## Installation

<CodeGroup>
  ```xml Maven theme={null}
  <dependency>
      <groupId>io.gorules</groupId>
      <artifactId>zen-engine</artifactId>
      <version>0.4.7</version>
  </dependency>
  ```

  ```groovy Gradle theme={null}
  implementation("io.gorules:zen-engine:0.4.7")
  ```
</CodeGroup>

## Basic usage

```java theme={null}
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

```java theme={null}
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

```java theme={null}
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

```java theme={null}
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

```java theme={null}
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:

```java theme={null}
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

```java theme={null}
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:

```java theme={null}
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:

```java theme={null}
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

<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.
</Note>

## Best practices

**Use try-with-resources.** `ZenEngine` implements `AutoCloseable` to release native resources.

```java theme={null}
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.
