Back to Blog
Case StudyVB6 MigrationLegal

How We Migrated a VB6 Legal Case Management System Without One Hour of Downtime

April 20, 2026 | 22 min read

Executive Summary

In late 2024, a US law firm with approximately 200 attorneys across four offices came to us with a problem they'd been deferring for a decade. Their case management system — the software their attorneys and paralegals used to track matters, deadlines, documents, and billing — was built on Visual Basic 6 in the early 2000s. The developer who built it had retired. VB6 had been out of Microsoft support for years. The underlying SQL Server 2008 database was also reaching end of support. Two of the four offices had experienced data corruption incidents in the previous 18 months, each requiring manual recovery from backups.

They needed a migration. They were terrified of disrupting billable work. Every day the system was unavailable was a day they couldn't enter time, couldn't check deadlines, couldn't invoice clients.

This is the story of how we executed that migration: zero hours of downtime, full business logic preserved, all 200 users switched over without a training gap, and a codebase that went from 287,000 lines of VB6 to 91,000 lines of TypeScript.

The client has approved this case study with identifying details anonymized. Dollar figures and specific case types are omitted. This is a representative example of a class of engagement we run regularly: multi-office law firms, insurance companies, and financial services organizations that have been running business-critical VB6 applications for 15 to 25 years and need a migration path that doesn't require risking the business to execute.

The Legacy System — Before State

The system they showed us was, in some ways, impressive. It had been in continuous production for 23 years. Over that time, it had accumulated functionality that wasn't in any documentation: billing rules that differed by matter type, deadline calculations that accounted for court-specific calendar exceptions, a report generator that could produce 47 different output formats. One module generated formatted letters with variable content — lawyers drafted the templates, the system filled in party names, matter numbers, and dates.

It was also held together with approaches that are the hallmark of long-lived VB6 systems: global state everywhere, error handling via On Error Resume Next (which silently swallowed errors and continued), shared module-level variables that were set in one subroutine and read in another with no documented contract between them.

The SQL Server 2008 database had 340 tables. Roughly 80 had fewer than 20 rows and appeared to be legacy configuration tables from features that had been removed. Approximately 60 stored procedures contained business logic: billing calculations, deadline scheduling, conflict checks.

The team using the system ranged from legal secretaries who had been using it for 15 years to associates who had started six months ago. Power users had keyboard shortcuts memorized. Any migration that required relearning workflows would face resistance.

Why They Moved Now

Three forces converged to make migration unavoidable.

SQL Server 2008 end of support had reached the point where their cyber insurance carrier flagged it as an unacceptable risk. Patching the known vulnerabilities required upgrading, and upgrading SQL Server 2008 with VB6 data access code is not straightforward — the VB6 database drivers in use were incompatible with newer SQL Server authentication modes.

Two of the four offices had been hit with ransomware in the previous 18 months. Neither attack succeeded in encrypting production data, but both were close. The old VB6 system ran on a Windows 2012 server that itself was end-of-life and could not be patched against the specific exploit used in the second attack.

The firm had recently added a fifth office through acquisition. The acquired office had their own case management software. The merger required either migrating everyone to the VB6 system (undesirable, given its state) or migrating everyone to something new.

MIRROR Phase: What Does This Thing Actually Do?

We start every migration with a MIRROR phase, and for this engagement it ran for eight weeks. The goal is not to read the source code — the source code tells you what was written, not what is correct. The goal is to understand what the running system actually does.

We ran the MIRROR phase in parallel across three tracks.

Business rule documentation. We sat with five power users — two partners, two senior paralegals, and one billing administrator — and walked through every workflow they considered critical. We were looking for business rules that were encoded nowhere: the billing rate that applied to pro bono matters after the first ten hours, the court deadline calculation rule that added three days for service by mail and five for out-of-state parties, the conflict check logic that flagged matters involving the same corporate family even when the counterparty name was different.

We found 34 business rules that were not in the source code documentation (such as it was) and not in any user manual. They lived in the institutional memory of the power users. Several had originated as manual corrections to wrong output years ago and had never been codified anywhere except in habit.

