Michel Moreira

Modernizing legacy VB6 batch processing with AI agents

2026 - present · VB6, Java 25, Spring Batch, Spring Boot 4, SQL Server, Testcontainers, REST, JWT, MCP, SSH, Azure DevOps

Problem

Legacy VB6 batch processes at Techne, launched as .exe executables by a Java interface. There are 97 of them, and that volume had made a rewrite unfeasible for years: too large for a big bang, and too risky to touch without a safety net, because the behavior has to stay identical while the system runs in production. It was the kind of project that sat in a drawer for years.

Approach

BEFOREAFTERJava interfaceVB6 .exeSQL Server(2 legacy databases)spawns .exeJava interfaceREST API + Spring BatchJava 25 · Boot 4 · chunk=1SQL Server (dual)session proxy = VB 1:1POST /executions (JWT)202 + jobExecutionIdGET /{id} · GET /{id}/log
From a UI-spawned .exe to a Spring Batch job triggered over REST.

Instead of rewriting everything at once, my deliverable was the architecture, the skeleton, and the migration process, so the rewrite becomes an incremental track a team can execute. Three decisions hold it together.

The first is a versioned architectural contract in the repository: a canonical catalog that maps each VB process to its target Java package and prevents naming collisions, plus a per-process migration roadmap. The work is structured for AI agents, with reusable skills and access to the real database via MCP, so the agents validate against the actual database rather than guesses.

The second is the safety net: characterization tests. The original VB binary runs on Windows via SSH and PowerShell and produces the golden output, .csv files and semantic logs; the Spring Batch job has to reproduce the same output for the same input. The parity required is functional, at the domain level: the same business output for the same input, with timestamp differences out of scope. Seven scenarios, from the happy path to idempotency, a comparison of 6 business fields per record, and a semantic log comparison that normalizes timestamp and encoding and matches each line to its equivalent VB log category. It all runs against a real SQL Server via Testcontainers.

VB6 .exe(Windows)expected/*.csv + semantic logsORACLE (golden)Spring Batch JobTestcontainers · SQL Server 2022producescompares 6 fields per record+ normalized semantic log7 scenarios (C1-C7) · idempotencybecomes a parity gate in CIruns via SSH · same input params
The VB binary produces the golden output; the Spring Batch job must reproduce it, and any divergence fails CI.

The third is fidelity where it matters and divergence where it pays off. The step uses chunk-size=1, one transaction per record, to preserve the VB commit and rollback semantics. Data access is plain JdbcTemplate, no ORM, to keep the queries faithful to the originals. A DataSource proxy replicates the legacy per-connection session bootstrap, so the old system’s security views keep behaving the same way. The divergence is deliberate: skip-limit=100, so one problematic record does not abort the whole batch the way it did in VB.

Delivery replaces OS-process spawning with REST. The consuming interface creates an asynchronous execution, the POST returns 202 and an identifier, polls the status, and retrieves the persisted log over HTTP, with JWT authentication. The service runs in a container, Java 25 on Alpine, published by an Azure DevOps pipeline.

Technology

Java 25 LTS, Spring Boot 4, and Spring Batch 6 at the core. SQL Server accessed through JdbcTemplate, no ORM; Flyway only for the Batch metadata schema; Testcontainers for the characterization tests; JWT authentication; container packaging (eclipse-temurin:25-jre-alpine) with an Azure DevOps pipeline and the image in Azure Container Registry. On the AI side, a versioned contract for agents, reusable skills, and MCP to the real SQL Server.

Result

The architecture, the skeleton, and the migration method are in place, validated end to end on 1 of the 97 backlog processes, with a characterization gate that proves parity before any merge. That is what took the rewrite out of the drawer: what was an unfeasible big bang became an incremental track, with a contract and a safety net, that the team runs one process at a time. Developers are already being allocated to migrate the rest of the backlog against the same contract and the same gate.

It is not finished, and that is the point: I delivered the path, not the whole migration. The cost of the track is concrete: each process requires capturing the original binary’s golden output on a Windows host and keeping a real SQL Server in the test loop.