Back to blog

Legacy System Examples and Where Each One Hides Its Rules

Eight legacy system examples from ordinary businesses, the exact places each one hides its rules, jobs and logins, and a one-page discovery sheet.

Luka Abramovic18 min read

Everyday business systems that have quietly become difficult to change

Legacy system examples in a 40 to 300 person business are rarely mainframes. They are an Access database that runs a department, a spreadsheet that became a system, a server nobody restarts and an integration nobody can explain. Age doesn't make them legacy. What does is that nobody currently understands them well enough to change them safely. So the first job is discovery, and it goes fastest when you know where each kind of system hides its work. An Access database keeps its rules in six places, and holding Shift while you open a copy stops its startup macro from running while you look. Below are eight examples, where each one hides its rules, jobs and logins, and a one-page sheet to record what you find.

1. The Access database that runs a department

A desktop database on a shared drive, built years ago by a capable member of staff, now holding a whole team's scheduling, stock or quality records. Two documented limits catch these out. An Access file can't exceed 2 GB, system objects included (Access specifications, as of September 2026), and attachments stored in records fill that fast. And Microsoft's sharing guidance is to split the file into a back-end for data and a front-end copy per user, and not to use it across a wide area network, where it is slow and can become corrupt. Opening it from home over a VPN is that case.

Access hides business rules in six places. Work on a copy of the file, and of the back-end file if the database is split:

  1. Field properties. Open each table in Design View and read the Validation Rule and Default Value of every field. The table's own property sheet can also hold a record validation rule that compares two fields.
  2. Action queries. Update, append, delete and make-table queries change data in bulk, run from a button, a macro or code. Each one is a business rule written as a query.
  3. Form code. VBA behind form events, especially Before Update, which can check a record and refuse to save it.
  4. Macros. A macro named AutoExec runs before anything else when the file opens. Holding Shift while you open the copy bypasses it, so nothing runs while you inspect.
  5. Data macros. These fire when a record is added, changed or deleted, whichever screen made the change. Open each table in Datasheet view and look at the Table tab: an event button shown highlighted already has a macro attached.
  6. Linked tables. External Data > Linked Table Manager lists every other Access file, workbook, SharePoint list or SQL Server database the front end reads.

Then run Database Tools > Database Documenter, pick All Object Types and save the report as a PDF. It prints the design of every object, including properties and code (Microsoft's instructions). Database Tools > Object Dependencies shows, for any table, which queries, forms and reports use it. Check the Access version while you are there: Access 2016 and 2019 left support with the rest of Office 2016 and 2019 on October 14, 2025, and Office LTSC 2021 follows on October 13, 2026.

Do this first: give the Documenter PDF and your notes on the six places to anyone quoting a rebuild, and ask which of those rules they plan to keep.

2. The internal tool built by someone who left

A web app, a set of scripts or a reporting tool built in-house. It works, and the person who built it has moved on. The departure is what made it legacy: every change now starts with an investigation, and much of the risk sits in accounts that still belong to that person. Run this handover audit before anyone touches the code.

Handover audit for a tool whose author has left
CheckWhere to lookWhat should worry you
HostingThe cloud or hosting account the tool runs in: whose email signs in, whose card paysA personal email or card, or a single admin
Domain and DNSThe registrar account for the domain, and wherever DNS records are managedRenewal notices going to the leaver's inbox, or auto-renew on an expired card
CertificatesThe certificate's expiry date (visible in the browser's certificate details) and whatever renews itRenewal done by a script on a laptop that has gone
CodeThe repository host and its admins, and whether the deployed version matches the latest commitThe only copy is on the server, or in a personal account
Scheduled jobsCron, Task Scheduler or the cloud provider's scheduler (the server inventory in example 6 lists them)Jobs that run under the leaver's account
SecretsPasswords and API keys in code and config files, including old commitsKeys that were never changed after the departure

For the last row, and to see whose knowledge left, a developer can run these two commands in a copy of the repository:

# Commits per author, most first, with email addresses
git shortlog -sne --all

