Legacy Migration

Moving Old Systems to Modern Platforms


Somewhere in your organisation, there's an old system. Maybe it's a PHP application from 2005 with register_globals still enabled. Maybe it's an Access database that's grown beyond all reason, with VBA modules nobody dares touch. Maybe it's a collection of Excel spreadsheets held together by hope and macros, where the formulas reference cells in files that no longer exist. We see this pattern constantly, and it's usually a sign that spreadsheets have reached their limits.

These systems run critical business processes. They contain years of valuable data. And they're increasingly difficult to maintain, secure, and build upon. Migration is necessary, but it's also risky. Done wrong, you lose data, break processes, and frustrate everyone involved. Done poorly, you end up with a new system that doesn't actually do what the old one did, and a team that quietly reverts to their spreadsheets within six months.

Done right, migration delivers modern, maintainable systems while preserving everything valuable from the old ones. The key word is "everything". Not just the obvious functionality, but the edge cases, the business rules embedded in obscure conditional statements, and the data that's been accumulating since before anyone can remember why.


The Constraint: Why Legacy Migration Fails

Most migration projects underestimate complexity by a factor of three to five. A system that looks simple from the outside (a few forms, a database, some reports) typically contains years of accumulated business logic. The original developers solved problems as they arose, patching and extending rather than rewriting. The result is software archaeology: layers of code representing different eras of the business, different developers, different assumptions about how things work.

The constraint isn't technical. Moving data from MySQL to PostgreSQL is straightforward. Rewriting PHP 5 code in Laravel 11 is mechanical. The constraint is knowledge: understanding what the old system actually does, why it does it, and what happens when specific edge cases occur.

Hidden complexity

Legacy systems accumulate business logic over years. Some of it is documented. Most isn't. Edge cases that the original developer handled are invisible until you try to replicate them. That obscure IF statement checking for customer_type = 7? It exists because Scottish VAT rules were different for three months in 2012, and some records from that period are still in the system.

Data quality issues

Old databases accumulate garbage: duplicate records, orphaned data, inconsistent formatting, fields used for unintended purposes. The "notes" field contains phone numbers because the phone field wasn't long enough. The "status" column has 47 different values, 31 of which haven't been used since 2015. Migration surfaces problems that were hidden.

Undocumented dependencies

Other systems might read from the legacy database directly. Reports might depend on specific field formats. Users have workarounds that nobody remembers creating. The accounting system pulls data at 2am via a scheduled task nobody knows about. Change the database schema, and you break three other systems silently.

Institutional knowledge gaps

The developer who wrote the system left in 2014. The operations manager who knew all the workarounds retired last year. Nobody knows why customers in region 4 are handled differently, but if you change it, invoices come out wrong. The knowledge exists only in the code, and the code doesn't explain itself.


The Naive Approach: Big Bang Rewrites

The tempting approach is the clean slate: analyse the old system, write a specification, build the new system from scratch, migrate the data, and switch over. Friday you're on the old system; Monday you're on the new one. Start fresh, do it properly, leave the legacy mess behind.

This approach fails predictably. The specification misses edge cases because nobody remembers they exist. The new system works perfectly for the 80% of cases that everyone thought about, then fails catastrophically for the 20% that nobody documented. Users lose functionality they depended on. Data doesn't migrate cleanly. The go-live weekend becomes a go-live month, then a go-live quarter.

The 80/20 trap: Most systems handle 80% of cases with 20% of the code. The remaining 20% of cases require 80% of the code. Big bang rewrites typically replicate the easy 80% and discover the hard 20% in production.

The problems compound:

  • Parallel development divergence: While you're building the new system, the old system keeps changing. Features get added. Data structures get modified. By the time you're ready to migrate, you're migrating from a different system than you analysed.
  • Accumulated small differences: A hundred small decisions in the old system (date formatting, rounding rules, sort orders) were made for reasons nobody remembers. The new system makes different decisions. Each one is trivial; together, they break user expectations.
  • No rollback path: Once you've migrated 50,000 customer records to the new schema, you can't easily go back. Changes made in the new system don't exist in the old one. Your rollback window closes hours after go-live.

We've seen big bang migrations succeed exactly once, and that was a simple inventory system with 6,000 records and two users. For anything more complex, you need a different strategy.


Migration Strategies: Choosing the Right Pattern

The right migration strategy depends on system complexity, risk tolerance, and how long you can sustain parallel operations. There's no universal answer, but there are well-understood patterns with predictable trade-offs.

Big bang

Build the new system completely, then switch over in one go. On Friday you're on the old system; Monday you're on the new one.

