Blog › ICP guides
Delphi developer on retainer: Object Pascal, VCL, and legacy Windows systems on monthly retainer
September 25, 2026 · ~20 min read
A manufacturing company had run its shop-floor ERP system on Delphi 7 for nearly fifteen years. The application managed production orders, parts inventory, and shift reporting for three factory sites. It had been stable enough — a well-understood set of forms backed by TADOQuery against SQL Server, a small team that knew the codebase — but over the past year, two of the three sites had started reporting that the application slowed to a crawl toward the end of each 8-hour shift, and by hour seven the Windows task manager showed it holding over 2GB of private memory. On Delphi 7 with a 32-bit process ceiling near 2GB, that meant intermittent GDI handle exhaustion, dialogs that failed to render, and a hard restart at shift change.
A Delphi developer on retainer traced the problem in six hours. The ERP system had a shift reporting module that built a summary report on demand throughout the shift — users clicked “Refresh Summary” as often as they liked to see current production counts. Each refresh instantiated a TADOQuery, looped through the result set, and for each row created a TStringList to hold the formatted report line items — part number, description, count, status. Those TStringList instances were added to a shared TObjectList named FReportItems declared in the form. The TObjectList had been created at form initialization with FReportItems := TObjectList.Create. The problem was a single missing property assignment: FReportItems.OwnsObjects defaulted to True for TObjectList, but the developer who built the reporting module had previously used a plain TList — and when they switched to TObjectList for type clarity, they never confirmed the ownership behavior. The refresh logic called FReportItems.Clear at the start of each refresh, intending to wipe the list before repopulating. With OwnsObjects:=True — TObjectList’s default — Clear would call Free on each contained TStringList before removing it. But the actual declaration in the production code read FReportItems := TObjectList.Create(False). The False argument sets OwnsObjects to False, meaning the list held pointers to TStringList instances but took no responsibility for freeing them. Every Clear discarded the references. Every Refresh created new TStringList instances. Over an 8-hour shift with a user refreshing the summary every 10 to 20 minutes, this accumulated 24 to 48 fresh TStringList instances per form instance — and there were three form instances running simultaneously on the shift supervisor’s workstation. The fix was changing False to True in that one constructor call. A Delphi developer on monthly retainer does this work continuously: auditing TObjectList and TList ownership semantics before memory accumulation shuts down a shift, verifying TThread.Synchronize coverage before VCL corruption surfaces as an intermittent access violation under load, and reviewing FireDAC transaction boundaries before concurrent query isolation failures corrupt production data.
Delphi language fundamentals: classes, generics, and anonymous methods
Delphi’s unit structure divides each source file into three sections. The interface section declares what the unit exports: type definitions, function and procedure signatures, and variable declarations visible to other units. The implementation section contains the actual code — method bodies, private helper functions, and anything that should not be exported. The initialization section runs at program startup before begin of the main block; the finalization section runs at shutdown. This separation enforces an explicit public API boundary that many larger codebases rely on to prevent circular dependency creep.
Class declarations follow a strict pattern. A class constructor is always named Create by convention; a destructor is always named Destroy and must call inherited Destroy (or simply inherited) before returning to ensure the parent class cleans up its own resources. Failing to call inherited in Destroy is a class of bug that silently leaks parent-class allocations — the compiler does not enforce it. The correct pattern for allocating and freeing a class instance is to call the constructor directly — obj := TMyClass.Create(params) — and to free it with either obj.Free or FreeAndNil(obj). TObject.Free checks whether the object pointer is nil before calling Destroy, making it safe to call on a nil reference. FreeAndNil(obj) calls Free and then sets the variable to nil, preventing use-after-free from a dangling non-nil pointer — the standard pattern for any object whose reference might be checked after destruction.
Virtual and abstract methods power Delphi’s polymorphism model. A method declared virtual can be overridden in descendants; a method declared abstract must be overridden and has no implementation in the declaring class. The override keyword on a descendant method explicitly opts into the virtual dispatch chain — without it, the descendant method shadows rather than overrides, and virtual dispatch through a base-class reference will still call the ancestor implementation. Published properties expose fields through the run-time type information (RTTI) system, enabling the DFM streaming mechanism that saves and restores VCL form state: property Caption: string read FCaption write SetCaption declares a property with a getter field and a setter method; the published visibility makes it visible to the Object Inspector and the form streaming code.
The IInterface reference-counting mechanism provides automatic lifetime management for objects that implement it. Any class descending from TInterfacedObject gets _AddRef and _Release implementations that increment and decrement a reference count; when the count reaches zero, the object frees itself. Interface variables are reference-counted automatically by the compiler: assigning an interface variable increments the refcount; scope exit decrements it. This eliminates manual Free calls for complex ownership graphs where multiple callers share access to the same object. The pattern is particularly useful for service or factory objects passed across form boundaries in large VCL applications.
Generics arrived in Delphi 2009 and became stable with Delphi XE. TList<T> is a typed dynamic array container that eliminates the cast-heavy pattern of plain TList: var items: TList<TProductOrder>; items := TList<TProductOrder>.Create stores TProductOrder references directly, and items[i] returns a TProductOrder without casting. TDictionary<K,V> provides an O(1) hash map: var cache: TDictionary<string, TPartRecord>; cache.AddOrSetValue(partNumber, rec); cache.TryGetValue(partNumber, rec) — TryGetValue returns False if the key is absent without raising an exception. Anonymous methods — declared as reference to procedure or reference to function — capture variables from the enclosing scope by reference, enabling closures: a TButton OnClick handler can be assigned a reference to procedure variable that closes over a loop variable, a database record, or a form reference. begin/end blocks delimit compound statements in if/then/else, try/except/finally, for, while, and repeat/until constructs — Delphi has no implicit block scoping from braces, so every multi-statement body requires explicit delimiters.
Record helpers extend existing record types — including built-in types — without subclassing: type TStringHelper = record helper for string adds methods to the string type itself, so myString.Trim or myString.Contains('substring') are valid call sites in Delphi XE3+. This pattern is widely used in framework code to add utility methods to primitive types without breaking existing inheritance hierarchies. It is also a common source of version compatibility surprises in codebases that must support Delphi 7 or Delphi 2007, where record helpers either do not exist or have different semantics than their modern counterparts.
VCL component library: forms, threading, and TStringList
The Visual Component Library is Delphi’s GUI framework for Windows desktop applications. Every VCL application is built around TForm — the top-level window class that wraps a Win32 HWND and exposes it as a first-class Object Pascal object. Forms contain component instances — TButton, TEdit, TListView, TTreeView, TStringGrid — placed at design time in the Form Designer and persisted in the .dfm resource file. The DFM file stores component properties as a stream that the VCL deserializes at runtime via published property RTTI, assigning Caption, Width, Height, and every other published property before calling the form’s OnCreate handler.
Form event handlers drive the VCL lifecycle. OnCreate fires after the form object is constructed and all components are streamed from the DFM; this is the correct place to initialize data structures, open database connections, and set up background threads. OnShow fires each time the form becomes visible — including after being hidden and reshown — making it the right handler for data refresh that should happen on every display, not just on first creation. OnCloseQuery fires when the user attempts to close the form, receiving a var CanClose: Boolean parameter; setting CanClose := False cancels the close and allows the handler to show a confirmation dialog, check for unsaved data, or gracefully stop background threads before allowing window destruction.
TWinControl.Handle exposes the underlying HWND for a windowed control. Accessing the Handle property on a control that has not yet been created forces HWND allocation — a side effect that can cause handle exhaustion if triggered in a background thread or during a loop that instantiates many controls. The safe pattern is to check HandleAllocated before accessing Handle in non-UI code, or to avoid accessing Handle at all outside the main message pump thread. This is directly related to VCL’s core constraint: all VCL calls must occur on the main thread that owns the message pump. A background TThread that touches TForm, TListView, TEdit, or any other TWinControl without marshaling to the main thread produces a race on the underlying HWND operations.
TThread.Synchronize and TThread.Queue are the two VCL thread marshaling mechanisms. Synchronize(procedure) blocks the calling background thread until the anonymous method or procedure reference has executed on the main thread — it posts a message to the main thread’s message queue and waits. Queue(procedure) posts the callback to the main thread queue without blocking, allowing the background thread to continue. Synchronize is appropriate when the background thread needs the UI result before continuing — reading a control value, checking a form state. Queue is appropriate when the background thread is fire-and-forget about the UI update and should not be blocked by main thread latency. The most common threading bug in legacy VCL code is writing directly to a TListView’s Items list from TThread.Execute without either Synchronize or Queue — this compiles and often appears to work under light load, but the underlying Win32 ListView SendMessage calls are not thread-safe and produce list corruption or access violations under concurrent access.
TTimer provides periodic callbacks on the main thread via the Windows WM_TIMER message — its OnTimer event fires on the message pump at the specified Interval in milliseconds. TTimer is single-threaded by design and safe to use with VCL controls directly, making it the appropriate mechanism for polling-based UI updates, debounced input handling, and periodic refresh at rates above a few hundred milliseconds. For shorter intervals or CPU-intensive work, TTimer is not appropriate — the WM_TIMER message is low-priority and can be starved by other message processing.
TStringList is one of the most widely used utility classes in Delphi codebases: a dynamic list of strings with optional key-value pairs accessed via the Names/Values properties. TStringList.Sort sorts the list in place using a quicksort; TStringList.Find performs a binary search on a sorted list, returning the index and a Boolean indicating whether the exact string was found. TStringList.Sorted := True maintains sort order on every Add operation at the cost of O(log n) per insertion. For key-value usage, list.Values['key'] returns the value portion of a key=value entry; list.IndexOfName('key') returns the line index. TMemoryStream provides an in-memory binary buffer implementing TStream: stream.Write(buffer, count), stream.Read(buffer, count), stream.Position := 0 to rewind, and stream.CopyFrom(sourceStream, 0) to copy all content from another stream. TMemoryStream is the standard pattern for building a binary payload in memory before writing it to a file, network socket, or blob field.
FireDAC and ADO database: TDataSet, parameters, and transactions
Delphi’s database access architecture is built around the abstract TDataSet class, which all database components — TFDQuery, TFDTable, TADOQuery, TADOTable — descend from. TDataSet provides a uniform record-navigation and editing model regardless of the underlying data source: First moves to the first record; Next advances one record; EOF is True when the cursor is past the last record; the standard traversal pattern is DataSet.First; while not DataSet.EOF do begin processRecord; DataSet.Next end. Dataset state machine transitions govern editing: Insert enters dsInsert state for a new record; Append enters dsInsert at the end; Edit enters dsEdit state for the current record; Post commits the pending insert or edit to the underlying store; Cancel abandons changes and returns to dsBrowse; Delete removes the current record.
Field access through the TDataSet API uses TField accessor methods for type-safe value reads: FieldByName('PartNumber').AsString returns the string value; FieldByName('Quantity').AsInteger returns an integer; FieldByName('UnitPrice').AsFloat returns a double. FieldByName('Notes').IsNull tests for database NULL before reading, preventing the empty-string-vs-null ambiguity that causes data integrity bugs in nullable text columns. For performance-critical loops over large result sets, FindField or cached TField references avoid the repeated string hash lookup that FieldByName performs on every call: var fQty: TIntegerField; fQty := DataSet.FieldByName('Quantity') as TIntegerField caches the field reference before the traversal loop.
SQL parameter binding via Params is the correct and only safe way to pass user-supplied or programmatic values into a query. TFDQuery.ParamByName('PartNo').Value := partNumber binds the value as a typed parameter that the database driver handles safely, preventing SQL injection and allowing the database to cache the query execution plan across parameter value changes. The Params[n].Value positional form works with both FireDAC and ADO components. For FireDAC specifically, TFDParam.DataType should be set explicitly for stored procedure parameters — Params[0].DataType := ftString; Params[0].Size := 50 — because FireDAC’s automatic type inference can produce incorrect bindings for varchar vs nvarchar distinctions that matter for MSSQL collation and index coverage.
TFDConnection is the FireDAC connection object, replacing both BDE TDatabase and ADO TADOConnection in modern Delphi codebases. TFDConnection.Connected := True opens the connection; the connection string parameters are set via Params: DriverID, Server, Database, User_Name, Password. TFDTransaction wraps explicit transaction management: FDTransaction1.StartTransaction begins a transaction; FDTransaction1.Commit commits; FDTransaction1.Rollback rolls back. For legacy ADO code, TADOConnection.BeginTrans, CommitTrans, and RollbackTrans serve the same role. The critical design error in legacy multi-form applications is sharing a single TADOConnection or TFDConnection instance across form instances that may issue concurrent queries — ADO connections are not re-entrant, and concurrent Execute calls on the same connection produce either serialized blocking or corrupted cursor state depending on the database driver version.
Memory management for heap-allocated structures outside the class model uses GetMem and FreeMem for raw byte allocation — GetMem(ptr, sizeInBytes) allocates an untyped block; FreeMem(ptr) releases it. New(typedPtr) allocates a typed pointer and calls the record’s Initialize procedure if applicable; Dispose(typedPtr) calls Finalize and releases the memory. For string fields inside a record, Dispose correctly runs the string reference count cleanup that FreeMem would silently skip, causing string heap leaks. TObjectList is the workhorse container for object ownership in legacy code: TObjectList.Create(True) or simply TObjectList.Create (default OwnsObjects:=True) creates a list that calls Free on every contained object when the list itself is freed or when Clear is called or when an item is deleted from the list. TObjectList.Create(False) creates a non-owning list — the same interface, but Clear and Delete only remove references without freeing the pointed-to objects. Every piece of code that adds an object to a TObjectList should have a comment or a test confirming which ownership model is in effect; the single boolean argument to Create is the entire object lifetime contract for every item in the list.
How HourTab tracks Delphi developer retainer hours
Delphi developer retainers — particularly in legacy ERP, industrial control, and medical device contexts — produce the most extreme mismatch between time invested and visible artifact of any Windows development retainer. The session that resolved the manufacturing company’s shift-end memory exhaustion produced one changed boolean argument in one constructor call: TObjectList.Create(False) to TObjectList.Create(True). That session involved reading the application’s full startup and teardown sequence to map which objects created the TObjectList and when; writing a FastMM4 leak report by enabling its full report mode in the project DPR; identifying TStringList as the leaked class with an instance count that grew by approximately 24 to 48 per hour of runtime; tracing every site in the 340-unit codebase where TStringList objects were created and added to a list; narrowing the candidate lists from 12 TObjectList instances to three non-owning ones by reading Create calls; confirming which of the three non-owning lists was the reporting module’s FReportItems by correlating the per-hour instance count growth rate with the average number of Refresh Summary clicks per hour logged in the shift activity report; changing the constructor argument; running the application under FastMM4 for a simulated 8-hour shift using a test harness that called the refresh logic 40 times; confirming zero leaked TStringList instances in the FastMM4 shutdown report; and documenting the OwnsObjects audit pattern as a standing checklist item for all future TObjectList additions.
The log entry “fixed memory leak, 6h” gives the client no path from six hours to the shift-end GDI exhaustion resolution, because nothing in one constructor argument change communicates the FastMM4 instrumentation, 340-unit codebase scan, instance count correlation, or simulated 8-hour soak test that preceded it. HourTab gives Delphi developers a public retainer-hours URL they share with each client at the start of the engagement. The client opens the URL and sees the current burn-down without logging in — no portal account, no report email, no status meeting. For VCL and database retainers specifically, the work log format carries the weight: each entry should name the memory management finding and the FastMM4 evidence (FReportItems TObjectList.Create(False) changed to Create(True) — OwnsObjects:=False allowing TStringList accumulation across Refresh Summary calls; FastMM4 soak test 40 refreshes: 48 TStringList leaks → 0 leaks; application memory after simulated 8-hour shift: 2.1GB → 178MB), the threading audit result (TShiftThread.Execute direct TListView.Items.Add call wrapped in Synchronize; intermittent AV under 3-user concurrent load: present → absent over 72-hour soak test), and the database transaction change (TADOConnection shared instance per form replaced with per-operation connection factory; explicit BeginTrans/CommitTrans around order insert batch; dirty reads under READ COMMITTED isolation: 23/1000 concurrent queries → 0/1000). That entry takes five minutes to write and converts the client’s next check-in from a forty-minute explanation of what TObjectList OwnsObjects means in a Delphi 7 shift reporting form into a two-sentence acknowledgment that the application no longer crashes at shift end.
The retainer model fits Delphi platform engineering because the language’s deployment contexts — legacy ERP, industrial SCADA, medical device interfaces, and document management systems — are long-lived platforms that run in production for ten to twenty years. A Delphi 7 codebase that shipped stable in 2008 accumulates subtle ownership and threading assumptions that only manifest as bugs when usage patterns change: shift supervisors who used to click Refresh Summary twice per shift now click it forty times because management added a real-time dashboard requirement; a three-user workstation becomes a fifteen-user terminal server; a single TADOQuery running sequentially becomes three concurrent TADOQuery instances from parallel report generation. A project contract closes when the current memory leak or access violation is resolved. A Delphi retainer stays open for the next TObjectList that gets created with the wrong ownership boolean when a new module is added six months later, the next TThread descendant that touches a VCL control directly when a background sync feature is added, and the next TADOConnection that gets shared across form instances when a copy-paste pattern replicates the shared-connection architecture into the new order entry module.
Track Delphi developer retainer hours without the status emails
HourTab gives Delphi engineers and Object Pascal consultants a public URL per client retainer. One link, no login, live burn-down. Your clients stop asking “how many hours do I have left?” and your work log becomes the proof of value that gets the retainer renewed.
See HourTab pricing →FAQ: Delphi developer retainers
What does a Delphi developer on retainer typically do?
A Delphi developer on monthly retainer provides ongoing advisory across core Object Pascal (unit interface/implementation/initialization structure, class constructor/destructor with inherited discipline, virtual/abstract/override polymorphism, published property RTTI for DFM streaming, TObject.Free and FreeAndNil lifecycle management, IInterface reference-counted lifetime, record helpers, generics TList<T>/TDictionary<K,V>, anonymous methods as reference to procedure/function, begin/end exception handling), VCL component design (TForm/TButton/TEdit/TListView/TTreeView published binding, OnCreate/OnShow/OnCloseQuery event sequence, TWinControl.Handle HWND allocation, TThread.Synchronize/Queue UI thread marshaling, TTimer periodic callbacks, TStringList.Sort/Find in-memory lists, TMemoryStream binary buffers), FireDAC and ADO database integration (TFDConnection/TFDQuery/TADOConnection/TADOQuery, TDataSet First/Next/EOF/Insert/Post/Cancel/Delete, TField.AsString/AsInteger/AsFloat/IsNull, Params[n].Value/ParamByName parameter binding, TObjectList OwnsObjects memory ownership, TFDTransaction/TADOConnection transaction management), and legacy modernization (Delphi 7 to Delphi 12 Unicode migration, BDE to FireDAC, 32-bit to 64-bit, AnsiString to UnicodeString conversion, generics adoption in formerly TList-heavy codebases).
What Delphi work is most underlogged in a retainer?
TObjectList ownership audits (TObjectList.Create(False) accumulating TStringList instances across shift report refreshes; FastMM4 leak report: 48 TStringList instances per hour → 0 after Create(True); application memory after 8-hour shift: 2.1GB → 178MB; 20–36 hours invisible in one boolean argument change), VCL threading race condition remediation (TThread.Execute direct TListView.Items.Add without TThread.Synchronize causing intermittent access violations under concurrent load; Synchronize wrapper added around 8 list update call sites; AV under 3-user concurrent load: present → absent over 72-hour soak; 12–24 hours invisible in wrapper procedures), and FireDAC/ADO transaction isolation failures (shared TADOConnection across form instances causing dirty reads under READ COMMITTED; per-operation connection factory introduced; explicit BeginTrans/CommitTrans boundary added to order insert batch; dirty read count: 23/1000 concurrent queries → 0/1000; 14–28 hours invisible in connection factoring and transaction wrapper additions) are the three most systematically underlogged Delphi retainer categories.
What are typical Delphi developer retainer rates?
Entry-level Delphi developers (1–3 years, VCL form design, basic TDataSet navigation, standard TStringList usage, straightforward unit structure) bill at $75–$130/hr. Mid-level Delphi engineers (3–7 years, TThread.Synchronize/Queue thread marshaling, TObjectList OwnsObjects memory management, FireDAC TFDQuery parameterized queries, TADOConnection transaction handling, generics TList<T>/TDictionary<K,V> adoption) bill at $125–$225/hr. Senior Delphi architects (7+ years, legacy Delphi 7 ERP modernization, BDE to FireDAC migration, 32-bit to 64-bit platform transition, IInterface reference-counted component design, custom VCL component authoring, COM/ActiveX interop, DataSnap or REST multi-tier architecture) bill at $185–$340/hr. Firm rates run $150–$265/hr. Monthly retainer amounts: $2,500–$6,500/mo for advisory (15–30 hrs), $8,000–$18,000/mo for full legacy modernization or ERP platform re-architecture engagements.
What should a Delphi developer retainer agreement include?
A Delphi developer retainer agreement should specify language scope (unit interface/implementation/initialization structure, class constructor/destructor with inherited Create/Destroy discipline, virtual/abstract/override method design, published property read/write accessors and DFM streaming, TObject.Free and FreeAndNil lifecycle, IInterface reference-counted lifetime, record helpers, generics TList<T>/TDictionary<K,V>, anonymous method reference to procedure/function closures), VCL scope (TForm/TButton/TEdit/TListView/TTreeView component design, OnCreate/OnShow/OnCloseQuery event sequence, TWinControl.Handle HWND patterns, TThread.Synchronize vs Queue marshaling choice, TTimer deferred/periodic actions, TStringList.Sort/Find, TMemoryStream binary handling), database scope (TFDConnection/TFDQuery/TFDTable FireDAC, TADOConnection/TADOQuery ADO for MSSQL/Access, TDataSet navigation state machine, TField.AsString/AsInteger/AsFloat/IsNull, Params[n].Value and ParamByName parameter binding, TObjectList OwnsObjects audit methodology, GetMem/FreeMem vs New/Dispose, TFDTransaction/TADOConnection transaction demarcation), and legacy modernization scope (Delphi 7/2007/2009/XE to Delphi 10.x/11.x/12.x, AnsiString to UnicodeString, 32-bit pointer arithmetic to 64-bit NativeInt, BDE to FireDAC dataset migration, COM/ActiveX IDispatch interop maintenance, and hour logging specifics for memory leak, threading audit, and transaction design work).
How should Delphi developer retainer hours be logged?
Log each Delphi retainer session with: advisory category (unit interface/implementation/initialization structure, class constructor/destructor inherited Create/Destroy, virtual/abstract/override polymorphism design, published property RTTI read/write accessor, TObject.Free and FreeAndNil(obj) reference nullification, IInterface _AddRef/_Release reference-counted lifetime, record helper extension, TList<T>/TDictionary<K,V> generic collection adoption, anonymous method reference to procedure/function closure design, TForm/TButton/TEdit/TListView/TTreeView VCL component, published property DFM streaming vs runtime assignment, OnCreate/OnShow/OnCloseQuery event sequence, TWinControl.Handle HWND allocation and HandleAllocated validation, TThread.Synchronize vs Queue marshaling choice, TTimer WM_TIMER periodic callback, TStringList.Sort/Find/Sorted in-memory list management, TMemoryStream binary stream buffer, TFDConnection/TFDQuery/TFDTable FireDAC connection and dataset, TADOConnection/TADOQuery ADO MSSQL/Access legacy, TDataSet First/Next/EOF traversal, Insert/Post/Cancel/Delete state machine, TField.AsString/AsInteger/AsFloat/IsNull type-safe read, Params[n].Value and ParamByName SQL parameter binding, TObjectList OwnsObjects:=True vs False ownership classification, GetMem/FreeMem raw heap vs New/Dispose typed pointer, TFDTransaction BeginTransaction/Commit/Rollback isolation design, Delphi 7 AnsiString to UnicodeString migration, BDE to FireDAC TDataSet API migration, 32-bit Pointer to 64-bit NativeInt size change, COM/ActiveX IDispatch interop maintenance, DataSnap or REST server endpoint design), specific unit, class, or form name, diagnostic tool (FastMM4 full leak report: class name and leaked instance count; AQTime profiler: allocation call stack and hotspot; Delphi debugger access violation: faulting address and call stack; Process Explorer GDI handle count: pre/post comparison; TADOQuery.Recordset RecordCount and lock type; TFDMonitor trace: connection reuse and parameter binding log), fix applied with rationale (TObjectList.Create(False) changed to Create(True) — callers do not manage TStringList lifetime independently; TThread.Synchronize wrapper added around TListView.Items.Add — VCL controls not thread-safe, must execute on message pump thread; explicit TADOConnection.BeginTrans/CommitTrans added — shared connection under concurrent access requires explicit transaction boundary to prevent dirty reads), before/after metric (application memory after 8-hour shift: 2.1GB → 178MB; GDI handle exhaustion: present → absent; TListView corruption under 3-user concurrent load: intermittent AV → zero AV over 72-hour soak; dirty read count under READ COMMITTED: 23/1000 → 0/1000), and hours.