Blog › ICP guides
Delphi developer on retainer: interface reference counting, memory management, FastMM, and Delphi on monthly retainer
October 2, 2026 · ~13 min read
A Delphi developer was maintaining an order processing application for a wholesale distributor. The application’s batch processor ran hundreds of orders per day without issue for months. After a refactor that introduced an interface-based design for the order processing pipeline, the application began producing intermittent access violations during order finalization — not on every run, but on approximately 3 out of every 50 batches processed. The access violation pointed to a call on a field variable that should always be a valid object reference.
The refactored TOrderProcessor class held two references to the same TOrder instance:
type
TOrderProcessor = class
private
FCurrentOrder: TOrder; // direct object reference
procedure ProcessSingleOrder(AOrder: IOrder);
procedure FinalizeOrder;
end;
Inside ProcessSingleOrder, the developer stored the same instance in the local IOrder interface parameter and in the FCurrentOrder field:
procedure TOrderProcessor.ProcessSingleOrder(AOrder: IOrder);
begin
FCurrentOrder := AOrder as TOrder; // store direct object reference
// ... processing steps ...
end; // AOrder interface variable goes out of scope here
TOrder descended from TInterfacedObject, which implements IInterface with automatic reference counting. When ProcessSingleOrder returned, the AOrder interface variable went out of scope. Delphi decremented the reference count. If no other interface variable held a reference to the instance, the count reached zero and Delphi freed the TOrder object. FCurrentOrder was a direct object reference — a typed pointer — that bypassed the reference counting system entirely. After AOrder went out of scope and the object was freed, FCurrentOrder pointed to freed memory. The next call to FinalizeOrder, which called FCurrentOrder.UpdateStatus, produced an access violation on that freed memory pointer. The intermittency was because some code paths held additional interface references that kept the count above zero, while others did not. Access violations: 3 → 0 after changing FCurrentOrder: TOrder to FCurrentOrder: IOrder to manage lifetime through the interface variable.
Delphi overview: Object Pascal, Borland 1995, now Embarcadero
Delphi was developed by Borland International (later Inprise, now Embarcadero) and first released in 1995. It combined Object Pascal — a strongly typed, object-oriented extension of Niklaus Wirth’s Pascal — with a visual RAD (Rapid Application Development) IDE and the VCL (Visual Component Library) for Windows GUI development. Delphi produced native compiled Windows executables with no runtime DLL dependency, a significant advantage over Visual Basic at the time. Delphi 1 targeted 16-bit Windows 3.1; Delphi 2 (1996) added 32-bit support; Delphi 3 (1997) added COM and ActiveX support; Delphi 5 (1999) is the version most commonly found in long-running production applications.
Delphi 2009 (2008) made the breaking change from AnsiString to UnicodeString as the default string type, aligning with Windows’s native UTF-16 encoding. Code written before 2009 that used string as a byte buffer, compared Length(s) with a byte count, or passed strings to Windows APIs expecting byte counts requires migration. Delphi 10.x through 11 Alexandria are the current versions under Embarcadero, adding 64-bit Windows support, macOS support via FireMonkey, and modern language features including generics, anonymous methods, and inline variables.
Delphi retainer work today covers: maintaining Delphi 5, 6, and 7 applications on Windows 10 and 11 (ANSI API compatibility, 32-bit application compatibility, VCL component updates); migrating applications between Delphi versions (particularly the Delphi 7 to Unicode migration and the 32-bit to 64-bit migration); FastMM memory leak diagnosis and repair; interface reference counting bug diagnosis; database layer migration (from BDE to ADO or FireDAC); and VCL form and layout maintenance. The organizations running legacy Delphi are primarily in financial services, healthcare, manufacturing, and logistics — industries where the Delphi applications were business-critical in the 1990s and the cost of replacement exceeds the cost of maintenance.
Interface reference counting and the TInterfacedObject model
Delphi implements COM-compatible automatic reference counting for objects that implement the IInterface (equivalent to COM’s IUnknown) interface. The counting works through interface variables: when an interface variable is assigned, Delphi calls _AddRef on the object, incrementing the count; when an interface variable goes out of scope or is assigned a new value, Delphi calls _Release, decrementing the count; when the count reaches zero, _Release frees the object. This happens automatically for interface variable assignments — the developer does not call _AddRef or _Release directly when using TInterfacedObject.
The critical rule: reference counting only works through interface variables, not through direct object variables. A variable declared as TOrder (a typed pointer to the object) does not participate in reference counting. Assigning to a TOrder variable does not increment the count; the variable going out of scope does not decrement the count. If a TInterfacedObject-derived instance is held only by interface variables, lifetime management is automatic and correct. If the instance is held by a mix of interface variables and direct object variables, the lifetime is driven by the interface variables and the direct object variable may become a dangling pointer.
The diagnostic pattern: an access violation that occurs intermittently after a method returns, pointing to a field that holds a direct object reference, is the signature of this bug. The intermittency — sometimes works, sometimes crashes — indicates that some call paths hold additional interface references (keeping the count above zero, keeping the object alive) while other call paths do not (count reaches zero, object freed). The fix is converting the field type from the concrete class to the interface type: FCurrentOrder: IOrder instead of FCurrentOrder: TOrder. With the field as an interface variable, assigning to it increments the count, keeping the object alive for the lifetime of the field.
A second reference counting trap: circular references. If object A holds an interface reference to object B, and object B holds an interface reference to object A, neither object’s count ever reaches zero — they keep each other alive. Delphi does not have a cycle-detecting garbage collector; circular interface references produce memory leaks. The fix requires breaking the cycle: one of the references must be a weak reference (a direct object variable or a [weak] attribute reference in Delphi 10.1 and later) that does not participate in reference counting.
FastMM memory leak detection
FastMM (Fast Memory Manager) is the memory manager used by Delphi since Delphi 2006. In release builds it provides fast allocation. In debug builds (via FastMM_FullDebugMode.dll or the FullDebugMode conditional define), it tracks all allocations, detects use-after-free, buffer overruns, and reports outstanding allocations at program shutdown.
When FastMM full debug mode is enabled, the application produces a leak report at shutdown listing each allocated-but-not-freed class (or untyped block) with the number of instances and total bytes. The report also includes the allocation call stack if FastMM_FullDebugMode.dll is present. A typical leak report entry: TStringList - 3 instances (96 bytes). The call stack points to where the allocation was made; the missing Free is somewhere after that allocation point.
Common sources of Delphi memory leaks: objects created in event handlers that are not freed in the corresponding cleanup event; objects created in a try block before the try–finally scope covers them; objects stored in a list that is freed without freeing the list items first; exception paths that bypass the finally block. The standard pattern: any object that is explicitly created with Create must have a corresponding Free (or FreeAndNil) call in a finally block or a destructor. Objects owned by a TComponent parent are freed automatically when the parent is freed; objects not owned must be freed explicitly.
AnsiString vs UnicodeString, and cross-version migration
Delphi’s string type changed from AnsiString (1-byte-per-character, system code page) to UnicodeString (2-bytes-per-character, UTF-16) in Delphi 2009. Code written before this change that used string for byte-level operations — reading binary file formats, processing network protocol buffers, calling Windows APIs with byte counts — requires migration.
The most common migration bugs: Length(s) in pre-2009 code returned the byte count of the string (because 1 character = 1 byte); in post-2009 code, Length(s) returns the character count (2 bytes per character for BMP characters). Code that stored a byte buffer in a string and used Length as the byte count now returns half the buffer size. The fix: use TBytes (a TArray<Byte>) or RawByteString for byte buffers, and use SizeOf or ByteLength for byte counts.
Delphi version migration retainer work also covers: compiler directive differences between versions (conditional defines for {$IF CompilerVersion >= X} to handle version-specific behavior); deprecated units and procedures (SysUtils function name changes, AnsiUpperCase to UpperCase for Unicode-aware handling); 64-bit compilation (pointer size changes from 4 to 8 bytes; any code that casts a pointer to Integer instead of NativeInt produces truncation bugs; Windows API Declare statement parameter types must match the 64-bit API signatures); and third-party component updates (components compiled for Delphi 7 do not work in Delphi XE without recompilation; components may be unavailable or only available through commercial update).
Typical Delphi retainer work and what it looks like in a work log
Interface reference counting diagnosis is the largest category of Delphi retainer work that produces no visible artifact in normal operation. An access violation that occurs intermittently in a batch process — not every run, only when a specific call path is taken — produces crash reports that show a valid-looking field access as the fault address. The fix is changing one type declaration from the concrete class to the interface type. The diagnosis requires understanding that TInterfacedObject-derived objects are lifetime-managed through interface variables only, that direct object references bypass the counting mechanism, and that the intermittency maps to call paths that hold vs. do not hold additional interface references. Work log entry: “TOrderProcessor.FinalizeOrder: access violation on FCurrentOrder.UpdateStatus; FCurrentOrder field held direct TOrder reference; IOrder interface variable in ProcessSingleOrder went out of scope on method exit; reference count reached zero; TOrder freed; FCurrentOrder dangling; fix: changed FCurrentOrder field type from TOrder to IOrder; count held while field is assigned; access violations before: 3; after: 0; 6h.”
FastMM leak diagnosis is the second category. An application that accumulates memory over hours of operation until it is killed by the OS produces a FastMM report at shutdown (if debug mode was enabled) or does not (if release mode). Enabling FastMM debug mode in a test run to capture the leak report, identifying the leaking class, and tracing the call stack to the allocation site reveals the missing Free. Work log entry: “Order import service: memory growing 40MB per hour; enabled FastMM FullDebugMode; shutdown report: TOrderDocument - 147 instances (58,800 bytes); call stack pointed to TOrderImporter.ParseXmlDocument; TOrderDocument created in parse loop, stored in processing queue, freed after processing; edge case: XML parse error raised exception before document added to queue; exception path exited try block without hitting finally-free; fix: moved TOrderDocument creation inside try-finally; Free in finally block; leaking instances before: 147 per test run; after: 0; 5h.”
Track Delphi developer retainer hours without the status emails
When a 6-hour session traces intermittent access violations to a FCurrentOrder: TOrder field that bypassed TInterfacedObject’s reference counting — because direct object variables do not participate in IInterface’s _AddRef and _Release mechanism — the work log needs to name the class, the field type, the reference counting mechanism, and the access-violation count before and after. HourTab gives your Delphi retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log that names the mechanism. No client login. No status emails. CSV in, URL out.
How HourTab tracks Delphi developer retainer hours
Delphi retainer work is invisible by the same mechanism that makes Delphi’s reference counting ergonomic: the compiler handles _AddRef and _Release automatically for interface variables, so the developer does not see the counting happening. The distinction between FCurrentOrder: TOrder (bypass counting) and FCurrentOrder: IOrder (participates in counting) is a one-word type change that has a profound effect on object lifetime. The wrong field type can be in production for years without crashing, because most call paths that use it happen to hold additional interface references that keep the count above zero. The intermittent access violation only appears on the specific call paths that do not.
The work log needs to name the mechanism: which field type bypassed the reference counting, which interface variable going out of scope decremented the count to zero, what the object lifetime was at the point of the access violation, and the concrete before-and-after crash count. A log entry that says “fixed intermittent crash in order processor, 6h” is not auditable. A log entry that names the field type, the interface variable scope, the reference count state, and the access violation count is auditable.
HourTab gives Delphi developers a public retainer-hours URL they send to clients — financial services companies with Delphi-based trading or back-office systems, healthcare organizations with Delphi-based clinical or billing software, manufacturers with Delphi-based production control applications, and logistics companies with Delphi-based order management systems. For Delphi retainers, each work log entry should name the Object Pascal mechanism: which field type bypassed the reference count, which FastMM class was leaking and at which allocation site, which AnsiString operation was producing wrong byte counts after the Unicode migration. Comparative context: Delphi retainer work has conceptual overlap with other compiled enterprise language environments — Visual Basic 6 (similar enterprise Windows application maintenance retainer patterns; COM component management; 32-bit/64-bit compatibility); C++ (similar manual memory management patterns; similar interface reference counting semantics; more demanding type system); and Java (similar interface-based design patterns; garbage-collected so reference counting bugs are not a concern; but interface-type-vs-object-type usage decisions still affect design). Delphi is uniquely positioned as the environment for the specific class of 1990s and 2000s Windows enterprise applications that produce native compiled executables without a runtime and cannot be easily replaced.
FAQ: Delphi developer retainers
What does a Delphi developer on retainer typically do?
A Delphi developer on monthly retainer covers interface reference counting diagnosis (TInterfacedObject-derived class stored in a direct object field bypasses reference counting; interface variable going out of scope frees the object while the object field still points to freed memory; access violation on next field access; fix: change field type from concrete class to interface type); FastMM memory leak detection and repair (running under FastMM FullDebugMode to identify leaking classes and allocation call stacks; tracing to missing Free calls or exception-bypass code paths); AnsiString to UnicodeString migration (pre-2009 code using string as a byte buffer; Length returning character count instead of byte count after migration; fix: use TBytes or RawByteString for byte buffers); VCL form and component compatibility maintenance; and Delphi version migration engineering (Delphi 7 to XE; 32-bit to 64-bit; NativeInt for pointer casts; third-party component updates).
What Delphi work is most commonly underlogged?
Interface reference counting diagnosis: a direct object field for a TInterfacedObject-derived instance bypasses counting; an interface variable going out of scope frees the object; the object field becomes a dangling pointer; access violation is intermittent (depends on whether other interface references keep the count above zero); diagnosis requires understanding the IInterface counting model; fix: change field type to interface type; 4 to 9 hours for a one-word type change. FastMM leak diagnosis: identifying the leaking class, the allocation call stack, the exception path that bypasses Free; 3 to 7 hours invisible. AnsiString vs UnicodeString: pre-2009 byte-buffer code produces wrong byte counts after migration; fix: use TBytes; 4 to 8 hours per affected code path.
What are typical Delphi developer retainer rates?
Entry-level Delphi developers covering VCL form development, basic database access, and simple Object Pascal code typically bill at $65 to $115 per hour. Mid-level Delphi programmers covering interface reference counting, FastMM leak diagnosis, generics, thread synchronization, and database transaction management typically bill at $95 to $170 per hour. Senior Delphi developers covering COM/ActiveX development, Delphi version migration, AnsiString to UnicodeString migration, cross-platform FMX development, and legacy Delphi 5/6 maintenance on modern Windows typically bill at $135 to $250 per hour. Monthly retainer ranges: $1,200 to $2,800 per month for advisory engagements (8 to 18 hours per month); $2,800 to $9,000 per month for full engagement development including version migration.
What should a Delphi developer retainer agreement include?
A retainer agreement should specify: Delphi version scope (Delphi 7 through 11 Alexandria; string type, Unicode support, generics, and 64-bit behavior differ across versions; which versions are in scope); VCL vs FMX scope (Windows-only VCL vs cross-platform FireMonkey; different rendering models and component hierarchies); platform scope (Windows 32-bit, Windows 64-bit, macOS — different calling conventions and pointer sizes); third-party component scope (DevExpress, Telerik, Indy; whether diagnosing and maintaining these libraries is in scope); and hour logging format (the field type and interface type involved, the FastMM allocation class and call stack, the AnsiString operation and wrong byte count, the access-violation or memory-leak count before and after).
How should Delphi developer retainer hours be logged?
Log each Delphi retainer session with: the class and method that produced wrong behavior (e.g., TOrderProcessor.FinalizeOrder); the field type and interface type involved (e.g., FCurrentOrder: TOrder field; IOrder interface variable in ProcessSingleOrder); what the application produced (e.g., access violation in FinalizeOrder on FCurrentOrder.UpdateStatus call; FCurrentOrder pointed to freed memory); what it should have produced (e.g., FinalizeOrder should complete without access violation; TOrder instance should remain live while FCurrentOrder references it); the Delphi mechanism (e.g., TInterfacedObject-derived TOrder freed when IOrder interface variable in ProcessSingleOrder went out of scope; FCurrentOrder direct object field not counted; dangling pointer; fix: changed FCurrentOrder type from TOrder to IOrder); access violations before: 3; after: 0. For FastMM: the leaking class name, the allocation call stack, the exception path that bypassed Free, and the leak count before and after.