A Backend Developer’s Daydream: “What if Frontend Felt Like Spring?”
Building a web UI with 100% Java 24. No JS, just Dependency Injection, Strong Typing, and backend architecture running in the browser.
A Backend Developer’s Daydream: “What if Frontend Felt Like Spring?”

As Java developers, we often feel most at home within the safe walls of our backend systems. We love the robustness of Dependency Injection (DI), the safety of Strong Typing, and the clarity of Layered Architecture.
But when we step into the frontend world, things feel different. React and Vue are fantastic tools, but for a Java developer, their syntax, toolchains, and state management patterns can sometimes feel like a foreign language.
“What if we could build a frontend application using the exact same design patterns and syntax we use in our backend?”
This project is a small experiment born from that curiosity. I built a modern frontend application using 100% Java (Java 24) without writing a single line of JavaScript.
👀 See it in Action
You can try the running application right now. It looks like a standard TodoMVC app, but remember — under the hood, compiled Java.
👉 **Live Demo**
1. DTO Validation? Done in the Constructor.
In the frontend world, we often use schema validation libraries (like Zod or Yup) to ensure data integrity coming from forms or APIs. But in modern Java, we have Records.
Here is the domain model. It’s not just a DTO; it guards its own validity.
public record Todo(String id, String title, State state) {
public Todo {
// Enforce business rules right at the creation moment
if (id == null) throw new IllegalArgumentException("id cannot be null");
if (title == null) throw new IllegalArgumentException("title cannot be null");
if (title.trim().length() < 2) throw new IllegalArgumentException("title must be at least 2 characters long");
if (state == null) throw new IllegalArgumentException("state cannot be null");
}
// Method for immutable state transition
public Todo withState(State state) {
return new Todo(id, title, state);
}
}
The Backend Vibe: “If an object is invalid, it shouldn’t exist in memory at all.” The principle we guard strictly on the server now works effectively in the browser.
2. Elegant Streams over Complex Hooks
Instead of wrestling with useEffect hooks or complex dependency arrays for state management, we can express data flow using an Rx (Reactive) style.
Here is the UI code for the “Active Items” counter. Look how clean it becomes when Java’s Switch Expressions meet Streams.
@Singleton
public class TodoCountActiveLabel implements IsElement<HTMLLabelElement> {
@Delegate private final HTMLContainerBuilder<HTMLLabelElement> label = label().css("todo-count");
@Inject
TodoCountActiveLabel(StatStore store) {
// Data Change (Store) -> Map Logic -> Update Label
// The entire process is connected declaratively.
store.map(stat -> switch (stat.active()) {
case 0 -> "Done!";
case 1 -> "1 item left!";
default -> stat.active() + " items left!";
}).subscribe(label::text);
}
}
The Backend Vibe: Doesn’t this look like a backend service method? Input enters, flows through logic, and produces output. The pipeline is crystal clear.
3. Let @Inject Assemble Your Components
Have you ever struggled with “Props Drilling” in React (passing data through multiple layers of components)? Spring developers solved this problem decades ago with Dependency Injection (DI).
In this architecture, components don’t ask their parents for data. They ask the Container.
@Singleton
public class TodoCardListElement implements IsElement<HTMLUListElement> {
@Delegate private final HTMLContainerBuilder<HTMLUListElement> ul = ul();
private final Map<TodoStore, TodoCardElement> cards = new HashMap<>();
private final TodoCardElement.TodoCardElementFactory factory;
@Inject TodoCardListElement(TodoFiltered todos, TodoCardElement.TodoCardElementFactory factory) {
this.factory = factory;
todos.subscribe(this::update);
}
private void update(JsArray<TodoStore> todos) {
ul.element().textContent = "";
todos.map((todo, idx)-> createElementIfAbsent(todo))
.forEach((child, idx)-> ul.add(child));
}
private TodoCardElement createElementIfAbsent(TodoStore todo) {
return cards.computeIfAbsent(todo, factory::create);
}
}
The Backend Vibe: The UI component doesn’t worry about how to fetch data. It just subscribes to the injected service. Separation of concerns is achieved naturally.
4. Frontend Testing with JUnit? Why Not?
There is a common bias that “Browser tests are slow, flaky, and hard to write.” But by combining Playwright with JUnit (Kotest), we can verify DOM state changes with the same rigor we apply to our API tests.
@GwtHtml("src/test/webapp/todoCardElementTest.html")
class TodoCardElementTest : GwtTestSpec({
Given("TodoCardElement is rendered") {
When("User edits title and presses Enter") {
val todoItems = page.locator(".todo-item")
val firstTodo = todoItems.nth(0)
// Simulate finding a DOM element, double-clicking, and changing value
val titleLabel = firstTodo.locator(".todo-title")
titleLabel.dblclick()
Then("It should enter editing mode") {
val classes = firstTodo.getAttribute("class")
classes shouldContain "editing"
}
Then("Edit input should be visible") {
val editInput = firstTodo.locator(".edit")
editInput.isVisible shouldBe true
}
And("edits title and presses Enter") {
val editInput = firstTodo.locator(".edit")
editInput.click(Locator.ClickOptions().setClickCount(3))
editInput.press("Backspace")
editInput.pressSequentially("Updated task title")
editInput.press("Enter")
Then("Title should be updated") {
val updatedTitle = firstTodo.locator(".todo-title")
updatedTitle.innerText() shouldBe "Updated task title"
}
}
}
}
})
The Backend Vibe: Stop debugging with console.log. We can define frontend behaviors clearly and automate them using BDD (Behavior Driven Development) style tests.
🚀 To Be Continued…
“I like the code style, but how does this actually run?”
I’m sure you have many questions. How does pure Java code turn into executable JavaScript in the browser? How do you set up Gradle? How was the Rx implementation built?
I plan to cover these technical details one by one in upcoming posts:
- **Environment Setup:** Building a modern Java Frontend environment with Gradle & GWT.
- **DI Container:** Compile-time Dependency Injection similar to Dagger2.
- **Reactive:** Implementing a lightweight Observable pattern without heavy libraries.
- **DOM Control:** Handling HTML type-safely with Elemental2.
- Testing: Stop slow browser tests! Verifying frontend logic with JUnit.
Join me on this journey of extending developer productivity from the backend to the browser.
🔗 Links:
- Live Demo: https://sayaya1090.github.io/todo/
- Source Code (GitHub): https://github.com/sayaya1090/todo
메타데이터
- post_id
- 546d697276f3
- slug
- a-backend-developers-daydream-what-if-frontend-felt-like-spring-546d697276f3
- url
- https://medium.com/@sayaya1090/a-backend-developers-daydream-what-if-frontend-felt-like-spring-546d697276f3
- canonical_url
- https://medium.com/@sayaya1090/a-backend-developers-daydream-what-if-frontend-felt-like-spring-546d697276f3
- author_url
- https://medium.com/@sayaya1090
- status
- ok
- fetched_at
- 2026-06-11 10:13:20