ZeroZ Stack · The full-stack framework

Four languages to move one object.

A field travels from a Java class, through a JSON encoder, across HTTP, into a TypeScript interface, and onto a DOM node — and at every one of those boundaries the compiler stops being able to help you. ZeroZ Stack removes the boundaries rather than optimising them.

Artifactcom.zeroz4j:zerozstack-*
RuntimeJava 21 · Jakarta EE · WebAssembly
LicenceApache 2.0

The cost

Two boundaries, one object.

The network mismatch

Java objects are serialized into text (JSON), sent over HTTP, and parsed back into JavaScript/TypeScript objects on the client. Every field crosses on faith.

The UI mismatch

JavaScript or TypeScript is required to mutate a browser DOM — fracturing the codebase's language ecosystem in the one place Java never reached.

What that costs on a Tuesday

Rename a field on the server and nothing breaks — not at compile time, not at deploy. It breaks in a browser, for a user, as undefined. Add a nullable field and three artefacts have to agree about it: the entity, the DTO, and the TypeScript interface. Ask an AI agent to add a column end to end and it must hold four languages in context at once, which is where it starts inventing endpoints that do not exist.

The solution

The object is never re-encoded.

A ZeroZ Stack client is compiled Java running as WebAssembly in the browser. It calls a Java interface. The call travels as packed binary over a persistent WebSocket and arrives at a CDI bean as the same object, with the same type. There is no controller, no deserializer and no mapping layer, because there is nothing to map between.

01 · How it works

A full-stack feature in four files.

You avoid boilerplate HTTP mapping, JSON translation, and ORM schemas entirely. Define a model, an interface, its implementation, and a UI — all Java, all sharing the exact same object.

SHARED · MODEL · 1 · ChatMessage.java
// becomes serializable + persistable
@DataModel
public class ChatMessage implements BinaryPackable {
    private String author;
    private String text;

    public ChatMessage() {}
    // constructor, getters, setters…
}
SHARED · API · 2 · ChatService.java
// the RPC contract — one interface
@RmiService
public interface ChatService {
    void sendMessage(ChatMessage msg);
}
SERVER · 3 · ChatServiceImpl.java
@ApplicationScoped
public class ChatServiceImpl implements ChatService {
    @Inject ZeroZDbNode db;

    @Override
    public void sendMessage(ChatMessage msg) {
        // receives the exact object sent — persist it
        db.localDb().write(ctx -> {
            root.getMessages().add(msg);
            ctx.store(root.getMessages());
        });
    }
}
CLIENT · WASM · 4 · ChatView.java
public class ChatView extends Div {
  ChatView(ChatService chat) {
    Button b = new Button("Send");
    b.onClick(e -> {
      // calls the backend over binary WS
      chat.sendMessage(new ChatMessage("Alice", "Hi!"));
    });
    add(b);
  }
}

→ scaffold the multi-module project with the Maven archetype, then mvn clean install and open http://localhost:8080

02 · The model

What the annotations do.

I want to…
The mechanism
What it means
Send this object over the wire
@DataModel
A binary serializer for the class is generated at compile time. No reflection, which WebAssembly handles badly.
Call the server
@RmiService on an interface
A client-side Wasm stub and the server-side dispatch are generated from one definition. You call the interface.
Keep an object current
@LiveSync
The object's state stays identical on the server and in every client holding it. Fetch once; it is patched inline thereafter. No watch, no subscribe, no polling.
Restrict who can call it
@Secured / @RolesAllowed
Authorisation lives on the method, with the call — not in a gateway that has to be kept in step.
React to state in the UI
ValueSignal / Computed / Effect
Dependency-tracked reactivity in the Wasm heap. A signal holds state, a computed derives from it, an effect re-runs when what it read changes.
Persist it
The object graph is the database.

@LiveSync is state synchronization; signals are reactivity. The first keeps the object true; the second decides what the interface does about it. Bind a synchronized object to a signal and a server-side change redraws the UI without a line of glue.

Why binary

A wire that beats JSON on every axis.

The client and server speak a dense binary RPC protocol over a persistent WebSocket. It is not a smaller JSON — it removes the text layer, and its costs, altogether.

Dimension
JSON over REST
ZeroZ4j binary
Encoding
Verbose UTF-8 text
Dense packed bytes
Typing
Untyped — validated at runtime
Statically typed end to end
CPU cost
Serialize + parse both ends
Direct read/write, no parse step
Payload size
Field names repeated per object
No field names on the wire
Contract drift
Silent until it breaks in the browser
Caught by the compiler
Reflection
Required by most JSON libraries
None — serializers generated at build

Under the hood

Ahead-of-time by design, reflection-free.

Performance in the browser is guaranteed at compile time, not hoped for at runtime.

Annotation processing

During Maven compilation, the annotation processor scans for @DataModel and @RmiService, generating _Serializer and _Stub classes. No runtime reflection — which WebAssembly handles poorly.

TeaVM WasmGC

The client module transpiles Java bytecode — including generated stubs and serializers — directly into WasmGC to run in the browser.

Cooperative coroutines

Wasm runs on the single UI thread, so it cannot block. An RMI call suspends the coroutine, sends its frame, yields to the browser, and resumes exactly when the response arrives.

Virtual threads (Loom)

Each incoming WebSocket frame is handed to a virtual thread, so the server's I/O threads never block on long queries — thousands of persistent connections stay responsive.

Modular backend

The core is an agnostic CDI engine and RMI dispatcher; a server binding supplies the HTTP/WebSocket transport.

Zero server-side DOM state

Unlike Vaadin, ZeroZ4j keeps no server-side DOM state. Components live entirely in the client Wasm heap, so a session costs the server nothing but its connection.

Deployment

Embedded, or hosted.

The same application code runs three ways. Embedded, and the store is private to the JVM with no socket in the path. Auto-server, and the first JVM to start owns the store and serves it while the rest become clients — no configuration decides which is which. Or point every JVM at a dedicated database server and let none of them own data. The application code does not change between them; only the mode does.

ZeroZ DB — what each mode guarantees →