# Every commit that added or removed something that looks like a secret
git log --all -i -G "password|secret|api[_-]?key|token" --format="%h %ad %an %s" --date=short

If one name owns most of the commits, the gaps in your understanding are that person's gaps. The second command matters because a password deleted from the code still sits in the repository's history, readable by anyone with a copy, so it still needs changing.

Do this first: move every account in the table to a company-owned role address with at least two admins, then change every secret the second command finds.

3. The ERP customization that blocks every upgrade

Your ERP was modified years ago for something specific. Vendor updates would break the modification, so updates stopped. SAP's name for the goal is a clean core: standard vendor code, with extensions kept outside it so that upgrades and extensions don't break each other. Its companion, fit-to-standard, makes every deviation from the vendor's standard process justify itself. Both tests work on any ERP once you have the list of changes. Where to find it on two common mid-market ERPs (menu paths as of September 2026):

  • NetSuite. Customization > Scripting > Scripted Records lists every record type with a script or workflow attached. Customization > Scripting > Script Deployments shows which scripts are deployed and on what, and each deployment's Execution Log subtab shows recent activity at its log level. Customization > Workflow > Workflows lists workflows, and Customization > SuiteBundler > Search & Install Bundles > List shows installed bundles.
  • Business Central. The Extension Management page lists every installed app and its publisher. Anything Microsoft didn't publish is either a third-party app or a customization written for you.

If you run Business Central online, "we just won't upgrade" has an end date. Microsoft checks per-tenant extensions against each coming major release and emails the environment's notification recipients about incompatibilities. An incompatible extension blocks the update, and after the grace period Microsoft may uninstall extensions that block it so the update can go ahead (update cycles, as of September 2026). Check that the notification recipients in the admin center include someone who will act on that email.

Do this first: put every item into one sheet with four columns: what it does in one sentence, who asked for it, when it last ran, and a decision (retire it, rebuild it outside the ERP as an extension or integration that survives updates, or keep it and accept the upgrade cost). Anything nobody can explain is a candidate to switch off in a sandbox while you watch what changes.

4. The spreadsheet that became a system

