Skip to content
PYD ENTERPRISES

VB6 anti-pattern

VB6 global module state

Public variables in .bas modules, implicit default form instances and hidden controls used as variables, so behavior depends on what any form did earlier in the session.

FAMILY
VB6
CUTOVER RISK
High
Hides business logic or coupling that a rewrite will drop unless it is extracted explicitly.
MODERN MAPPING
Explicit request context and injected services
GENERATED ARTIFACT
state-cells.json

Before and after

legacy source → modern mapping

Legacy: exhibits the pattern

modGlobals.bas + frmQuote.frmVB6
1' modGlobals.bas
2Public gCurrentBranch As String
3Public gUserLevel As Integer
4Public gLastQuoteNo As Long
5
6' frmQuote.frm
7Private Sub cmdRate_Click()
8 If gUserLevel < 3 And CCur(txtDiscount.Text) > 5 Then
9 MsgBox "Supervisor approval required"
10 Exit Sub
11 End If
12 gLastQuoteNo = gLastQuoteNo + 1
13 lblPremium.Caption = Format(CalcPremium( _
14 gCurrentBranch, CCur(txtSumInsured.Text), _
15 CCur(txtDiscount.Text)), "#,##0.00")
16 ' implicit load of frmSummary default instance
17 frmSummary.txtHiddenPremium.Text = lblPremium.Caption
18End Sub

Modern: Explicit request context and injected services

QuoteService.csC#
1public sealed record QuoteContext(
2 string Branch, // was gCurrentBranch
3 int AuthorityLevel); // was gUserLevel
4
5public async Task<Quote> RateAsync(
6 QuoteContext ctx, QuoteInput input)
7{
8 if (ctx.AuthorityLevel < 3 && input.DiscountPct > m)
9 throw new ApprovalRequiredException("supervisor");
10
11 long quoteNo = await _sequences
12 .NextAsync("quote"); // was gLastQuoteNo + 1
13 decimal premium = _rating.Calculate(
14 ctx.Branch, input.SumInsured, input.DiscountPct);
15 return new Quote(quoteNo, premium);
16}

Why it is dangerous in modernization

VB6 desktop applications were single-user processes, and global state was cheap: a Public gUserLevel As Integer in a module is visible to every form and class. Forms add more state. Referencing frmSummary.txtTotal anywhere silently loads the default instance of frmSummary, running its Form_Load, and invisible text boxes on forms are routinely used as variables to pass values between screens. Business rules read these globals and hidden controls, so a premium calculation can depend on which branch the user picked on a login screen an hour ago. Moving that logic to a multi-user web service without making the state explicit causes the worst kind of defect: one user's branch, discount authority or running counter leaks into another user's request.

How PYD ENTERPRISES detects it

The VB6 parser builds a symbol table across all modules, forms and classes in the .vbp project, including the controls declared in .frm files. Every Public and Global module variable, every Static local, and every control property used as data storage is a state cell. Dataflow analysis records which procedures write each cell and which read it, and the call graph gives the event order that can lead from a write to a read. Implicit form loads are detected wherever a form's members are referenced without an explicit instance. Rules extracted from procedures that read global state get those cells listed as inputs.

Maps to: Explicit request context and injected services

Per-user values such as branch and authority level become fields of a request or session context passed explicitly to services, typically from the authenticated identity. Shared reference data such as rate tables becomes an injected, read-only service with its own cache policy. Counters and sequence numbers move to the database. Hidden controls disappear: whatever they carried becomes a property of a request or a resource.

Generated specification fragment

state-cells.jsonJSON
1{
2 "project": "QuoteDesk.vbp",
3 "stateCells": [
4 { "cell": "gCurrentBranch", "kind": "module public",
5 "writers": ["frmLogin.cmdOK_Click", "frmBranch.lstBranch_Click"],
6 "readers": ["frmQuote.cmdRate_Click", "modRating.CalcPremium"],
7 "scope": "per user session" },
8 { "cell": "gLastQuoteNo", "kind": "module public",
9 "writers": ["frmQuote.cmdRate_Click"],
10 "scope": "shared counter, move to database sequence" },
11 { "cell": "frmSummary.txtHiddenPremium.Text",
12 "kind": "hidden control",
13 "readers": ["frmSummary.cmdBind_Click"],
14 "scope": "pass as request property" }
15 ]
16}

Every generated element links back to the source file and line range shown in the legacy pane. Review and sign off before building against it.

Why a counter in a module is dangerous on a server

gLastQuoteNo = gLastQuoteNo + 1 produces unique numbers only because each user runs a separate copy of the program, and the starting value is loaded from the database at login. Put the same logic in a web service and every request shares one variable: two concurrent quotes can receive the same number, and a restart resets the sequence. The engine classifies each global by how it is written (per-session value, shared counter, cache of reference data) so the right replacement is chosen, not just a mechanical move to a static field.

Hidden controls and implicit loads

Assigning frmSummary.txtHiddenPremium.Text loads frmSummary if it is not loaded yet, which runs its Form_Load event, which in many applications queries the database and sets other globals. A single line can therefore trigger a chain of side effects. PYD ENTERPRISES marks each implicit load in the call graph and includes the triggered Form_Load in the slice of any rule that depends on it, so reviewers can see the whole chain behind a value.

Money in VB6 types

The example uses CCur for monetary values, which is right: Currency is a scaled 64-bit integer with four decimal places. Many VB6 applications use Double or Single instead, sometimes in the same calculation, and a rate table in Double carries binary rounding error into every premium. The engine reports the numeric type of every variable on a monetary path, which tells you where the new decimal-based service will legitimately differ in the last cent and where it should match exactly.

Find every VB6 global module state in your codebase.

Run PYD ENTERPRISES in your private VPC and get each occurrence with file, line range, modern mapping and equivalence fixtures, ready for your architects to review.