When it works: Simple systems, small datasets, limited users, short go-live window acceptable

Pros: Clean cut, no need to maintain parallel systems, no synchronisation complexity

Cons: High risk, all problems surface at once, limited rollback options, stressful go-live

Phased migration

Migrate module by module. Move customer management first, then orders, then billing. Old and new systems coexist during transition.

When it works: Modular systems, clear boundaries between subsystems, when continuous operation is critical

Pros: Lower risk, problems are contained to each phase, learn from early phases

Cons: Need integration layer between old and new systems, longer overall timeline, complexity of dual-running

Strangler fig pattern

Gradually replace functionality. New features are built in the new system. Old features are migrated over time. Eventually the old system has nothing left and can be retired.

When it works: Complex systems, risk-averse organisations, when there's no hard deadline

Pros: Lowest risk, continuous delivery of value, each change is small and reversible

Cons: Can take years, requires discipline to finish, sustains two codebases longer

Parallel running

Run both systems simultaneously, processing the same data. Compare results to verify the new system behaves correctly.

When it works: Financial systems, regulatory environments, when correctness is more important than speed

Pros: High confidence in correctness, issues detected before they affect users

Cons: Expensive (double the processing), complex data reconciliation, operational overhead

The Strangler Fig in Detail

The strangler fig pattern deserves special attention because it's the safest approach for complex legacy systems. Named after the tropical fig that grows around a host tree, eventually replacing it entirely, the pattern works by incrementally replacing pieces of the old system until nothing remains.

The implementation typically involves:

1

Facade layer

Place a routing layer in front of the legacy system. All traffic passes through this layer. Initially, it routes 100% to the old system. This establishes the interception point without changing behaviour.

2

Feature-by-feature migration

Implement one feature in the new system. Route traffic for that feature to the new system, while everything else continues hitting the old system. Test thoroughly. Repeat.

3

Data synchronisation

During the transition, data must stay consistent between systems. Changes in the new system may need to propagate to the old, and vice versa. This is the hardest part and requires careful design.

4

Progressive traffic shifting

For each feature, you can route a percentage of traffic to the new system (canary releases). Start at 5%, monitor for errors, increase to 25%, then 50%, then 100%. Problems are detected before they affect everyone.

5

Decommission

When no traffic hits the old system for a sustained period, you can safely retire it. Keep it available read-only for a transition period, then archive.

The strangler fig extends timelines but dramatically reduces risk. Each step is small, testable, and reversible. You never bet the business on a single go-live weekend.


Data Migration Patterns

Data migration is where most projects discover how messy reality is. The source database contains records from before validation was implemented, fields repurposed over the years, and encoding issues from when someone imported a CSV with the wrong character set in 2009. Understanding your target data model before you start makes transformation rules clearer.

Understanding the Source

Before writing any migration code, you need to understand what you're actually migrating. This means querying the data, not just looking at the schema.

1

Profile every column

For each field: distinct value count, null rate, min/max lengths, value distribution. A "status" field might have 47 values when you expected 5. A "required" field might be null 30% of the time.

2

Identify orphaned records

Find records that reference non-existent parents. Orders for customers that were deleted. Line items for orders that don't exist. These will fail foreign key constraints in a properly modelled system.

3

Map field purposes

Document what each field actually contains vs. what it was designed for. The "notes" field containing phone numbers. The "fax" field containing mobile numbers. The "spare1" field containing anything.

4

Date and time issues

Check for timezone handling. Are dates stored as UTC? Local time? Mixed? Are there impossible dates (31st February, 0000-00-00)? How are null dates represented?

The ETL Pipeline

Migration code should be structured as Extract-Transform-Load (ETL), with each stage independently testable. Never write migration scripts that do everything in one pass. You need to re-run them, debug them, and verify them.

Stage Purpose Key Principle
Extract Pull data from source in original format No transformation. Preserve exactly what exists, including garbage.
Transform Clean, validate, and reshape data Log every transformation. Record what was changed and why.
Load Insert into target system Validate against target constraints. Fail loudly on errors.

Transformation Rules

Transformations must be explicit and documented. Each rule should be a discrete, testable unit. Examples:

  • Normalise phone numbers: Strip spaces and dashes, standardise to E.164 format, flag invalid numbers for manual review
  • Handle missing required fields: Set default value, or create placeholder record, or quarantine for manual processing
  • Merge duplicate records: Define matching criteria (email address? company name + postcode?), define merge rules (which record is the survivor? how are conflicts resolved?)
  • Date handling: Convert all dates to UTC, handle null dates consistently, validate against realistic ranges
  • Encoding fixes: Convert to UTF-8, handle malformed sequences, strip control characters