A workbook with macros, lookup tables and sheets nobody opens. It calculates pricing, plans capacity or tracks work in progress, and its file name ends in a version number and someone's initials. Nobody decided to build a system, so nobody designed one, and the business rules ended up as numbers typed into formulas. Excel hides them in six places:

  • Numbers inside formulas. Press Ctrl+` (the key above Tab) to show formulas instead of results. Every number that isn't a cell reference, like the 1.07 in =B2*1.07, is a rule: a markup, a tax rate, a cut-off.
  • Typed-over calculations. Home > Find & Select > Go To Special > Constants selects every cell someone typed a value into. On a sheet that should be all formulas, each one is a manual override.
  • Named ranges. Formulas > Name Manager (Ctrl+F3) lists them, except hidden names, which Name Manager does not display.
  • Very hidden sheets. A sheet set to "very hidden" through VBA doesn't appear in the Unhide dialog at all.
  • Links to other workbooks. Data > Queries & Connections > Workbook Links (Edit Links in older versions) lists every file this one reads. A link to a file on someone's desktop is a dependency on that person.
  • What feeds what. Select a key input and use Formulas > Trace Dependents to see every cell it changes.

To list hidden sheets and hidden names, press Alt+F11 to open the VBA editor, then Ctrl+G to open the Immediate window. Paste each line and press Enter. It only reads the workbook that is currently active:

For Each s In ActiveWorkbook.Sheets: Debug.Print s.Name, "very hidden: " & (s.Visible = xlSheetVeryHidden): Next
For Each n In ActiveWorkbook.Names: Debug.Print n.Name, "hidden: " & (Not n.Visible), n.RefersTo: Next

The Inquire add-in's Workbook Analysis builds a similar inventory as a report, but as of September 2026 Microsoft offers Spreadsheet Inquire only in Excel for Windows with Microsoft 365 Apps for enterprise or equivalent editions, so the Business plans don't have it.

Do this first: copy every hard-coded number and hidden object onto a new sheet called Rules, write what each means in one sentence, and have the workbook's owner confirm or correct it. Where those rules should live afterwards is the question our integrate, replace or build guide answers.

5. Software from a vendor that dropped support

A product still in daily use whose vendor was acquired, discontinued the line or moved everyone to a successor you haven't adopted. Nothing changes day to day, which is the danger: only the support has ended, and you find out what that means during an incident. The question that matters is whether you can get your data out complete, and "it exports to CSV" doesn't answer it. Run a test export this month and check four things:

  1. Row counts. Count records per entity (customers, orders, jobs) in the export and compare them with the totals the application shows. If they differ, look at archived, closed and deleted-but-kept records first.
  2. Attachments. Check that the files themselves came out, not only their names, and open a sample.
  3. History. Check whether who changed what, and when, came out at all. An export of current values can't settle a dispute or satisfy an auditor.
  4. Custom-field definitions. You need the list of allowed values for each custom field and what each value means, not just the codes stored against each record.

Then read the contract for a source-code escrow clause. If there is one, ask the escrow agent three questions: when was the last deposit made, has anyone ever built the software from it, and do the release conditions cover the vendor discontinuing the product, or only insolvency? A deposit from four versions ago that nobody has built won't get you running again.

Do this first: load the test export into an empty database or workbook and reconcile the four checks against the live system. Once you know the export is complete, leaving the vendor becomes a scheduled project instead of an emergency.

6. The on-premise server nobody touches

A physical or virtual machine in a closet or rack, running something important, last restarted a long time ago. Years of small adjustments mean its real configuration exists only on the machine. Capture it before anything else with a read-only inventory. Your IT person or managed service provider runs this in PowerShell as an administrator, on Windows Server 2012 or later. It changes nothing and writes CSV files to C:\inventory:

$out = "C:\inventory\$env:COMPUTERNAME"
New-Item -ItemType Directory -Force -Path $out | Out-Null

Get-CimInstance Win32_OperatingSystem | Select-Object Caption, Version, LastBootUpTime |
  Export-Csv "$out\os.csv" -NoTypeInformation
Get-CimInstance Win32_Service | Select-Object Name, DisplayName, State, StartMode, StartName, PathName |
  Export-Csv "$out\services.csv" -NoTypeInformation
Get-ScheduledTask | Where-Object { $_.TaskPath -notlike '\Microsoft\*' } |
  Select-Object TaskPath, TaskName, State,
    @{n='RunAs'; e={ $_.Principal.UserId }},
    @{n='LastRun'; e={ ($_ | Get-ScheduledTaskInfo).LastRunTime }},
    @{n='Runs'; e={ ($_.Actions | ForEach-Object { "$($_.Execute) $($_.Arguments)" }) -join ' ; ' }} |
  Export-Csv "$out\scheduled-tasks.csv" -NoTypeInformation
Get-NetTCPConnection -State Listen | Select-Object LocalAddress, LocalPort,
    @{n='Process'; e={ (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName }} |
  Export-Csv "$out\listening-ports.csv" -NoTypeInformation
Get-SmbShare | Select-Object Name, Path, Description |
  Export-Csv "$out\shares.csv" -NoTypeInformation
Get-ChildItem Cert:\LocalMachine\My | Select-Object Subject, NotAfter, Thumbprint |
  Export-Csv "$out\certificates.csv" -NoTypeInformation
Get-OdbcDsn | Select-Object Name, DsnType, Platform, DriverName |
  Export-Csv "$out\odbc-sources.csv" -NoTypeInformation
Copy-Item "$env:SystemRoot\System32\drivers\etc\hosts" "$out\hosts.txt"
net localgroup administrators | Out-File "$out\local-admins.txt"

What to look for in the output:

  • services.csv: the StartName column is the account each service runs as. A named person's account there is a service that will fail at its next start once that person's password changes or their account is disabled.
  • scheduled-tasks.csv: every task outside Microsoft's own folders is part of the system, listed with its account and the program it runs.
  • listening-ports.csv: what other machines connect to, and which program answers (netstat -ano gives similar output on any version).
  • certificates.csv: anything with a NotAfter date in the next few months is an outage with a date on it.
  • odbc-sources.csv: the database connections applications use. Windows keeps 32-bit and 64-bit system data sources apart, so an old 32-bit application's connections don't show in the 64-bit ODBC administrator. This list has both.
  • hosts.txt: any line that isn't a comment redirects a server name, and nobody remembers why.
Microsoft end-of-support dates for versions you may find on these servers (as of September 2026)
ProductExtended support endsExtended Security Updates (bought separately)
Windows Server 2012 and 2012 R2Ended October 10, 2023Until October 13, 2026
Windows Server 2016January 12, 2027Not yet due
Windows Server 2019January 9, 2029Not yet due
SQL Server 2014Ended July 9, 2024Until July 8, 2027
SQL Server 2016Ended July 14, 2026Until July 17, 2029
SQL Server 2017October 12, 2027Not yet due

Check your exact version on Microsoft's lifecycle search. Then prove you can bring the server back. Sysinternals Disk2vhd copies a running machine's disks into virtual hard disk files, using Windows volume snapshots so the server stays up (it can't convert BitLocker-encrypted volumes until they are decrypted). Attach the files to a new Hyper-V virtual machine on a different host. Microsoft warns that attaching them on the machine they came from changes the disk signature, and the copy then won't boot. Connect the copy only to a private virtual switch, which lets virtual machines talk to each other and nothing else. On the normal network, the copy's scheduled tasks would wake up and run against your live systems, sending the same emails or files a second time.

Do this first: run the inventory, then boot the copy on its isolated switch and have someone who uses the system daily confirm it works. Until that has happened once, you don't know that you have a backup.

7. The three-year-old app that is already hard to change

A recent custom build where every change takes longer than expected and fixing one area breaks another. When a young system is this hard to change, look for two causes: no automated tests, and requirements that moved after the architecture was set. Discovery here means finding where the fragility is concentrated, and the repository already knows. A developer can run this in Git Bash or any Unix shell:

git log --since="12 months ago" --name-only --format="" | sort | uniq -c | sort -rn | head -20

It lists the 20 files changed most often in the past year. Put that list next to the past year's bug reports. Files on both lists are where the cost of change lives. Before anyone edits them, their current behavior should be pinned down with characterization tests, which record what the code does today, right or wrong, so any change to that behavior shows up at once.

Do this first: run the command, match it against the bug list, and ask whoever maintains the app which of those files they are afraid to touch. That short list is the scope of the first piece of work.

8. The integration nobody can explain

Two systems exchange data. It works, and nobody employed today knows exactly how, what happens when it fails, or whether anyone would notice. One that fails quietly leaves stale data behind while reports built on it keep answering confidently, which is why freshness checks like those in our article on AI and numbers exist. They hide in five places.

Inside the database

If either system runs on SQL Server, a database administrator can run these read-only queries in SQL Server Management Studio. The third one needs running in each database:

-- Every SQL Server Agent job, who owns it, and what each step runs
SELECT j.name AS job_name, j.enabled, SUSER_SNAME(j.owner_sid) AS job_owner,
       s.step_id, s.subsystem, s.database_name, s.command
FROM msdb.dbo.sysjobs AS j
JOIN msdb.dbo.sysjobsteps AS s ON s.job_id = j.job_id
ORDER BY j.name, s.step_id;

-- Linked servers: other databases this one reaches into directly
SELECT name, product, provider, data_source
FROM sys.servers
WHERE is_linked = 1;

-- Triggers: code that runs whenever a row in a table changes
SELECT OBJECT_NAME(parent_id) AS table_name, name AS trigger_name, is_disabled
FROM sys.triggers
WHERE parent_class = 1;

Steps whose subsystem is SSIS run Integration Services packages, and the command text says which package and where it is stored. A job owned by a person's Windows login rather than a service account is one to fix before that person leaves: SQL Server Agent checks the owner's access each time the job runs, and a disabled or deleted Windows account can make that check fail.

On servers

The inventory in example 6 covers Windows Task Scheduler. On Linux, run crontab -l as each user that owns jobs and read /etc/cron.d.

In integration platforms

Zapier, Make and Power Automate automations belong to the person who built them. In Power Automate, open the Power Platform admin center, select the environment, then Resources > Flows, and look for flows with no owner listed. Microsoft calls these orphaned flows, and any that use the departed owner's connections can start failing. In Zapier, transferring a departed member's Zaps changes the URL of any Catch Hook webhook trigger, because that URL belongs to the Zap's owner (Zapier help). Whatever system posts to the old URL needs updating, or its data stops arriving.

In SaaS admin consoles

Apps that someone connected to your Microsoft 365, Google or Salesforce data in a few clicks are integrations too, and they appear on no diagram. Where to list them, as of September 2026:

  • Microsoft Entra ID: Entra ID > Enterprise apps > All applications. Open each non-Microsoft app and select Permissions to see what was granted, on the Admin consent and User consent tabs.
  • Google Workspace: Security > Access and data control > API controls > Manage Third-Party App Access lists apps your users have connected. On the same page, Manage Domain Wide Delegation lists service accounts allowed to act on behalf of any user in your domain, within the scopes shown next to each one.
  • Salesforce: in Setup, search for OAuth and open Connected Apps OAuth Usage. Clicking an app's user count shows who uses it and when they last did.

In the directory

An expiring password would break an integration, so the accounts integrations sign in with are normally set never to expire, which also makes them easy to list. With the Active Directory PowerShell module, Search-ADAccount -PasswordNeverExpires -UsersOnly | Select-Object Name, SamAccountName, Enabled, LastLogonDate lists them (Microsoft reference). A never-expiring account with a recent LastLogonDate is in use by something, and each one needs a name next to it.

Do this first: for every item you find, write one row: source, destination, schedule or trigger, the account it uses, and what happens when it fails. The last column is the one nobody can fill in, and it tells you what to fix first.

The one-page discovery sheet

Whichever example you recognized, the same questions apply. Copy this table into a document, fill in one per system, and treat every row you can't answer as a risk.

Legacy system discovery sheet
QuestionWhere to find the answer
What does it do, in the words of the person who uses it daily?A 30-minute conversation while they use it, recorded
Who could change it tomorrow, and who could if they left?The admin lists of the application, server and repository
Where does it run, and whose account pays for and owns that?Hosting, registrar and licensing accounts (example 2)
What runs without anyone clicking anything?AutoExec and data macros, scheduled tasks, Agent jobs, cron, flows and Zaps (examples 1, 6 and 8)
Which accounts and secrets does it use, and when do they expire?Service accounts, certificates and API keys (examples 2, 6 and 8)
What does it read from and write to?Linked tables, linked servers, ODBC sources, workbook links, OAuth grants
Where are the business rules?Validation rules, action queries, formulas, scripts, triggers (examples 1, 3 and 4)
How close is it to a hard limit?File size against documented limits, such as 2 GB for Access
When does support end for each component?The vendor's lifecycle pages (example 6)
When did anyone last restore it, and did it work?The date of the last test restore (example 6)
Can you get all the data out?A reconciled test export (example 5)
What stops first if it is down for a day, and who notices?The people downstream of each integration row

If the sheet leaves you with more blanks than answers, bring it to a conversation, along with the system, the workflow that depends on it and the change you have been postponing. We would start with the rows about unattended jobs and credentials, because they decide what breaks first. Our software improvement and rebuild work begins with that assessment, and sometimes concludes that the smallest useful change is the right one.

Build with Adamant Code

Is your business outgrowing its tools?

Bring one workflow or software problem. We’ll discuss where your current setup falls short and the next step worth exploring.

Talk through your workflow
Legacy System Examples and Where Each One Hides Its Rules | Adamant Code