Production data analysis. We ran read-only queries against the production database looking for implicit constraints: columns that were always null, columns where the documented value range didn't match actual values, foreign key relationships that weren't enforced at the database level but were enforced in application code (or were assumed to be and weren't). We found three data integrity issues that the firm didn't know existed: roughly 400 matters with inconsistent status flags, a handful of time entries with negative durations (from a date calculation bug that had been quietly present for years), and 12 client records that appeared twice under different IDs.

Output comparison baseline. For every report type the system could produce, we generated sample output from the production system and saved it. This gave us a regression baseline: after migration, we could produce the same report from production data and compare output exactly.

MODEL Phase: Building the TypeScript Skeleton

The MODEL phase is where we build the TypeScript type system and test suite before writing a line of implementation code.

Every entity in the VB6 system gets a TypeScript interface. Matter, Client, TimeEntry, BillingRate, CourtDeadline, ConflictCheck. The interfaces encode the constraints we found in MIRROR: the status field for a matter is not a string, it's a MatterStatus union type limited to valid statuses. A billing rate is not a number, it's a BillingRate object with a rate-per-hour field, a matter ID, and a rate type that distinguishes standard from pro bono.

The 34 undocumented business rules become test cases. Each one gets a failing test before any implementation. A sample test comment for the deadline calculation rule:

Deadline calculation — mail service adds 3 days, out-of-state adds 5. Discovered in MIRROR phase, not in original source code documentation.

We write the test to match the behavior of the running VB6 system, including behavior that looks wrong but turns out to be correct. The negative-duration time entries got a test: the system should reject them on input but display a warning rather than refusing to show existing entries. The decision to handle historical wrong data gracefully rather than hiding it came from a conversation with the billing administrator, who knew about the bad records and had been manually accounting for them.

At the end of the MODEL phase, we had 847 test cases across 12 test suites. All of them failing — we had no implementation yet, only interfaces and tests.

MIGRATE Phase: Module by Module

We divided the system into 14 functional modules, sized to be completable in two-to-three sprint cycles each. The order was driven by dependency: modules that other modules depended on went first. Client and matter management came first. Deadline calculation came second (it depends on matter type and court assignments, which live in client/matter). Billing came last (it depends on everything).

Each module followed the same cycle:

First, we implemented the module in TypeScript until all tests in that module's suite passed. We did not consider a module complete until the test suite was entirely green.

Second, we deployed the module behind a feature flag set to "old" — meaning all users still hit the VB6 code path. The new TypeScript module ran in shadow mode, processing every real request in parallel and logging its output alongside the VB6 output.

Third, for two to four weeks (depending on the module's criticality), we compared outputs. When the TypeScript output matched VB6 output in production, we noted it. When they diverged, we investigated every divergence. Most divergences were bugs we'd fixed. A few were bugs we hadn't found yet. One was a case where the VB6 behavior was intentional and we'd modeled it incorrectly.

Fourth, when the divergence rate was below our threshold (we used less than one per thousand transactions over a five-day rolling window), we flipped the feature flag to "new." The old VB6 code path continued to run in shadow mode, now logging VB6 output for comparison.

Fifth, after ten business days on the new code path with no production issues, the feature flag was removed and the old code path was deleted.

Over nine months, we migrated all 14 modules. No user ever experienced a service interruption.

MONITOR Phase: Parallel Run

After the fourteenth module was migrated, the VB6 system was still running. Not serving requests — the TypeScript system was handling everything — but still running, still connected to the database in read-only mode, still available to generate output for comparison if needed.

We ran a 60-day parallel run. Both systems processed every transaction. At the end of each day, we ran a comparison job that looked for any divergence in matter status, time entry totals, billing calculations, and deadline computations. For the first two weeks, we found small divergences — two billing rate calculation differences in unusual edge cases, one deadline computation that was off by one day for a specific court that had an exception we hadn't found in MIRROR.

By day 15, the divergence rate was zero. It stayed zero for the remainder of the 60-day run.

At the end of the parallel run, we decommissioned the VB6 system. It has not been started since.

What Broke (And Why It Was Good News)

The most significant issue we found during migration was a date calculation in the deadline module that the firm had been unknowingly working around for several years.

The VB6 system calculated response deadlines for certain motions by counting calendar days and then subtracting weekends. The calculation was off by one day for motions filed on Fridays after 5 PM. The workaround — which nobody had consciously decided on, it had just emerged as practice — was that paralegals would add one day to any deadline they set on Friday afternoons.

When our TypeScript implementation calculated Friday-afternoon deadlines correctly, the shadow-mode comparison flagged divergences. We investigated, found the bug, documented it, and fixed it in TypeScript. We also informed the firm. They were not pleased to learn this had been wrong for years, but they were very pleased to have a correct system going forward.

This is the reason we run MIRROR before writing code. If we had accepted VB6 behavior as ground truth and replicated it exactly, we would have migrated a bug into production. The combination of rigorous documentation and shadow-mode comparison found the bug and allowed us to fix it during migration rather than after.

Numbers

Timeline: 11 months from start of MIRROR to end of parallel run.

Lines of code: 287,000 VB6 → 91,000 TypeScript (68% reduction).

Test coverage: 847 test cases, all passing at end of migration.

Downtime: Zero. No user experienced a service interruption during migration.

Data integrity: The three data integrity issues found during MIRROR were corrected in the database as part of the migration. No new data integrity issues were introduced.

Production issues post-migration: Two minor bugs found in the first 30 days, both in edge cases in the billing module, both resolved within 24 hours, neither caused data loss.

The Data Problem

Production data analysis is the most uncomfortable part of any legacy migration. Nobody wants to discover that their database has integrity issues. But discovering them during migration — when you can fix them with a known clean state — is vastly better than discovering them after migration, when the integrity of the new system becomes suspect.

The three issues we found in this engagement were representative of what we typically find in long-running VB6 systems.

The 400 matters with inconsistent status flags were the most common type: records where a workflow transition had partially completed. A matter had been closed, but one of the closure steps — updating the billing status or the conflict check flag — had failed silently (due to On Error Resume Next, the VB6 error was swallowed) and the matter was left in a half-closed state. These were not causing visible problems because the VB6 system checked the primary status field and ignored the secondary flags that were inconsistent. The new TypeScript system's stricter data model would have surfaced these immediately as type errors, so we resolved them before cutover.

The handful of time entries with negative durations came from a date calculation that had gone wrong under a specific edge case: time entries entered spanning midnight. The VB6 code calculated the duration as end time minus start time, which returned a negative number when someone entered 11:30 PM to 12:30 AM. Most timekeepers had learned not to do this, but a few historical entries existed. We corrected the durations by inspecting surrounding entries and applying the obvious correction.

The 12 duplicate client records were a people problem, not a code problem. Two attorneys at different offices had entered the same client independently, resulting in parallel matter histories under different client IDs. We merged these during migration. The new system's client creation workflow includes a duplicate check that prevents recurrence.

None of these issues would have been discovered by migrating the code. They required querying the data directly with the intent of finding inconsistency.

Lessons Learned

Power user time is a migration asset. The 34 undocumented business rules we found in MIRROR came from power users. This knowledge was not in the source code, not in documentation, and would have been lost if we had approached the migration purely as a code-to-code translation. Budget time with the people who know the system, not just the people who built it.

Shadow mode is non-negotiable. The VB6 system running in shadow mode for 14 module cycles found more bugs than code review. The combination of real production data and automated output comparison is more effective than any amount of test case design. We've attempted migrations without it. The defect escape rate in production was significantly higher.

Undocumented behavior is not wrong behavior. Several patterns in the VB6 codebase that looked like bugs turned out to be intentional accommodations to real business constraints. The date calculation bug was a genuine bug. But a billing rate rounding rule that rounded up to the nearest six-minute increment — which looked like a floating-point error — was intentional policy. The difference between a bug and a policy is not visible in the code. It requires asking.

The fifth office absorbed cleanly. The acquired office migrated to the new TypeScript system without going through the VB6 system at all. Their data was imported during the MIGRATE phase, and they went live on the TypeScript system while the VB6 migration was still in progress for the original four offices. The modular migration made this possible.

User training happened naturally. We were asked before the engagement how we planned to handle user training — 200 attorneys and staff learning a new interface. The answer was that we didn't plan a formal training program. Each module was migrated independently, behind a feature flag, so users who had a choice could self-select into the new interface one module at a time. By the time the final cutover happened, approximately 80% of users had already been using the TypeScript system for at least two modules. The remaining 20% — mostly the least-frequent system users — switched with minimal friction because their workflows were common enough to be self-evident.

Scope creep comes from features, not scope. The engagement ran two months over the initial 9-month estimate. The overage came entirely from requests to add new features during migration. The firm recognized that with a modern system being built, this was the right time to add the client portal module they'd wanted for years. We scoped and executed the additions, but the lesson is that the initial estimate covered migration, not new development. Separate these clearly in the contract and the expectation-setting.

Involve a partner in each office. The engagement had a designated partner at each office who was responsible for communicating the migration plan internally, gathering user feedback during the shadow runs, and approving module cutovers. This made the project feel like a firm initiative rather than an IT project imposed from outside. That organizational support was worth more than any technical decision we made.

What Comes Next

The firm now has a TypeScript application running on a modern stack. The application is deployed as a containerized web service accessible from any device, including mobile. SQL Server has been upgraded to a supported version. The application runs in Docker containers on their existing server infrastructure, which simplified their IT management. They're currently building a client portal — a feature they couldn't have added to the VB6 system at any cost.

If you're running a VB6 system in a legal context and the risk profile is starting to sound familiar, we're glad to have a technical conversation about what migration would look like for your system.

Read more about our VB.NET and VB6 migration practice →

For legal-specific concerns — matter confidentiality during migration, IOLTA account handling, bar association compliance questions — see our legal vertical page →

Contact us to discuss your system →

Need This Kind of Engineering?

We build the systems we write about. Let's talk about your project.