Quarantine tables: Records that fail transformation should go to a quarantine table, not disappear. Include the original data, the error reason, and a timestamp. Manual review and correction is part of the process.

Validation and Reconciliation

After migration, you must prove the data moved correctly. This means running reconciliation queries:

  • Record counts: Do the counts match? Account for any records deliberately not migrated.
  • Aggregate sums: Do monetary totals match? Are there rounding differences?
  • Sample verification: Select random records and manually verify they migrated correctly.
  • Edge case checks: Verify specific records you know are problematic (maximum values, special characters, oldest records).
  • Referential integrity: Do all foreign keys resolve? Are parent-child relationships intact?

Build these checks into the migration pipeline. Run them automatically after every migration. Any discrepancy halts the process for investigation.


Preserving Business Logic

Business logic is the hard part. Moving data is mechanical; understanding why the system behaves the way it does requires detective work.

Extraction Techniques

Logic hides in multiple places. You need to check all of them:

Application code

Read it line by line. Every conditional statement represents a business decision. Document what each branch does and why. Pay special attention to exception handling (what happens when things go wrong is often business logic).

Database triggers and stored procedures

Logic in the database layer is easily overlooked. Triggers that update audit columns, stored procedures that calculate pricing, constraints that enforce rules. All of this must be captured.

Scheduled tasks and batch jobs

What runs overnight? End-of-day processing? Month-end closures? Report generation? These are often critical business processes that aren't visible in the main application.

Configuration and lookup tables

Tax rates, pricing tiers, discount rules, territory mappings. Sometimes the real business logic is in reference data, not code.

User Interviews

Users know things that aren't in the code. Interview the people who use the system daily:

  • What do you do on a typical day?
  • What workarounds have you developed?
  • What situations require special handling?
  • What would break if we changed X?
  • Who else should we talk to about this?

Ask about edge cases specifically. "What happens when a customer is also a supplier?" "How do you handle returns?" "What's different about orders from region 4?" Users will often reveal logic that nobody documented because it seemed obvious at the time.

Historical Bug Reports and Change Requests

Old bug reports are a gold mine. Each one represents a scenario where the system didn't behave as expected. The fix implemented represents a business rule. Read through years of change history if you can access it.

Pay particular attention to:

  • Bugs that were fixed by adding conditional logic
  • Changes requested by specific departments
  • Workarounds that became permanent features
  • Patches applied during specific periods (tax changes, regulatory updates)

Risk Mitigation During Migration

Migration projects fail when risks materialise without contingency. Risk mitigation is not about avoiding risk; it's about knowing what will go wrong and having responses prepared.

Common Risks and Mitigations

Risk Mitigation Contingency
Data loss Full backup before migration, verified restore tested Restore from backup, re-run migration with fixes
Extended downtime Rehearsal migrations with realistic data volumes, time estimates Defined abort criteria, partial rollback procedure
Missing functionality User acceptance testing before go-live, feature parity checklist Hotfix prioritisation, temporary workaround procedures
Integration failures End-to-end testing with connected systems, rollback API versions Fallback API endpoints, manual processing procedures
Performance issues Load testing with production data volumes, performance benchmarks Scaling resources, query optimisation on standby
User resistance Early involvement, training before go-live, super-user network Extended parallel running, dedicated support

The Rollback Plan

Every migration needs a rollback plan, even if you never intend to use it. The plan must answer:

  • Trigger criteria: What conditions would trigger a rollback? Be specific. "Too many errors" is not a trigger. "More than 50 failed transactions in the first hour" is.
  • Decision authority: Who can authorise a rollback? Can the on-call engineer decide, or does it need a project sponsor?
  • Rollback window: How long do you have? Once users start entering data in the new system, rolling back becomes harder. Define the point of no return.
  • Data synchronisation: If you roll back, how do you handle data entered in the new system? Does it need to be migrated back to the old system?
  • Communication plan: Who needs to be told? How quickly? What's the message?

Test the rollback: A rollback plan that hasn't been tested is a hope, not a plan. Run through the rollback procedure before go-live, on a test environment, to verify it actually works.


Maintaining Operations During Transition

For most businesses, you cannot simply stop operations during migration. Orders keep coming. Customers keep calling. The business must continue to function while you replace its operating system.

Dual-Running Strategies

During phased migration, you'll have periods where both systems are operational. This creates complexity, and you'll need solid integration patterns to keep data synchronised:

