{"slug": "the-archaeologist-s-copilot", "title": "The Archaeologist's Copilot", "summary": "A developer modernized a 20-year-old Java 1.5 codebase using AI and Docker, learning that LLMs produce plausible but unreliable answers when not grounded in evidence. The project succeeded by constraining AI with validation in a stable Docker environment and gradual refactoring protected by tests.", "body_md": "# The Archaeologist’s Copilot\n\nRestoration of a 20-year-old Java “Big Ball of Mud” using AI and Docker\n\n*\nThis article explains the approach I used to modernize a Java 1.5 codebase that no longer\nbuilt reliably on modern machines. My early use of LLMs gave me plausible answers that\ndid not hold up in the codebase. Progress came when I grounded the process in evidence,\nusing AI to support analysis, validation in a stable Docker environment, and gradual\nrefactoring protected by tests. The main takeaway is practical: AI was most useful when\nconstrained by evidence, clear roles, and a step-by-step modernization strategy.\n*\n\n16 July 2026\n\n## The “Tourist” Trap\n\nWe all have “that” repository in our organization. The one written in 2005, built with Ant, using Java 1.5. It hasn't been compiled since the Obama administration, and it certainly doesn't run on your new Apple Silicon MacBook.\n\nWhen I inherited this “Brownfield” project, the temptation is to treat Generative AI as a universal translator. I paste the code into an LLM and ask the most natural question in the world: “How do I run this?”\n\nThis is what I call the “Tourist Prompt.” Like a tourist visiting ancient ruins, I am asking for a guided tour and a gift shop souvenir. I want a happy path.\n\nIn our experiment, I tried exactly this. I opened a chat with a standard LLM and asked:\n\n“Hi, I need to start working with this library. Can you please act as a Senior Developer and help me get started? Read the repo and give me a high-level summary... and a simple 'Hello World' code example.”\n\n### The Hallucination of Competence\n\nThe AI acted like a polite, eager-to-please tour guide. It scanned the\n`README.txt`\n\n, ignored the decades of dust, and confidently generated a\nmodern “Starter Kit.” It gave me a pristine `build.gradle`\n\nfile and a\nclean `HelloBlobStore.java`\n\n. It told me exactly how to connect to the\ndatabase. On the surface, it looked like a miracle.\n\nThe AI-generated `build.gradle`\n\n```\nplugins {\n   id 'java'\n}\n\ngroup = 'com.legacycorp.blobstore'\nversion = '1.1'\n\nsourceCompatibility = '1.8'\ntargetCompatibility = '1.8'\n\nrepositories {\n   mavenCentral()\n}\n\ndependencies {\n   implementation 'org.apache.commons:commons-pool2:2.11.1'\n   implementation 'log4j:log4j:1.2.17'\n   testImplementation 'junit:junit:4.13.2'\n}\n\ntest {\n   useJUnit()\n}\n```\n\nThe result was a lie — and a structural lie at that. By generating a modern build file, the AI merely painted a fresh coat of paint over a crumbling structural wall.\n\nFirst, it hallucinated dependencies by suggesting ```\ncommons-pool2\n(v2.x)\n```\n\nwhen the legacy code actually relied on ```\norg.apache.commons.pool\n(v1.x)\n```\n\n. Because these libraries have completely different APIs, blindly\nrunning the AI's code would have crashed the build with “Class Not Found”\nerrors, sending me down a frustrating rabbit hole of debugging “modern”\ncode that was never meant to be modern.\n\nNext came the structural gaslighting. The AI confidently assumed a\nstandard Maven layout `src/main/java`\n\n, completely ignoring the reality\nof a non-standard Ant structure `java/com/legacycorp...`\n\n. It was describing\nthe reality it wanted to see, not the one that actually existed.\n\nFinally, it hid the underlying rot. Its pristine “Hello World” example\nfeatured `PooledBlobStoreImpl`\n\n, omitting the fact that the core\nimplementation `SimpleBlobStoreImpl`\n\nwasn't even thread-safe, the\nerror-handling code routinely swallowed exceptions, and the so-called\n“Unit Tests” were actually integration tests that required a live MySQL\ndatabase to run.\n\n### The Lesson\n\nAI defaults to optimism. When I ask “How do I run this?”, it assumes I can run it. In a restoration mission, optimism is fatal.\n\nIf I had\nfollowed the Tourist path, I would have started refactoring\nimmediately—changing `List`\n\nto `ArrayList<>`\n\nor adding Generics—likely\nbreaking hidden behaviors I didn't fully understand. I would have been\nmaking blind changes to a fragile system without establishing its current\nstate.\n\nTo truly restore a brownfield project, I need to stop acting like Tourists and start acting like Archaeologists.\n\n## Phase I: The Analysis\n\nAfter the “Tourist” prompt failed by offering a modern build that couldn't exist, I realized that what was needed here was not a tour guide, but a construction inspector. I shifted my mental model from “How do I run this?” to “Why did this fail?”. I reset the context with the AI, asking it to be critical rather than helpful.\n\n### The Archaeologist Prompt\n\nI crafted a prompt designed to strip away the optimism. I assigned the AI a specific persona: Senior Legacy Systems Architect. I explicitly forbade it from summarizing the README (which is often a lie in legacy projects) and ordered a “Forensic Code Audit”.\n\n```\nI am conducting a technical due diligence assessment on this legacy Java\nrepository: https://github.com/nikmalykhin/java-blobstore.\n\nAct as a Senior Legacy Systems Architect. Your goal is not to tell me what the\ncode \"does,\" but to evaluate its structural health and \"age.\"\n\nDo not summarize the README. Instead, perform a \"Forensic Code Audit\" focusing\non these four pillars:\n\n1.  **Carbon Dating (The Era):**\n    * Based on syntax (e.g., raw types vs. generics, annotations), imports, and\n    build tools (Ant vs. Maven), estimate the specific Java version (e.g., 1.4,\n    1.5, 6) and the year this code was likely written.\n    * Cite specific lines of code as \"forensic evidence.\"\n\n2.  **Architectural Integrity (The Structure):**\n    * Does it follow standard separation of concerns (Transport vs. Protocol vs.\n    Logic), or is it a \"Big Ball of Mud\"?\n    * Identify any \"God Classes\" that are doing too much.\n\n3.  **Data Flow & Typing (The \"Stringly\" Trap):**\n    * Analyze how data is passed. Is it using proper Domain Objects, or is it\n    relying on \"Stringly-typed\" Maps and raw arrays?\n    * Look for \"Leaky Abstractions\" where protocol details leak into business logic.\n\n4.  **The \"Safety\" Check (Error Handling & Threading):**\n    * Look for anti-patterns in error handling (swallowed exceptions, returning null).\n    * Analyze the threading model. Is `SimpleBlobStoreImpl` thread-safe?\n\nOutput your findings as a structured \"Risk Assessment Report\" for a stakeholder\ndeciding whether to refactor or rewrite.\n```\n\nThe AI dropped the polite facade immediately. Instead of a “Starter\nKit,” it handed me a Risk Assessment Report with a brutal verdict:\n“*critical rewrite recommended*“.\n\n### Finding 1: Carbon Dating the Artifact\n\nThe AI analyzed the syntax like an archaeologist uncovering historical\nstrata, citing evidence rather than guessing. Looking first at the\nhistorical record, it found a `build.xml`\n\nfile without any sign\nof a `pom.xml`\n\n, placing the project squarely in the pre-2010\n“Ant Era.” Next, it flagged key syntax markers, spotting the legacy use of\n`org.apache.commons.pool.ObjectPool`\n\n(Version 1.x) alongside\nraw types like `Map`\n\ninstead of ```\nMap<String,\nString>\n```\n\n. This led to an unmistakable verdict: this was Java 1.5\ncode written during the transition era of 2005–2008, completely predating\nmodern generics, try-with-resources, and standard directory layouts.\n\n### Finding 2: The “Transliteration” Trap\n\nThe most damning insight was that this was actually Perl code\nmasquerading as Java; the original author had simply taken a procedural\nPerl script and forced it into Java syntax. This procedural mindset\nmanifested directly in `SimpleBlobStoreImpl`\n\n, a massive monolithic *god\nclass* that tried to handle everything from low-level socket connections\nand protocol parsing to core business logic. Furthermore, the codebase was\naggressively *“stringly-typed”*. Instead of utilizing proper domain\nobjects like `Device`\n\nor `File`\n\n, the code constantly passed around raw\n`Map<String, String>`\n\nobjects and manually constructed raw protocol\nstrings just to open a file. This introduced an immense operational risk:\na single typo in a string key, such as `get(“fiel_id”)`\n\n, would trigger a\ncatastrophic runtime crash instead of being caught safely at compile\ntime.\n\n### Finding 3: Lying Tests\n\nThe Archaeologist revealed that the perceived test coverage was nothing\nmore than a dangerous illusion. The entire test suite relied heavily on\n`LocalFileBlobStoreImpl`\n\n, a complete re-implementation of the storage\nsystem that wrote directly to the local disk instead of traversing the\nnetwork. While these tests successfully proved that this local mock worked\nflawlessly in isolation, that superficial success masked an alarming\nreality: the actual networking code, the thread-unsafe pooling, and the\nfragile protocol parser—the absolute most volatile parts of the\nsystem—were being completely bypassed.\n\n### The Decision: Containment over Repair\n\nThe report saved me from disaster. Had I followed the optimistic\n“Tourist” advice to refactor `SimpleBlobStoreImpl`\n\nimmediately, I would have\nblindly introduced generics and broken the fragile parsing logic. The\ntests would have still passed—thanks to that deceptive local mock —but the\nactual production code would have been completely non-functional.\n\nRealizing the code was an absolute liability, too fragile to touch and too opaque to trust. I made the strategic decision to halt all active changes. I refused to fix the bugs, update the dependencies, or even reformat the whitespace. Instead, I transitioned directly into a phase of complete containment, wrapping the legacy code inside an isolated, standardized Docker environment before trying to analyze it any further.\n\n## Phase II: The Wrap\n\nWith the audit complete and the “Critical Legacy” label applied, my goal shifted. I didn't want to change the code; I just wanted to run it. If I could get the existing tests to pass, I would have a verifiable baseline. To achieve this, I switched AI persona from Architect to Senior DevOps Engineer.\n\n### The Mission: Brownfield Restoration\n\nFor this phase of brownfield restoration, the core mission was to\nestablish a standardized environment. To guide the AI and actively prevent\ninsidious “modernization creep”, I established a strict set of prime\ndirectives. First, we had to preserve the era, meaning absolutely no\nupdates to the legacy build tools or the Java version—we were strictly\nmimicking the year 2008. Second, I prioritized containment over\nmodernization, keeping the original Ant `build.xml`\n\nfile and running the\nentire process inside an isolated Docker container to avoid polluting my\nhost machine. Finally, there were to be absolutely no code changes; I\nrefused to slap public modifiers onto classes just to fix visibility bugs.\nIf it worked in 2008, it had to work now inside the right container.\n\nYet, despite these strict rules, my first instinct was still secretly\ntainted by the tourist mindset—a classic case of modernizer's hubris. I\ncaught myself thinking, *“Okay, I can't change the Java code, but surely I\ncan swap out this ancient Ant build for Gradle 8, right?”* Acting on that\nimpulse, I asked the AI to perform a swift “lift and shift,” grabbing the\nraw Java 1.5 source files and dropping them directly into a modern Gradle\n8 container.\n\nThe result was a spectacular crash. The build failed not because the\nlegacy code itself was buggy, but because the foundational rules of the\nsoftware environment had radically shifted over two decades. The legacy\ncode routinely relied on accessing package-private classes from entirely\nseparate packages (such as `TestBackend`\n\nreaching into\n`Backend`\n\n). Back in 2008, Ant and Eclipse were incredibly\npermissive about these structural violations; by 2026, Gradle 8 and\nmodern JDKs had become strict, unyielding enforcers of\nencapsulation.\n\nThe Build Failure Log\n\n```\n/src/test/java/com/legacycorp/blobstore/test/TestBackend.java:12:\nerror: Backend is not public in com.legacycorp.blobstore; cannot be accessed from outside package\n        Backend backend = new Backend(trackers, true);\n        ^\n```\n\nFaced with this roadblock, the AI's immediate suggestion was\npredictable: *“Just add public to the class.”* But I refused. Doing so\nwould directly violate one of my prime directive of making zero code\nchanges. Modifying production source code solely to appease a modern build\ntool is a slippery slope, and I wasn't going to step onto it.\n\n### The Pivot: The “Time Capsule” strategy\n\nRealizing that I couldn't stabilize the artifact in a modern environment, I pivoted to a “Time Capsule” strategy. If I wanted to capture this system, I had to build a containment zone that strictly mirrored the standards of 2008. I turned to Docker to recreate the exact environment the code was born in, searching for an old image that bundled Java 6 and Ant 1.5 together.\n\nBut I hit an immediate hardware reality check. The only available Java\n6 Docker images were compiled for x86 (`linux/amd64`\n\n), while I was\nattempting to run the build on a modern Apple Silicon (ARM64) laptop.\nWhile emulation layers like Rosetta or QEMU are theoretically possible,\nthey introduce a dangerous, unpredictable variable into an already fragile\nprocess. If the build fails, how do you know whether it's an inherent code\ndefect or just the emulation layer choking on twenty-year-old\nbinaries?\n\nTo eliminate that variable entirely, I changed my environment. I abandoned the laptop and switched to a native Intel machine powered by a modern i9 processor. The lesson here was clear: sometimes software archaeology requires the right shovel. I only made progress when I stopped fighting the host architecture and moved directly onto the native ground of the artifact.\n\n### The “Wet” Test: Bending Reality\n\nOnce I had the compiler working on Intel—completing the “dry” capsule—I\nfaced the final structural challenge: a stubborn integration test named\n`TestBlobStore.java`\n\n. This “wet” test was a pure artifact of its time,\nlittered with hardcoded assumptions tied directly to the original\ndeveloper's local machine. Specifically, it looked for a magic host,\ntrying to connect to `qbert.legacycorp.com:7001`\n\n, and relied on a magic file\npath located at `~/Projects/blobstore/…`\n\n. In a standard\nmodern refactor, I would have simply deleted these lines. But because I\nwas strictly in containment mode, touching the test file was off the\ntable. Instead of changing the code to fit modern reality, I had to change\nreality to fit the code.\n\nThe solution lay in environment emulation via Docker Compose. I\nprompted the AI to act as a network engineer to help me pull off some\ninfrastructure illusions. First, we executed some network trickery: I spun\nup a modern BlobStore container and used a Docker network alias to trick\nthe test runner into believing this container was actually the long-lost\n`qbert.legacycorp.com`\n\n. Next came the filesystem trickery, where I configured\nDocker volumes to mount our live, local source code directory inside the\ncontainer at the exact, identical path engineer had used back in 2005.\n\nThis environment trick materialized in my `docker-compose.yml`\n\nfile:\n\nThe network configuration.\n\n```\n  services:\n    blobstore:\n      image: hrchu/blobstore-all-in-one:latest\n      networks:\n        default:\n          aliases:\n            - qbert.legacycorp.com  \n  \n    builder:\n      image: blobstore-legacy-builder\n      volumes:\n        - .:~/Projects/blobstore/java/com/legacycorp/blobstore/\n      command: ant test\n```\n\nConfigures the specific network alias and maps the local directory to the expected legacy volume path.\n\nThis orchestrated illusion brought about complete stabilization. When I\nexecuted `docker-compose up`\n\n, the legacy test suite fired up and ran\nflawlessly. It looked up `qbert.legacycorp.com`\n\nand seamlessly routed straight\nto my local Docker container; it reached out for engineer's old hardcoded path\nand found our live volume mount instead.\n\nThe build succeeded. Without changing a single byte of historical source code, I had successfully restored full functionality to a twenty-year-old application. The environment was stable, the code was finally verifiable, and I could at long last think about moving it into the future.\n\n## Phase III: The Lift (Unwrapping the Artifact)\n\nWith the artifact safely stabilized inside the “Time Capsule” of Docker, Java 6, and Ant, I finally possessed a verifiable baseline. I now had concrete proof that the code was fully functional in its native environment, meaning any failures from this point forward would be the direct result of our active modernization efforts, not pre-existing rot. With this safety net firmly established, I began the transition, launching the project fifteen years into the future with the ultimate goal of reaching Java 8 and Gradle.\n\n### The Hardware Rationale\n\nThe choice of Java 8 was not aesthetic; it was a pragmatic necessity\ndriven by my hardware constraints. I needed to run the project natively on\nApple Silicon (ARM64), but that goal crashed into a double-ended technical\nwall. On one side of the timeline, modern JDKs (Java 17+) have dropped\nsupport for compiling legacy Java 1.5 source code entirely, rejecting the\nold `-source 1.5`\n\nflag. On the other side, ancient JDKs like Java 6 refuse\nto run natively on ARM64 architecture, trapping you in buggy emulation\nlayers.\n\nSo I turned to Java 8, the single, specific version capable of satisfying both ends of the timeline. Because it stands as the absolute last version to support the compilation of Java 1.5 targets and one of the earliest versions that can be installed natively on modern Mac hardware, it became our perfect architectural entry point.\n\n### The “Java 17 Trap”\n\nI hit the first hard technical wall when choosing the tool version. My instinct was to use the latest release, Gradle 8.5, but this choice immediately crashed into a wall: Gradle 8 requires Java 17 just to run its internal daemon, and as we already noted, Java 17 is incapable of compiling legacy Java 1.5 source code.\n\nTo resolve this bottleneck, I settled on a pivot to Gradle 7.6. This stands as the absolute last modern-ish Gradle version that can still execute on a Java 8 JVM, allowing me to establish a perfect chain of environmental compatibility:\n\nApple Silicon -> Java 8 JVM -> Gradle 7.6 -> Java 1.5 Source\n\n### The Execution: Mapping the Legacy Structure\n\nI didn't just wrap the old `build.xml`\n\n. Realizing the Ant script was\nactively obscuring the underlying logic, I configured Gradle to map\ndirectly to the legacy directory structure. To achieve native compilation,\nI overrode the modern defaults and explicitly instructed Gradle to look\nfor the source code in `srcDirs = ['java']`\n\ninstead of expecting the\nstandard `src/main/java`\n\nlayout.\n\nNext, I had to tackle the legacy test runner. Because the historical\ntests were structured as old-school `main()`\n\nmethods rather than a modern\nJUnit suite, the standard out-of-the-box `gradle test`\n\ncommand couldn't\nfind them. To bypass this limitation, I wired up a custom `JavaExec`\n\ntask\nnamed `runLegacyTest`\n\nto execute those test entry points manually.\n\nMapping Gradle to the Legacy Layout\n\n```\n  java {\n      sourceCompatibility = JavaVersion.VERSION_1_5\n      targetCompatibility = JavaVersion.VERSION_1_5\n  }\n  \n  sourceSets {\n      main {\n          java {\n              srcDirs = ['java'] \n          }\n      }\n  }\n  \n  tasks.register('runLegacyTest', JavaExec) {\n      mainClass.set(project.findProperty('mainClass'))\n      classpath = sourceSets.main.runtimeClasspath\n  }\n```\n\nConfigures Gradle for Java 1.5 compatibility, maps the source directories to the legacy layout, and registers a execution task for legacy tests.\n\n### The “Lying Tests” Discovery\n\nWith the build modernized to Gradle, the `runLegacyTest`\n\ntask executed\nsuccessfully. But the tests ran suspiciously fast. When I audited the\nsource of `TestBlobStore.java`\n\nto find out why, I discovered a classic\nlegacy anti-pattern: the silent swallow. The code was actively capturing\nfailures and smothering them before they could bubble up to the runtime\nenvironment:\n\nLegacy Code Pattern\n\n```\n  public static void main(String[] args) {\n      try {\n          BlobStore bs = new PooledBlobStoreImpl(...);\n          bs.storeFile(\"test_file\", ...);\n          System.out.println(\"Success!\");\n      } catch (Exception e) {\n          System.out.println(\"Failed: \" + e.getMessage());\n          e.printStackTrace();\n      }\n  }\n```\n\nThe catch block logs the exception but swallows it, so the error isn't noticed.\n\nWhile a human reading the console outputs would easily recognize this as a blatant failure, an automated build tool sees it very differently. Because the exception is caught and handled internally without throwing it further or exiting the program, the process finishes with a perfect exit code 0. These tests were completely misleading; the backend connection could fail entirely, yet our modern pipeline would still confidently report a green pass.\n\n### Hardening the Baseline\n\nTo strip away this false security, I initiated a process of deliberate hardening. I instructed the AI to refactor the old test harness so that it would explicitly throw exceptions all the way up the execution stack. This marked my very first structural change to the legacy codebase, and it was done with a singular purpose: to force my verifiable baseline to become completely honest. Instead of wrapping the operations in an error-smothering blanket, I stripped out the try-catch block entirely and forced the application to crash naturally if something went wrong:\n\nHardened Pattern\n\n```\n  public static void main(String[] args) throws Exception { \n      BlobStore bs = new PooledBlobStoreImpl(...);\n      bs.storeFile(\"test_file\", ...); \n  }\n```\n\nThere's no catch block, so any exception crashes the application.\n\nSuddenly, the build turned bright red. Far from a defeat, this was a massive narrative victory—a red build meant I was finally looking at the unvarnished reality of the system. I spent the next hour tracing down and repairing the broken connection configurations until the build pipeline finally flipped back to green. But this time, it was an honest green.\n\n### The AI-Compiler Feedback Loop\n\nOnce the tests were “honest” and the build turned Green, I was faced with a mountain of technical debt. The build was successful, but the compiler was screaming:\n\n```\nNote: Some input files use or override a deprecated API.\nNote: Recompile with -Xlint:deprecation for details.\nNote: Some input files use unchecked or unsafe operations.\nNote: Recompile with -Xlint:unchecked for details.\n```\n\nTo systematically clean this up, I moved away from general refactoring and established a tight, iterative AI-compiler feedback loop. I didn't just ask the AI to vaguely “fix the codebase”; instead, I used the compiler itself as the ultimate driver.\n\nFirst, I explicitly enabled the `-Xlint:unchecked`\n\nflag inside the\nGradle build configuration to force the compiler to reveal the exact\nsource lines triggering the violations. Whenever the build ran and\ncaptured a specific warning block—such as an unsafe call to a raw type\nlist—I fed those exact error logs directly to the AI with a highly\ntargeted prompt, instructing it to refactor only those specific lines to\nresolve the warnings using modern Java generics.\n\nThis highly localized strategy was incredibly effective at neutralizing historical runtime risks. For example, the original codebase used primitive, Java 1.5-style raw collections where the compiler had no idea what objects actually lived inside them, forcing me to rely on blind, dangerous type casting:\n\nBEFORE: The “Raw Type” Risk (Java 1.4 Style)\n\n```\n  public class Backend {\n      private List hosts;\n      private Map deadHosts;\n  \n      public void reload(List trackers, boolean connectNow) {\n          this.hosts = trackers;\n          this.deadHosts = new HashMap();\n      }\n      \n      InetSocketAddress host = (InetSocketAddress) hosts.get(index); \n  }\n```\n\nThe compiler has no idea what is inside `hosts`\n\nand\n`deadHosts`\n\n. Developers must cast objects blindly, if `hosts`\n\ncontains a String instead of an address, the highlighted line will\ncause a `ClassCastException`\n\nat runtime.\n\nBy passing this exact snippet and its accompanying warning log to the AI, it swiftly updated the architecture to the proper Java 8 type-safe standard, transferring the burden of validation from runtime guesswork to compile-time enforcement:\n\nAFTER: The “Type Safe” Standard (Java 8 Style)\n\n```\n  public class Backend {\n      private List<InetSocketAddress> hosts;\n      private Map<InetSocketAddress, Long> deadHosts;\n  \n      public void reload(List<InetSocketAddress> trackers, boolean connectNow) {\n          this.hosts = trackers;\n          this.deadHosts = new HashMap<>();\n      }\n      \n      InetSocketAddress host = hosts.get(index);\n  }\n```\n\nThe compiler guarantees that `hosts`\n\nonly contains\nInetSocketAddresses. No casting required. Zero runtime risk.\n\nBy maintaining this disciplined, repetitive cycle across every file, I eventually crossed the finish line with a successful build and absolutely zero warnings. The historic artifact wasn't just functional; it was officially standardized.\n\n## Phase IV: The Refactor\n\nAlthough I had successfully unwrapped the historical artifact, it\nremained fundamentally disorganized. The core codebase was still locked in\na convoluted, non-standard `java/com/...`\n\nfolder structure, and its test\nsuite still consisted of primitive, standalone `main()`\n\nscripts. To make\nmatters worse, the source code itself was completely riddled with raw\ntypes—a legacy artifact of the Java 1.5 era that forced developers to cast\nobjects blindly and constantly exposed the system to unpredictable runtime\ncrashes. Yet, with a modern build chain now humming and a hardened safety\nnet finally secured beneath me, I was at long last equipped to transition\nfrom strict containment into a full-scale architectural renovation.\n\n### Mastering the Craft\n\nBefore attacking the production code, I had to fix the workbench. I\nstarted with a much-needed phase of sanitization, moving the source files\nout of their archaic `java/`\n\nroot folder and into the industry-standard\n`src/main/java`\n\nlayout. Making this shift allowed me to delete my previous\ncustom Gradle directory workarounds entirely; by finally bowing to\nstandard conventions, the build tool simply worked out of the box.\n\nWith the project's skeleton straightened out, I tackled a comprehensive\nJUnit 5 migration to convert the primitive legacy `main()`\n\nscripts—such as\n`TestBackend`\n\nand `TestBlobStore`\n\n—into genuine unit tests. Throughout the\nimplementation, I systematically swapped out the old, crude\n`System.out.println(“Error”)`\n\ntraps for proper `Assertions.assertEquals()`\n\nstatements. This immediately paid off with a deeply satisfying result: I\ngained granular, automated test reporting, permanently freeing me from\nhaving to manually audit endless text logs just to check if a test had\npassed in favor of receiving a standard, unambiguous green checkmark.\n\n### The TestContainers Trap\n\nI got ambitious and considered replacing the manual docker-compose\nsetup with `TestContainers`\n\nto make the tests truly self-contained, but the\nattempt quickly collapsed. The migration rapidly degenerated into a messy\n“Big Bang” refactor—I found myself trying to overhaul the test runner, the\nnetwork topology, and the startup logic all at once, while simultaneously\nwrestling with complex Docker-in-Docker networking issues on ARM\narchitecture.\n\nThis friction taught me a vital engineering lesson: *momentum is\noxygen*. The moment I realized I was spending all my energy fighting the\ntooling rather than recovering the actual code, I made the conscious\ndecision to abort the experiment. I gladly accepted the “External Sidecar”\npattern—running `docker-compose up`\n\nmanually—because it was reliable and\nit worked, deliberately choosing ground-level pragmatism over\nover-engineered perfection.\n\n## The Final Sweep: Concurrency & Stress Testing\n\nI had successfully unwrapped the artifact and hardened its core, but\ntwo final loose ends remained before I could confidently declare the\nproject's restoration complete. First, there was a forgotten sibling:\n`LocalFileBlobStoreImpl.java`\n\n. This legacy mock implementation desperately\nneeded to be updated to implement our brand-new, generic-based `BlobStore`\n\ninterface. Second, I had to address the ultimate proof of our\narchitecture: `StoreALot.java`\n\n, a multi-threaded load-testing tool buried\ndeep within the historical repository.\n\nThese files mattered immensely because they held the keys to verifying\nour concurrency rules. If the pooling logic inside `PooledBlobStoreImpl`\n\nwas even slightly misaligned, `StoreALot`\n\nwould immediately crash with a\n`ConcurrentModificationException`\n\nor succumb to silent race conditions. To\nprove that my modernizations were actually thread-safe, I needed to\noverhaul these files and push them to their absolute limits.\n\nTo execute this final performance engineering phase, I prompted my AI\ncopilot to act as a senior performance engineer. Together, we\nsystematically modernized the old load-testing script, cleaning up its raw\nsyntax with generics and modern loggers while ensuring it remained\nexecutable. I instructed the AI to configure the test runner to target our\nbackend using `PooledBlobStoreImpl`\n\nto hit the Docker container alias at\n`qbert.legacycorp.com:7001`\n\n. Finally, we swapped out the primitive, manual\nthreads for a modern `ExecutorService`\n\nto guarantee the system could\nelegantly handle a parallel load without buckling under concurrent\nexceptions.\n\nWe had modernized the core API. Now we must verify thread safety.\n\n- Modernize the Load Test: Refactor\n`StoreALot.java`\n\n. It’s currently a main script; keep it executable but clean up the syntax with Generics and modern Loggers. - Target the Backend: Ensure it uses\n`PooledBlobStoreImpl`\n\nto hit the Docker container alias (`qbert.legacycorp.com:7001`\n\n). - Concurrency Verification: Run with\n`ExecutorService`\n\ninstead of manual threads. Handle parallel load without throwing`ConcurrentModificationException`\n\n.\n\nThis rigorous orchestration yielded the definitive empirical proof I\nneeded. I launched the stress test, firing 100 iterations across 10\nconcurrent threads directly at my Docker-contained BlobStore backend. The\nresults were crystal clear: the application's entire thread-safety\narchitecture successfully relied on `PooledBlobStoreImpl`\n\n—utilizing Apache\nCommons Pool—to seamlessly provision isolated backend instances to each\nactive thread. By verifying this behavior under intense, simulated\nreal-world conditions, I confirmed that our deep modernizations—the\ngenerics, the JUnit migration, and the structural collection swaps—had not\ndestabilized the core historical logic.\n\nI had finally done it. I took a twenty-year-old piece of code archaeology that was completely uncompilable, untestable, and broken, and transformed it into a modern, thread-safe, and fully containerized Java 8 library.\n\n## Conclusion: The Handover\n\nA software restoration mission is never truly finished just because an artifact suddenly becomes functional; it is only complete when it meets a clear, unyielding definition of what it means to be done. For this legacy project, that milestone wasn't about achieving theoretical perfection, but rather about bringing the system to a specific, verifiable state where the code was completely runnable, testable, and predictable on modern hardware. By clearing that precise bar, I successfully transformed the repository from an opaque archaeological mystery into something much more familiar and manageable: standard technical debt.\n\n### Scrubbing the Environment\n\nTo ensure the next developer doesn't have to repeat my tedious\narchaeological dig, I switched my AI persona one last time to act as a\nlead repository maintainer. With this final objective in mind, I\nidentified and systematically purged every historical artifact that\nbelonged firmly to the past. First went `build.xml`\n\n, the legacy Ant script\nthat had dictated the repository's rules for decades. Next, I emptied out\nthe old `lib/`\n\nfolder—permanently discarding a loose bag of unversioned,\nhardcoded JARs—and swept away `.classpath`\n\nand `.project`\n\n, which were\nnothing more than abandoned artifacts from long-forgotten IDE setups.\nRunning `rm build.xml`\n\nstood as the final, cathartic act of modernization;\nit officially severed our fragile link to the ancient Ant era and\npermanently forced the repository to rely on my modern Gradle engine.\n\n### The Project Roadmap: README.md\n\nI didn't just leave behind a clean repository; I left a map. Working\nwith the AI, I generated a comprehensive `README.md`\n\nfile that perfectly\nreflects this new, standardized reality. Instead of an undocumented\nlabyrinth, the file outlines a completely frictionless path to\nproductivity, specifying basic prerequisites like Docker and Java 8+, and\noffering a dead-simple quick start that builds the project with a single\n`./gradlew build`\n\n. Testing the entire infrastructure is now just as\nstraightforward, requiring a quick `docker-compose up -d`\n\nto spin up the\nbackend dependencies followed by a standard `./gradlew test`\n\n. This single\ndocument completely transforms the project from an intimidating mystery\nbox into a predictable, standard Java library, permanently shifting the\nexperience for the next engineer from a grueling forensic investigation to\na routine, standard onboarding.\n\nThe Transformation: Before vs. After\n\n| Feature | Day 0 (The Archive) | Day N (The Product) |\n|---|---|---|\n| Build System | Ant | Gradle 8 |\n| Compiler | Java 1.5 | Java 8 |\n| Environment | “Works on my machine” | Docker |\n| Testing | Manual Scripts | JUnit 5 |\n| Safety | Runtime Risk | Compile-time Safety |\n| Confidence | Swallowed Exceptions | Hardened Tests |\n| Onboarding | “Good luck figuring it out” | README.md |\n\n### Final Thought: The Augmented Archaeologist\n\nThe most important lesson from this experiment was fundamentally about human agency. When I initially leaned on the helpless “Tourist Prompt”—vaguely asking the machine to “fix this for me”—the entire attempt collapsed because the AI lacked a foundational understanding of the environment and the rigid constraints of the past. Success only arrived when I fluidly shifted mindsets to direct the execution: acting first as an archaeologist to identify the true architectural decay, then as a DevOps engineer to design the containerized time capsule, and finally as an architect to define a strict refactoring policy.\n\nThe AI didn't magically restore this historical system on its own; rather, I restored it by wielding the technology as a powerful force multiplier. It took care of the tedious, repetitive translation layers—churning through the conversion from Ant to Gradle, drafting the Dockerfiles, and systematically squashing fifty distinct compiler warnings—while I focused entirely on high-level strategy. Because of this active partnership, the codebase is no longer an intimidating, tangled black box. It has become entirely runnable, testable, and predictable—fully equipped to endure the next ten years and perfectly positioned for whatever future refactoring awaits it.\n\n## Acknowledgments\n\nThanks to Matteo Vaccari for the series of articles on AI-assisted modernization. They were a key inspiration for the idea of using AI-powered refactoring for truly old software, such as this Java 1.5 code.\n\nMany thanks to Martin Fowler for his feedback and guidance throughout the writing process. His help made this article clearer and, hopefully, more readable.\n\nI used AI in this article. I started by using Gemini to highlight the key moments from my experiment and turn my notes into an outline. Then I used it to help draft sections from that outline, which I reviewed, commented on, and revised by hand. Finally I used AI for a pass on flow and grammar. The experiments, conclusions, and final wording were all reviewed and edited by me and GitHub Copilot.\n\n## Significant Revisions\n\n*16 July 2026: *published", "url": "https://wpnews.pro/news/the-archaeologist-s-copilot", "canonical_source": "https://martinfowler.com/articles/archaeologist-copilot.html", "published_at": "2026-07-17 15:58:43+00:00", "updated_at": "2026-07-17 16:21:40.287863+00:00", "lang": "en", "topics": ["large-language-models", "developer-tools", "artificial-intelligence"], "entities": ["Docker", "Java", "Ant", "Gradle", "Maven", "LLM", "Apple Silicon MacBook"], "alternates": {"html": "https://wpnews.pro/news/the-archaeologist-s-copilot", "markdown": "https://wpnews.pro/news/the-archaeologist-s-copilot.md", "text": "https://wpnews.pro/news/the-archaeologist-s-copilot.txt", "jsonld": "https://wpnews.pro/news/the-archaeologist-s-copilot.jsonld"}}