+52 55 6809 1145

How we deconstructed an 8-year-old Vaadin application with an AI and resurrected it as a modern SPA without rewriting a single line of business logic.

The Starting Point: The Golden Cage

Night has fallen. My colleague and I are sitting there, and in the center of my screen is an old Vaadin 8 project that we haven't been able to compile...

We've all been there. You have a legacy application. It's robust, it gets the job done, and for years, it was the backbone of the business. Whether it's still running daily or, in my case, had been left dormant on an old PC, the feeling is the same: you're dealing with a golden cage. Mine was a condominium management application built on Vaadin 8—a true work of engineering in its time, with a complex, data-rich UI built entirely in Java

The problem with golden cages is that, while comfortable, they are still cages. The coupling between the UI and the backend was total. The first and most frustrating obstacle was simply trying to get the project to compile. We didn't have the local Maven repository, the famous .m2 folder, which contained the original dependencies. When trying to download them again, we hit a wall:

  • Some dependencies in the pom.xml simply no longer existed in public repositories. They were digital ghosts.
  • Others appeared online, but the Vaadin repositories from that era were unstable, and the download calls would simply fail.
  • Our natural reaction was to start "patching" the pom.xml. We tried upgrading versions, downgrading versions, looking for replacements... and every change made things worse. We fell into a dependency hell from which it was impossible to escape.

 

It was at that point of frustration, with a project we couldn't even build, that we made a radical decision. If we couldn't revive the monolith, why not extract its soul—the business logic—and build it a new body?

The Strategy: Deconstruct, Don't Destroy

Our strategy was based on one premise: separate the UI from the backend as surgically as possible. We weren't going to rewrite the business logic. We were going to set it free. The plan was divided into three clear phases, almost like a surgical act.

Phase 1: AI-Assisted Replication (The Static Clone)

This is where things get interesting. Instead of manually translating thousands of lines of VerticalLayouts and TextFields from Java to HTML, we decided to use a sharper tool: an AI-based coding assistant, in this case, we used Gemini Code Assist.

The goal was to create a static HTML/CSS/JS prototype that was 100% faithful to the existing UI. The process was a dialogue, a man-machine collaboration:

  1. Structural Analysis: I asked the AI to analyze the Vaadin source code. Its first task was to identify the main navigation structure (TabSheet) and all the nested views.
  2. Component Translation: The AI then translated the Vaadin components into their standard web equivalents. A VerticalLayout became a <div class="layout-vertical">, a Button became a <button>, and a Grid became a <table>.
  3. Modularization (The Key Lesson): My first impulse was to generate a single, monstrous HTML file. Mistake. We quickly realized we needed modularity. The AI, under my direction, created "partials" (_admon.html, _finanzas.html) that were loaded dynamically with fetch(). This not only kept the code clean but also laid the groundwork for a future migration to a component framework like React or Vue.

 

The result of this phase was a pixel-perfect visual mock-up, created in a fraction of the time it would have taken manually. And most importantly: a file I put together, promptIA.txt, a master prompt that became a reusable asset for future migrations.

Phase 2: Freeing the Backend (The Headless API)

With the UI replicated and isolated, the Java backend, which previously served HTML, now had a new purpose: to serve data. Since the project already used Spring, the path was natural: Spring Web.

For every Vaadin UI controller (e.g., PrivadasCRUDController.java), we created a twin: a @RestController (e.g., PrivadasRestController.java). The beauty of this approach is that we reused 100% of our service classes (AdminService, IngresosService). We didn't touch the business logic. We just put a REST facade on it.

Suddenly, we had endpoints like /api/privadas and /api/residentes. Our monolithic backend had become, de facto, a headless API.

Phase 3: The Spark of Life (Connecting the Wires)

This was the final phase. We had a body (the HTML mock-up) and a brain (the REST API). It was time to connect them.

Using vanilla JavaScript, we implemented a simple but effective pattern in home.html:

  1. Load and Link: When a tab was clicked, the corresponding HTML partial was loaded.
  2. Link Function: Immediately after, a specific function for that module was executed (e.g., linkAdmonEvents()).
  3. async/await Magic: Inside that function, fetch called our new API endpoints to populate the tables. The addEventListener on the "Save" or "Delete" buttons collected the form data, sent it to the API with POST or DELETE, and then refreshed the table.

 

Seeing the first table populate with real data from the backend was the "Eureka!" moment. The cage had been opened.

Lessons Learned from the Trenches

This was not a path without obstacles. Here are the most raw and valuable lessons:

What Worked (and why you should copy it):

AI as an Accelerator: Don't underestimate the power of an AI for code "translation" tasks. The key was prompt engineering. A well-structured and detailed prompt is the difference between a mediocre result and one that saves you weeks of work. Our promptIA.txt is now pure gold.

Incremental Migration: Don't try to boil the ocean. Isolating the UI first and then adding the API layer is a low-risk approach that delivers value quickly.

Post-Migration Cleanup: Once everything was working, the most satisfying step was deleting the Vaadin dependencies from the pom.xml. It was a symbolic and practical act that lightened the project and solidified the new architecture.

What Went Wrong (and how we fixed it):

Local CORS Hell: Our first attempt to use fetch with local files (file:///) crashed head-on into browser security policies. The lesson: Always, always, serve your frontend files from a local web server during development, even if it's as simple as python -m http.server or the "Live Server" extension in VS Code.

The Vanilla JavaScript Trap: While connecting everything with vanilla JS was a great exercise, it quickly became apparent that for an application of this scale, it's a recipe for chaos. The practical advice: Consider this step a bridge. The final destination should be a component framework (React, Vue, Angular). Our mock-up is now the perfect foundation to start building those components.

Careless Compilation Errors: After removing the Vaadin dependencies, the project failed to compile because we forgot to remove the .java files from the old UI. The lesson: The migration isn't over until the obsolete code is removed. Be ruthless.

Contenido del artículo
Resulting Layout

Conclusion: The Future is Decoupled

What began as a technical migration turned into a strategic transformation. We now have an application whose interface can evolve at the speed the market demands, while our robust Java backend continues to do the heavy lifting, intact and more relevant than ever.

If you're trapped in your own golden cage, remember this: you don't have to demolish it. You can, with the right strategy and the right tools, simply open the door.

I'm sharing my promptIA.txt here for you to use and adapt to your needs. If it helps, buy me a coffee!

Nuestros clientes: