Server-side UI framework for Java: how webforJ works

webforJ is a server-side UI framework for Java. Your components are Java objects that run on the server, the browser renders them as web components, and user actions come back to your Java event handlers. There is no JavaScript app to write, no REST layer between the screen and your services, and your UI code never leaves the server.
This post explains the runtime model step by step: where components live, how events travel, which thread runs your code, and where state is kept. It also covers the trade-offs of a server-driven UI and how webforJ handles each one.
What a server-side UI framework is
In a server-side UI framework, the program that decides what the screen looks like runs on the server. The client displays what the server describes and reports what the user did. You do not split the logic between two runtimes.
In webforJ, that program is ordinary Java. You build a view from component objects such as Button, TextField and Table, attach listeners, and route the class with @Route. The framework keeps the browser in step with your objects.
How the browser and the server split the work
When a user opens a webforJ app, the browser loads a small bootstrap script, about 2.5 kB gzipped, to start the session. The client/server interaction page describes the details. After that it downloads further chunks on demand. The code for a Button, for example, is sent once, when your app creates its first Button.
The traffic between the two sides falls into three groups:
| Direction | Example | Waits for a reply? |
|---|---|---|
| Server to client | setText() on a component | No. Calls are batched. |
| Client to server | A Button.onClick() event | No. The event carries its data. |
| Round trip | Label.getText() or Checkbox.isChecked() | Yes. The app pauses until the client answers. |
The first two are the normal flow. The third is the one to watch, and the next section shows how to avoid it.
Events return to Java handlers
A click in the browser becomes a call to a Java lambda. You register it with onClick, onModify, or the add...Listener method for the event you need. The events guide lists them.
Events carry their payload with them, so read it from the event rather than asking the component again. This handler reads the text from the event and causes no round trip:
field.onModify(event -> {
String text = event.getText();
// use text
});
Calling field.getText() inside the same handler would send a request to the browser and wait.
One UI thread, and how to leave it
All UI operations in webforJ happen on the Environment thread. Events are processed sequentially on it, so two handlers never run against the same components at the same moment.
Long work does not belong on that thread. Run it on another thread and hand the result back with Environment.runLater():
report.onClick(e -> {
status.setText("Working...");
new Thread(() -> {
String result = slowReport();
Environment.runLater(() -> status.setText(result));
}).start();
});
A thread created from inside the Environment thread inherits access to it, as the asynchronous updates guide explains. Threads created elsewhere, such as a static initializer or a library's own thread pool, do not, and runLater() throws an IllegalStateException there.
Where state lives
State lives on the server, in the objects you create. A text field's value, the rows in a table and your own fields in a view class are plain Java state. There is no second copy in a client store to keep in sync.
webforJ gives you scopes when you want that state to outlive a single view. With Spring, @WebforjSessionScope creates a bean shared across the browser tabs of one user session, and @EnvironmentScope creates one per tab or window. Session-scoped beans are stored as HTTP session attributes, so they must implement Serializable:
@Service
@WebforjSessionScope
public class Cart implements Serializable {
private final List<String> items = new ArrayList<>();
public void add(String item) { items.add(item); }
public int size() { return items.size(); }
}
Route classes are different. Each navigation to a route gets a fresh instance of the view, so per-visit state does not leak between users or between visits. The scopes guide and the Spring routing guide cover both behaviors.
What you gain
No API layer to secure. A click handler calls your service in the same JVM, so the UI does not need endpoints that exist only to feed it. You still secure what is reachable: route security annotations such as @RolesAllowed are enforced before a component is rendered. See the security overview. The Spring Boot without a REST API post looks at what drops out of a project when both sides are Java.
Your logic stays on the server. The browser receives the instructions needed to render components and the data you put in them. Your validation rules, pricing logic and service calls are Java classes that run on the server. If an endpoint is still needed for another client, Spring MVC controllers can run beside the UI.
The compiler covers the whole screen. The UI, the handlers and the logic they call are one Java codebase. Rename a method on a service and the compiler flags every view that calls it. Your IDE refactoring tools work across the boundary because there is no boundary.