Read from old, write to both

The old system remains the source of truth. Writes go to both systems. This allows the new system to accumulate data while the old system runs. Risk: the systems can diverge if synchronisation fails.

Read from new, write to new, sync to old

The new system becomes primary for a module. Critical data syncs back to the old system for dependent processes. Good for phased migration. Risk: sync lag creates temporary inconsistency.

Feature flags per user

Some users get the new system, others stay on old. Useful for pilot groups and gradual rollout. Tools like LaunchDarkly manage this complexity. Risk: user confusion, support complexity, two sets of training.

Shadow mode

The new system processes everything but its output is discarded. Results are compared with the old system to verify correctness. No user impact during verification. Risk: doubles processing load.

Change Freeze Periods

Define periods where no changes are made to the old system. This prevents the target from moving while you're migrating. Typical approach:

  • Hard freeze: No changes to the old system at all. Usually 2-4 weeks before go-live.
  • Soft freeze: Only critical bug fixes to the old system. Changes must be replicated to the new system.
  • No freeze: Changes continue in both systems. Requires sophisticated synchronisation. Avoid if possible.

Support During Transition

Plan for increased support load during and after migration:

  • Extended hours for IT support during go-live week
  • Super-users in each department trained on the new system
  • War room for the first few days (key personnel available for rapid response)
  • Clear escalation paths for different types of issues
  • Pre-written communication templates for common scenarios

Testing and Validation

Testing a migration is not the same as testing new software. You're not verifying that the system works; you're verifying that it works exactly like the old one did, including all the quirks and edge cases that nobody documented.

Test Categories

1

Functional parity testing

For each function in the old system, verify the new system produces identical results. Same inputs, same outputs. Document any intentional differences.

2

Data integrity testing

Verify migrated data is complete and accurate. Record counts, aggregate checks, sample verification, referential integrity.

3

Integration testing

Verify connected systems still work. APIs return expected formats. File exports have correct structures. Scheduled jobs complete successfully.

4

Performance testing

Run with production data volumes. Verify response times are acceptable. Identify bottlenecks before they affect users.

5

User acceptance testing

Real users, real scenarios, real data. Not IT verifying the system works; business users confirming it supports their work.

6

Regression testing

As fixes are applied, verify they don't break other things. Maintain a suite of tests that run after every change.

Production Data for Testing

Test with production data, not fabricated test data. Fabricated data doesn't include the edge cases, the garbage, and the weird records that break assumptions. There are considerations:

  • Data privacy: Mask or anonymise personal data before copying to test environments. Names, addresses, financial details.
  • Data volume: Test with full production volumes. A query that takes 100ms on 1,000 records might take 10 minutes on 1,000,000.
  • Data age: Include old records, not just recent ones. The record from 2008 with the weird encoding is the one that will break your import.
  • Refresh frequency: Keep test data reasonably current. Stale test data means you're not testing against what you'll actually migrate.

Parallel Result Comparison

For critical functions, run the same operation on both systems and compare results:

  • Process the same invoice through both systems. Compare the amounts.
  • Run the same report. Compare the figures.
  • Calculate the same pricing. Verify they match.

Automate this comparison. Build tools that highlight differences. When results don't match, investigate whether it's a bug, a data issue, or an intentional improvement.


Common Migration Scenarios

Each type of legacy system presents specific challenges. Here's what to expect for the most common migrations we perform:

Access to web application

Common for small businesses that outgrew desktop databases:

  • Export Access data to SQL format (watch for data type mismatches)
  • Handle Access-specific date handling (null dates, especially)
  • Migrate VBA logic to server-side code
  • Replicate Access forms in web UI (users expect similar layouts)
  • Handle multi-user properly (Access concurrency is limited)
  • Enable remote access and collaboration
Excel to proper system

When spreadsheets become too complex:

  • Inventory all spreadsheets (there are always more than you think)
  • Identify the "master" version (often there are multiple conflicting copies)
  • Extract formulas and document the calculations
  • Decide which data to preserve (not all of it is valuable)
  • Design input forms that match user mental models
  • Preserve ability to export to Excel (users still want spreadsheets)
Old PHP to modern framework

Upgrading from legacy PHP:

  • Extract business logic from presentation (often interleaved)
  • Handle global state and session dependencies
  • Replace raw SQL with parameterised queries
  • Implement proper authentication (old PHP auth is often insecure)
  • Add proper error handling (old code often fails silently)
  • Structure for maintainability (tests, separation of concerns)

The .NET to Laravel Migration

