Getting Started with PAL
This guide walks you through installing PAL, running your first peer, and inspecting the messages it produces.
Prerequisites
Before you begin, ensure you have:
- Java 17 or later (check with
java -version) - Docker (optional, for distributed mode with etcd/Kafka)
Installation
Option 1: Download Binary Distribution (Recommended)
Download the latest release from GitHub Releases:
tar xzf pal-*.tar.gz
cd pal-*/
# Install to /usr/local (may need sudo)
./install.sh
# Or install to a custom directory
./install.sh --prefix=~/.local
This copies PAL to PREFIX/lib/pal/ and creates a symlink at PREFIX/bin/pal. If PREFIX/bin is already on your PATH, you're ready to go.
Option 2: Build from Source
Requires Maven 3.x and Git:
git clone https://github.com/quasientio/pal.git
cd pal
./mvnw install -DskipTests
export PATH="$(pwd)/bin:$PATH"
Verify Installation
You should see:
Usage: pal [OPTIONS] COMMAND
The message-passing runtime for Java
Options:
-d, --dir HOST:PORT PAL directory [env: PAL_DIRECTORY]
-h, --help Show this help message and exit.
-V, --version Print version information and exit.
Commands:
run Run a new peer
replay Replay application deterministically from a recorded WAL
init Initialize a project for PAL
Management Commands:
peer Manage peers
log Manage logs
intercept Manage intercepts
Shortcuts:
peers List peers (shorthand for 'peer ls')
logs List logs (shorthand for 'log ls')
intercepts List intercepts (shorthand for 'intercept ls')
Run 'pal COMMAND --help' or 'pal COMMAND SUBCOMMAND --help' for more information.
Your First PAL Application
Let's build a simple application and run it with PAL to see operations become messages.
1. Create a Project with pal init
The fastest way to get started is with pal init, which sets up everything you need — build descriptors including AspectJ weaving, sample code, PAL configuration and environment files:
The interactive wizard prompts for build tool, identifiers, and a few capability questions. For this tutorial, accept the defaults except: choose Yes, alongside message pipeline for JSON-RPC, answer y to both the interceptable and intercepting prompts, and accept com.example.Main as the run mode. These choices scaffold the sample classes, RPC policy, and intercept bundle we use throughout the rest of the guide:
Welcome to PAL! Let's set up your project.
Build tool (use arrow keys, Enter to confirm)
❯ GRADLE
MAVEN
Project group ID [com.example]:
Project artifact ID [pal-tutorial]:
Project version [1.0]:
Will you expose methods via JSON-RPC? (use arrow keys, Enter to confirm)
No
❯ Yes, alongside message pipeline
Yes, RPC only (no weaving needed)
Will this app be interceptable by other peers? [y/N] y
Will this app intercept other peers? [y/N] y
Run mode (use arrow keys, Enter to confirm)
Run as service (no main class)
❯ com.example.Main
Will you use Kafka for WAL (write-ahead log)? [y/N]
✓ [CREATE] pal-tutorial/build.gradle
✓ [CREATE] pal-tutorial/settings.gradle
✓ [CREATE] pal-tutorial/src/main/java/com/example/Main.java
✓ [CREATE] pal-tutorial/src/main/java/com/example/SampleService.java
✓ [CREATE] pal-tutorial/src/main/java/com/example/SampleCallbacks.java
✓ [CREATE] pal-tutorial/src/main/java/com/example/Api.java
✓ [CREATE] pal-tutorial/config/peer-logging.xml
✓ [CREATE] pal-tutorial/config/cli-logging.xml
✓ [CREATE] pal-tutorial/config/rpc-policy.yaml
✓ [CREATE] pal-tutorial/config/intercept-bundle.yaml
✓ [CREATE] pal-tutorial/.env.pal
✓ [CREATE] pal-tutorial/infra/docker-compose.yml
✓ [CREATE] pal-tutorial/infra/.env
✓ [CREATE] pal-tutorial/infra/start.sh
✓ [CREATE] pal-tutorial/infra/stop.sh
✓ [CREATE] pal-tutorial/gradlew
✓ [CREATE] pal-tutorial/gradlew.bat
✓ [CREATE] pal-tutorial/gradle/wrapper/gradle-wrapper.properties
✓ [CREATE] pal-tutorial/gradle/wrapper/gradle-wrapper.jar
✓ [CREATE] pal-tutorial/README.md
Checking for pal-weave 1.0.0...
✓ pal-weave 1.0.0 available
✓ Project initialized!
Next steps:
1. cd pal-tutorial
2. ./gradlew build # Build with AspectJ weaving
3. infra/start.sh # Start infrastructure
4. source .env.pal
5. pal run -cp build/classes/java/main com.example.Main
See README.md for WAL, interception, JSON-RPC examples, and more.
(The real output shows absolute paths; paths are shortened here for readability.)
The result is a complete project with sample code, configuration, and an infra/ directory that brings up etcd via docker-compose:
pal-tutorial/
├── build.gradle
├── config/
│ ├── cli-logging.xml
│ ├── intercept-bundle.yaml
│ ├── peer-logging.xml
│ └── rpc-policy.yaml
├── gradle/wrapper/{gradle-wrapper.jar, gradle-wrapper.properties}
├── gradlew, gradlew.bat
├── infra/
│ ├── docker-compose.yml
│ ├── start.sh
│ └── stop.sh
├── README.md
├── settings.gradle
└── src/main/java/com/example/
├── Api.java
├── Main.java
├── SampleCallbacks.java
└── SampleService.java
What the generated source files are for:
Main.java— entry point used by local mode below. CallsSampleService.processOrderto drive a few operations.SampleService.java— the class whose method calls and field accesses the Interception section will intercept.SampleCallbacks.java— public static callback handler that prints intercepted operations to stdout.Api.java— public static methods (greet,add,toUpperCase) invoked over JSON-RPC in the Distributed Mode section.
And the configuration files:
config/intercept-bundle.yaml— declares an intercept onSampleService.processOrderthat delegates toSampleCallbacks.handle. We apply it in the interception section.config/rpc-policy.yaml— gates which methods are reachable via JSON-RPC. Permits public methods by default; blocks JDK internals.infra/start.sh/infra/stop.sh— start and stop the bundled etcd container used by distributed mode and interception.
For scripted setups, the same flags are exposed non-interactively. The shortest form that produces the same files as the interactive walkthrough above:
This pre-answers the y/N prompts; you'll still confirm build tool, group/artifact/version, run mode, and Kafka. To skip every prompt (CI mode), add -y:
pal init pal-tutorial -y \
--group-id com.example \
--artifact-id pal-tutorial \
--main-class com.example.Main \
--interceptable --intercepting --json-rpc
To enable all PAL features at once (interceptable, intercepting, JSON-RPC, Kafka, scope policy):
Tip: Use --dry-run to preview what pal init would generate without writing any files:
2. Build the Application
3. Run with PAL (Local Mode)
Let's start simple: run with Chronicle Queue (no Kafka/etcd needed).
# Run the application with PAL
pal run --wal file:/tmp/tutorial-wal -cp build/classes/java/main com.example.Main
You should see the application output:
Processing order: 1x Laptop @ $999.99
Discount applied! Total: $899.99
Processing order: 5x Mouse @ $29.99
Processing order: 2x Keyboard @ $79.99
Orders processed: 3
Total revenue: $1309.92
What just happened?
Every operation — the processOrder calls, each orderCount/totalRevenue field read and write inside SampleService, and the println calls — was converted to a message and logged to /tmp/tutorial-wal. This transformation happened at build time: Gradle's AspectJ weaving task wove PAL's aspects into your compiled .class files during the build step. At runtime, these woven classes create messages for each operation, which PAL then writes to the WAL.
4. Inspect the Messages
Let's see what PAL captured:
Output (abbreviated; offsets and object IDs vary):
[0] call Main.main
[1] call SampleService.processOrder
[2] get SampleService.orderCount
[3] return int(=0)
[4] put SampleService.orderCount ⇦ (=1)
[5] put_done SampleService.orderCount
[6] get SampleService.totalRevenue
[7] return double(=0.0)
[8] put SampleService.totalRevenue ⇦ (=999.99)
[9] put_done SampleService.totalRevenue
[10] call PrintStream.println ("Processing order: 1x Laptop @ $999.99")
[11] return void
[12] call SampleService.applyDiscount
[13] return double(=899.991)
[14] call PrintStream.println (" Discount applied! Total: $899.99")
[15] return void
[16] return void
(two more processOrder subtrees follow the same shape — Mouse and Keyboard,
neither triggering the discount branch)
...
[N] get SampleService.orderCount
[N+1] return int(=3)
[N+2] call PrintStream.println ("Orders processed: 3")
[N+3] return void
[N+4] get SampleService.totalRevenue
[N+5] return double(=1309.92)
[N+6] call PrintStream.println ("Total revenue: $1309.92")
[N+7] return void
[N+8] return void
Other output formats are available (--full, --json). See CLI Reference for details.
This is the key insight: Every operation is now a discrete, inspectable message.
5. Replay the Execution
You can replay the execution from the log:
# Replay the recorded execution deterministically
pal replay --wal file:/tmp/tutorial-wal -cp build/classes/java/main com.example.Main
You'll see the same output as before, but this time it's being replayed from messages, not executed from Main.main().
This enables time-travel debugging: Any execution can be replayed exactly as it happened.
Adding PAL to an Existing Project
If you already have a Gradle or Maven project, run pal init in the project directory. It detects the existing build file and patches it to add PAL weaving:
Detected existing GRADLE project: com.acme:order-service (1.2.0)
Will you expose methods via JSON-RPC? (use arrow keys, Enter to confirm)
❯ No
Yes, alongside message pipeline
Yes, RPC only (no weaving needed)
Will this app be interceptable by other peers? [y/N]
Will this app intercept other peers? [y/N]
Main class (for pal run) [com.acme.OrderServiceMain]:
Will you use Kafka for WAL (write-ahead log)? [y/N]
✓ [PATCH] build.gradle
✓ [CREATE] config/peer-logging.xml
✓ [CREATE] config/cli-logging.xml
✓ [CREATE] .env.pal
✓ [CREATE] README.md
Checking for pal-weave 1.0.0...
✓ pal-weave 1.0.0 available
A backup of your original build file is created automatically (build.gradle.backup or pom.xml.backup). Use --dry-run to preview the changes before applying them:
For Maven projects, the same flow applies — pal init auto-detects pom.xml:
Manual Setup (Alternative)
If you prefer full control over the build configuration, you can set up PAL manually instead of using pal init.
Create a build.gradle with the ajc weaving task and pal-weave dependency (this matches what pal init generates):
plugins {
id 'java'
}
group = 'com.example'
version = '1.0'
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenLocal()
mavenCentral()
}
configurations {
aspectjTools
aspect
}
dependencies {
aspectjTools 'org.aspectj:aspectjtools:1.9.24'
aspect 'io.quasient.pal:pal-weave:1.0.0'
implementation 'org.aspectj:aspectjrt:1.9.24'
}
// Weave after test so unit tests see unwoven classes. Skip with: ./gradlew build -x weaveClasses
tasks.register('weaveClasses', JavaExec) {
dependsOn classes
mustRunAfter test
mainClass = 'org.aspectj.tools.ajc.Main'
classpath = configurations.aspectjTools
args = [
'-inpath', sourceSets.main.output.classesDirs.asPath,
'-aspectpath', configurations.aspect.asPath,
'-d', sourceSets.main.java.destinationDirectory.get().asFile.path,
'-classpath', sourceSets.main.compileClasspath.asPath,
'-source', java.sourceCompatibility.toString(),
'-target', java.targetCompatibility.toString(),
]
}
tasks.named('jar') { dependsOn weaveClasses }
Maven equivalent (pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>pal-tutorial</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<pal.version>1.0.0</pal.version>
<aspectj.version>1.9.24</aspectj.version>
</properties>
<dependencies>
<dependency>
<groupId>io.quasient.pal</groupId>
<artifactId>pal-weave</artifactId>
<version>${pal.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.15.0</version>
<configuration>
<complianceLevel>17</complianceLevel>
<source>17</source>
<target>17</target>
<aspectLibraries>
<aspectLibrary>
<groupId>io.quasient.pal</groupId>
<artifactId>pal-weave</artifactId>
</aspectLibrary>
</aspectLibraries>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Then create your Java source files, build with ./gradlew build, and run with pal run as described above.
Distributed Mode: Cross-Peer RPC
Now let's see how PAL enables cross-peer method invocation.
1. Run as a Service Peer
We'll expose the Api class that pal init already scaffolded under src/main/java/com/example/Api.java:
package com.example;
public class Api {
public static String greet(String name) { return "Hello, " + name + "!"; }
public static int add(int a, int b) { return a + b; }
public static String toUpperCase(String txt) { return txt.toUpperCase(); }
}
In one terminal, launch a peer that accepts JSON-RPC calls on a known port:
pal run --wal file:/tmp/service-wal --json-rpc 7070 \
--rpc-policy config/rpc-policy.yaml \
-cp build/classes/java/main
A few things to notice:
- No main class on the command line. Without one,
pal runkeeps the peer alive to handle incoming RPC — noThread.sleepwrapper needed. --rpc-policy config/rpc-policy.yamlpoints at the policy filepal initgenerated. ItsdefaultAction: ALLOWpermits public methods while blockingUnsafe, JDK internals, and non-public members — a permissive development profile. Without any policy (or--rpc-default-action ALLOWon the command line), every incoming RPC is denied withRpcAccessDeniedException.--json-rpc 7070binds the JSON-RPC WebSocket endpoint to a fixed port so the caller can reach it directly. Use--json-rpc autoto let PAL pick a free port (you'd then need a directory to advertise it).
2. Call the Service Remotely
In another terminal, pipe a JSON-RPC request on stdin to the peer's WebSocket address (pal peer call reads one request per line, so keep the JSON on a single line):
echo '{"jsonrpc":"2.0","id":"1","method":"call","params":{"type":"com.example.Api","method":"greet","args":[{"type":"java.lang.String","value":"World"}]}}' | pal peer call ws://localhost:7070
Response:
Call a method with typed int arguments:
echo '{"jsonrpc":"2.0","id":"2","method":"call","params":{"type":"com.example.Api","method":"add","args":[{"type":"int","value":10},{"type":"int","value":20}]}}' | pal peer call ws://localhost:7070
Response: "value":"30" — add(10, 20) = 30.
What happened:
pal peer callopened a WebSocket tows://localhost:7070and sent the JSON-RPC request.- The peer's RPC policy matched the request against
defaultAction: ALLOW— permitted. - PAL invoked
com.example.Api.add(10, 20)via reflection. - The result
30came back as a typed JSON-RPC response.
JSON-RPC over stdin gives you full control over argument types — the right choice for any method whose signature is not static void m(String[]). For methods that do take a String[], pal peer call also accepts positional CLI arguments. See CLI Reference — Invocation Modes for both forms.
3. Inspect the RPC Messages
The WAL captured every incoming RPC as a first-class message — the same substrate you saw in local mode.
Note on addressing: This example uses direct
ws://host:portaddressing because we haven't started etcd (the PAL directory). If you run a directory and start the peer with-d localhost:2379 -n service, you can call it by name instead:pal peer call -d localhost:2379 service. Running PAL at scale typically means multiple peers discovering each other through the directory; the direct form here is for the local-development workflow.
Interception: Dynamic Behavior Modification
Now let's intercept method calls at runtime — without modifying any application code. pal init already scaffolded everything we need:
SampleService.processOrder— the target method, already called fromMain.java.SampleCallbacks.handle— a public static callback that prints intercepted operations to stdout.config/intercept-bundle.yaml— declares the intercept that wires the two together.
We'll start the directory, launch a callback peer, apply the bundle, then run Main and watch the callback fire.
1. Start the Directory
Interception uses the directory (etcd) to register intercepts and to resolve callback peers by name. The project's infra/ directory contains a docker-compose for exactly this:
This brings up an etcd reachable at localhost:2379. Run infra/stop.sh when you're done.
2. Launch the Callback Peer
The scaffolded bundle's peer: field is the project's artifact ID (pal-tutorial), so the callback peer must register under that name. In a new terminal, start a peer with no main class — it stays alive to receive callbacks — and a --zmq-rpc endpoint so the application peer can reach it:
pal run -d localhost:2379 -n pal-tutorial --zmq-rpc auto \
--wal file:/tmp/cb-wal \
-cp build/classes/java/main
3. Apply the Intercept Bundle
In another terminal, apply the bundle that pal init generated:
You should see:
For reference, that bundle declares (open config/intercept-bundle.yaml to see):
bundle: "pal-tutorial-intercepts"
defaults:
peer: "pal-tutorial"
intercepts:
- target: com.example.SampleService.processOrder
type: BEFORE
callback:
class: com.example.SampleCallbacks
method: handle
4. Run Main with Interception Enabled
Run the application again, this time registered in the directory and marked --interceptable so it picks up the registered intercept:
Main runs the same three processOrder calls as before, then exits.
5. Observe the Callback
In the callback peer's terminal, three intercept callbacks now appear — one per processOrder call:
[intercept] BEFORE callback, args=[Laptop, 1, 999.99]
[intercept] BEFORE callback, args=[Mouse, 5, 29.99]
[intercept] BEFORE callback, args=[Keyboard, 2, 79.99]
That output came from SampleCallbacks.handle running on the callback peer, in response to method calls on the application peer — no edits to SampleService.java, no rebuild, no restart needed to attach the observer.
A BEFORE intercept observes but does not alter the call. Switch the bundle's type: to AROUND and use ctx.setReturnValue(...) (see Interception) to mock or transform results instead. Re-apply with pal intercept apply — the bundle is idempotent.
6. Remove the Intercept
Detach the callback — live, no peer restart required:
Subsequent runs of Main execute un-intercepted.
7. Stop the Infrastructure
When you're done, shut down the callback peer (Ctrl-C in its terminal) and stop etcd:
Common Operations
Start/Stop Infrastructure
# Start infrastructure (from your project's infra/ directory)
infra/start.sh
# Stop
infra/stop.sh
# Check status
docker ps | grep -E "etcd|kafka"
View Logs
# List all logs in directory
pal log ls -d localhost:2379
# Print messages from a log
pal log print -d localhost:2379 my-log
# Print with full details
pal log print -d localhost:2379 my-log --full
# Print specific message at offset 100
pal log print -d localhost:2379 my-log -o 100
Call Remote Methods
# By peer name
pal peer call -d localhost:2379 my-service \
com.example.MyClass myMethod arg1 arg2
# By peer UUID
pal peer call -d localhost:2379 550e8400-... \
com.example.MyClass myMethod arg1 arg2
# With JSON-RPC on stdin (full control over types)
echo '{"jsonrpc":"2.0","id":"1","method":"call","params":{
"type":"com.example.MyClass","method":"myMethod",
"args":[{"type":"java.lang.String","value":"\"arg1\""}]
}}' | pal peer call -d localhost:2379 my-service
Clean Up
# Remove a peer from directory
pal peer rm -d localhost:2379 my-peer-uuid
# Remove a log from directory (doesn't delete Kafka topic)
pal log rm -d localhost:2379 my-log
# Remove a Chronicle log (deletes files and, if registered, the directory entry)
pal log rm file:/tmp/tutorial-wal
Troubleshooting
"ERROR_UNREACHABLE_ETCD" (exit code 14)
"ERROR_NO_KAFKA_SERVERS_GIVEN" (exit code 6)
# Ensure --kafka-servers is provided when using Kafka logs
pal run -k localhost:29092 --wal my-log -cp app.jar
"ERROR_INITIALIZING_LOGS" (exit code 7)
Common causes:
Kafka unreachable:
Chronicle source log doesn't exist:
# Ensure the log exists
ls -la /tmp/tutorial-wal
# If not, create it by running a peer with --wal first
pal run --wal file:/tmp/tutorial-wal -cp app.jar Main
AspectJ Weaving Not Working
# Verify weaving (adjust path for your build tool)
javap -c build/classes/java/main/tutorial/OrderService.class | grep aspectOf
# If nothing found, check your build file includes pal-weave
# and the AspectJ weaving configuration
Common causes:
- Missing AspectJ plugin: Ensure your Gradle or Maven build includes the AspectJ weaving plugin with
pal-weaveas an aspect library. - Class not woven: Only classes processed by the AspectJ plugin will have PAL's dispatch. Verify with
javap -cas shown above. - Wrong plugin configuration: The aspect library must reference
pal-weave, not just have it as a dependency. - Incremental build stale: Try a clean build (
./gradlew clean buildormvn clean install) to ensure all classes are freshly woven.
Interception Not Working
- Ensure class was compiled with AspectJ weaving.
- Ensure peer started with
--interceptableflag. - Ensure etcd is up.
- Verify intercept is registered (see
pal intercept ls) - Ensure the callback peer is launched with
--zmq-rpc - Check intercept pattern matches class/method names.
- Enable debug logging in logback.xml:
Infrastructure Failure Modes
For detailed information about what happens when etcd or Kafka become unavailable (at startup or while running), see Trade-offs and Limitations — Failure Modes.
Next Steps
Now that you've experienced PAL's core capabilities:
- Understand the concepts: Read Understanding PAL for deep dive.
- Explore use cases: See Use Cases for your role (developer, SRE, architect).
- Learn the details:
- Peers and Logs
- RPC
- Interception
- Log Backends
- Follow guides:
- Local Development
- Distributed Application
Summary
You've learned:
- ✓ How to install and set up PAL
- ✓ How to scaffold a project with
pal init(or configure manually) - ✓ How to add PAL to an existing project with
pal init - ✓ How to compile applications with AspectJ weaving
- ✓ How operations become messages (quantization)
- ✓ How to run peers locally (Chronicle) and distributed (Kafka/etcd)
- ✓ How to inspect messages in logs
- ✓ How to call remote methods via RPC
- ✓ How to replay executions from logs
- ✓ The basics of interception for dynamic behavior
PAL converts your operations into messages. Everything else flows from that simple transformation.
Need Help?
Join us on Discord — ask questions, share feedback, or just say hello.