The Archaeologist's Copilot: AI for Legacy Java Modernization

Inheriting a Java 1.5 codebase built with Ant? Stop pasting it into an LLM. Nik Malykhin's case study shows the archaeological method: audit, contain in a 2008 time capsule, then modernize with the compiler as feedback.

axonn bots
axonn bots
·5 min read
Nik Malykhin's case study on modernizing a Java 1.5 codebase with AI walks through the archaeological method: start with a Forensic Code Audit, not a starter kit; build a Docker-based time capsule to run the original artifact in its native environment; use the compiler as the feedback loop for incremental refactoring. The core lesson is that AI is most useful when constrained by role, evidence, and step-by-step strategy, not when used as a one-shot translator. The post also includes a reusable prompt for the audit phase and details the time capsule build that made the rest of the work possible.

The Tourist Prompt is a trap

Most engineers who inherit a Java 1.5 codebase from the Ant era do the same thing first. They paste the repo into an LLM, ask how to run it, and wait for the magic. The LLM is happy to oblige. It scans the source, ignores the decades of structural rot, and confidently returns a modern Gradle build file, a clean starter class, and a Hello World example. On paper, this looks like a miracle. In practice, the LLM has just painted a fresh coat of paint over a crumbling wall.

Malykhin calls this the Tourist Prompt. Like a tourist visiting ruins, you are asking for a guided tour and a gift shop souvenir. You want a happy path. The model wants to give you one. The codebase doesn't actually have a happy path, and the first generation phase hides that fact.

The result is structural gaslighting. The LLM invents dependencies that weren't there. It assumes standard Maven layouts that don't exist. It hides the fact that the test suite is actually a pile of integration tests that need a live MySQL to run. If you follow the path, you will start refactoring fragile code, introduce generics that break parsing logic, and trust a green build that is lying to you.

Switch roles: from tourist to archaeologist

The first move that actually works is to stop asking "how do I run this?" and start asking "why did this fail?" Reset the context. Tell the model to be critical, not helpful. Hand it a role: Senior Legacy Systems Architect. Forbid it from summarizing the README, which is usually the worst kind of source of truth in old projects. Ask instead for a Forensic Code Audit across four pillars: era estimation, architectural integrity, data flow, and safety.

Malykhin's prompt template is worth saving. The output is a Risk Assessment Report, not a starter kit, and the report calls out the bad news: this is Java 1.5 code from 2005 to 2008, written in a procedural style, aggressively stringly typed, with tests that mock out the actual networking code. The compiler never had a chance to validate the system, and the test suite confirms only that the local mock works, not that the production system does.

Containment before modernization

Once the audit lands, the rule is: do not change the code. Not even whitespace. The job of phase two is to build a Time Capsule that runs the artifact in the environment it was born in. For a 2008-era Java app, that means a Docker image with Java 6 and Ant 1.5, run on a native Intel machine, with the right hostnames and filesystem paths recreated via Docker Compose aliases and volume mounts.

The reason this is non-negotiable: if the code doesn't run in its native environment, every future failure is ambiguous. You cannot tell whether the bug is in the code or in your emulation. Malykhin tried running a 2008 build on an Apple Silicon Mac through emulation. It almost worked. Almost is worse than failing, because it generates a stream of false signals. He moved to a native Intel box, recreated the original hostname, mounted the original path, and got a clean test run. Only then did he have a baseline.

The compiler as a refactoring driver

With a green baseline, the next step is to let the compiler do the talking. Enable -Xlint:all and -Werror. Run the build. Feed the warning blocks back to the LLM with surgical instructions. The LLM is now in a feedback loop where the compiler enforces every change, and the LLM only proposes changes the compiler will accept. This is the difference between blind refactoring and auditable refactoring.

Raw types become List<InetSocketAddress> and Map<InetSocketAddress, Long>. The compiler now refuses to let a String sneak into the host list. The runtime ClassCastException risk moves to compile time. The same pattern applies to deprecated APIs, raw socket handling, swallowed exceptions, and any of the other thousand small paper cuts that legacy Java carries.

The lesson: roles, evidence, and incremental trust

The generalizable lesson is not "use AI to refactor legacy code." It is that AI is a force multiplier only when the human sets the role, the evidence requirement, and the iteration cadence. Malykhin's process had four distinct personas, each with a tightly scoped job: Architect for the audit, DevOps Engineer for the time capsule, Architect again for the refactor policy, Performance Engineer for the load tests. The AI handled the tedious translation. The human handled the judgment.

The codebase at the end is a Java 8 library with Gradle, JUnit 5, type-safe collections, and a Docker Compose setup that spins up a real backend. The same code now runs natively on a modern laptop, builds in seconds, and tests in milliseconds. None of that happened because an LLM rewrote the project. All of it happened because the engineer used an LLM the way an archaeologist uses a trowel: as a tool, in service of a method.