We see this increasingly as .NET Framework applications age and organisations want to move away from Windows Server licensing:

  • Database: SQL Server to PostgreSQL is straightforward for most queries. Watch for T-SQL specific syntax, stored procedures, and SQL Server data types (hierarchyid, geometry, etc.).
  • Business logic: C# translates reasonably well to PHP. Class structures, dependency injection patterns, and service layers map across. Watch for strong typing assumptions.
  • Authentication: Windows authentication needs replacement. Plan for SSO integration if users expect single sign-on.
  • Reporting: SSRS reports need replacement. Consider PDF generation, or move to a browser-based reporting approach.

After Migration: Stabilisation and Decommissioning

The Hypercare Period

The first 4-6 weeks after go-live are critical. Issues that testing missed will surface. Users will encounter scenarios nobody anticipated. Plan for intensive support:

  • Extended support hours: Key personnel available outside normal hours
  • Rapid response: Issues triaged and addressed within hours, not days
  • Daily stand-ups: Quick check-ins to surface and prioritise issues
  • User feedback channel: Easy way for users to report problems and suggestions
  • Performance monitoring: Dashboards showing system health, response times, error rates

Decommissioning the Old System

Don't switch off the old system immediately. It contains historical data, provides a reference for comparison, and serves as a fallback if critical issues emerge.

Go-live

New system primary

Week 4

Old system read-only

Month 3

Access by request only

Month 6

Archive and retain backups

Year 1

Decommission infrastructure

Before decommissioning:

  • Verify all data is accessible in the new system or archived
  • Confirm all integrated systems are using the new endpoints
  • Take final backups and store them securely (consider retention requirements)
  • Document where historical data now lives
  • Update any documentation or runbooks that reference the old system

Continuous Improvement

Migration is not the end. It's the beginning of the new system's lifecycle. Use the post-migration period to:

  • Gather user feedback and prioritise improvements
  • Address performance issues that emerge under real load
  • Build the features that were descoped to meet go-live deadlines
  • Clean up technical debt accumulated during the migration rush
  • Document the new system properly (you'll migrate again someday)

The Business Case

Migration is expensive. It consumes months of effort, distracts from new feature development, and carries genuine risk. Why do it?

Maintenance burden

Old systems get harder to maintain. Developers who know the codebase leave. Dependencies become unsupported. Security patches stop. Every change takes longer and risks breaking something. The burden grows until the system is effectively unmaintainable.

Opportunity cost

Time spent nursing the old system is time not spent improving the business. Features competitors are shipping, you can't. Integrations partners expect, you can't support. The cost isn't just maintaining the old; it's what you can't do while you're maintaining it.

Risk accumulation

Old systems accumulate risk: security vulnerabilities, single points of failure, knowledge leaving the organisation. A system running on Windows Server 2008 is a security incident waiting to happen. An Access database on one person's machine is a disaster waiting to happen.

Scaling limits

Legacy systems hit ceilings. Access can't handle concurrent users. Old PHP can't handle modern traffic. The architecture that worked for 10 users fails at 100. Growth requires migration eventually; the question is whether you do it proactively or in crisis.

The right time to migrate is when the cost of not migrating exceeds the cost of migration. That calculation includes the ongoing maintenance burden, the opportunity cost, the accumulating risk, and the approaching scaling limits. Wait too long, and migration happens in crisis mode, with less time, more pressure, and higher risk of failure.


What You Get

  • Preserve your data Everything valuable moves to the new system, cleansed, structured, and verified
  • Capture hidden logic Business rules extracted from code, user interviews, and historical changes before rebuilding
  • Minimise disruption Migration approach matched to your risk tolerance and operational requirements
  • Maintain operations Business continues running throughout transition, with clear dual-running strategies
  • Provide rollback options Tested contingency plans ready if something goes wrong
  • Validate thoroughly Comprehensive testing against real data before go-live
  • Support the transition Training, documentation, and intensive support during hypercare period

Modern systems replacing legacy headaches, without losing what made the old system valuable. Migration done properly, once, so you don't have to do it again in two years.

Legacy System Pain Assessment

Check the issues that apply to your legacy system.

How Much Pain Is Your Legacy System Causing?

Modernise Your Legacy System

We migrate legacy systems to modern platforms. Old PHP, Access databases, Excel spreadsheets held together by macros, ageing .NET applications: moved to maintainable, secure, extensible systems. Your data preserved, your business logic captured, your team supported through the transition. Migration strategy matched to your risk tolerance and timeline.

Let's talk about your legacy system →
Graphic Swish