Here's something I've learned from years of working with Delphi teams: you can write excellent, shipping, money-making software for twenty years and never once declare an interface. Many of my customers have done exactly that. Classes, forms, try/finally, a well-organized data module — that toolkit carries you remarkably far, and there's nothing wrong with it.
But there's a curious gap. The libraries Delphi developers admire most lean on interfaces heavily. In my recent tour of Spring4D's collections, I verified something striking in the source: every single one of its 127 factory methods returns an interface, not a class. The RTL's own IComparer<T> is an interface. All of COM — the machinery behind decades of Windows automation — is interfaces. So the feature many of us nodded past is quietly load-bearing across the ecosystem.
I promised in that Spring4D post that interfaces would get a proper introduction of their own. This is it: what an interface actually is, why it's worth your time in 2026, how reference counting makes memory management easier rather than scarier — and, just as honestly, the sharp edges and the situations where a plain class is still the better call.
We'll start from what you already know and build up slowly, with runnable-sized examples at every step.
What you already do: classes, objects, and try/finally
Let's start on familiar ground, because interfaces make the most sense as a contrast to it.
In Delphi, an object created from a class is yours to destroy. The compiler doesn't watch its lifetime; you do. The idiom every Delphi developer has typed a thousand times:
var
list: TStringList;
begin
list := TStringList.Create;
try
list.Add('Hello');
// ... work with list ...
finally
list.Free;
end;
end;This is manual memory management, and — said with genuine respect — it's a perfectly good model. It's explicit, it's predictable, there's no garbage collector pausing your app at odd moments, and ownership is always answerable: whoever created it (or whoever it was handed to) frees it. Delphi developers have built rock-solid software on this model for three decades.
Since Delphi 10.4 Sydney unified memory management across all platforms, this story is also beautifully consistent: object instances are managed manually everywhere — Windows, macOS, Linux, iOS, Android. No more separate ARC compiler on mobile with different rules. One language, one model.
But notice one thing about the snippet above: the try/finally is ceremony. It says nothing about your problem — it exists purely so a forgotten Free doesn't become a leak. Hold that thought.
An interface is a contract — nothing more, nothing scarier
Here's the whole idea in one sentence: a class says how something is done; an interface says only what can be done.
An interface is a type that declares methods without implementing any of them. It's a promise — a contract — that some class, somewhere, will fulfill:
type
ILogger = interface
['{8B3E4C21-9D5A-4F6E-B2C7-1A0D9E8F3B42}']
procedure Log(const msg: string);
end;That's a complete, legal interface declaration (Object Interfaces in the Delphi docs). No fields, no implementation, no constructor. Just: anything claiming to be an ILogger can Log.
A class fulfills the contract by listing the interface and implementing its methods. The easiest way to start is to inherit from TInterfacedObject, which handles the plumbing for you:
type
TFileLogger = class(TInterfacedObject, ILogger)
private
FFileName: string;
public
constructor Create(const fileName: string);
procedure Log(const msg: string);
end;
procedure TFileLogger.Log(const msg: string);
begin
TFile.AppendAllText(FFileName, msg + sLineBreak);
end;And using it looks like this — note what's missing:
var
logger: ILogger; // the variable is the INTERFACE type
begin
logger := TFileLogger.Create('app.log');
logger.Log('Application started');
// no try/finally, no logger.Free — see the next section
end;The variable's type is ILogger, not TFileLogger. The calling code knows the contract and nothing else. That one shift unlocks both of the big advantages, so let's take them one at a time.
Advantage 1: swap the "how" without touching the callers
Because callers depend only on the contract, you can change the implementation behind it freely — and this is where interfaces start paying rent in real projects.
Picture three classes fulfilling the same contract:
type
TConsoleLogger = class(TInterfacedObject, ILogger)
procedure Log(const msg: string); // writes to stdout
end;
TSilentLogger = class(TInterfacedObject, ILogger)
procedure Log(const msg: string); // does nothing — perfect for tests
end;
// ... and TFileLogger from aboveAny code written against ILogger works with all three, unchanged:
procedure ProcessOrders(const logger: ILogger);
begin
logger.Log('Starting order run');
// ... business logic ...
logger.Log('Done');
end;
// production:
ProcessOrders(TFileLogger.Create('orders.log'));
// in a unit test — no file system touched:
ProcessOrders(TSilentLogger.Create);Here's the relationship as a picture — the caller touches only the contract, never the concrete classes.
Take from the picture: the arrows from your code stop at the contract. The concrete classes on the right can be added, replaced, or rewritten without your calling code ever knowing — which is exactly what makes testing, logging swaps, and "we need a second backend" requests painless.
Notice this is polymorphism without inheritance. TFileLogger and TSilentLogger don't share an ancestor (beyond TInterfacedObject); they share a promise. You're no longer forced to design deep class hierarchies just to get substitutability — and a single class can implement several unrelated interfaces at once, something single inheritance can never give you.
Advantage 2: the object frees itself
Here's the part that surprises people most, and it's the reason the earlier snippet had no try/finally: interface references are counted, and when the count hits zero, the object destroys itself.
You already trust this mechanism every day, by the way — Delphi strings have been reference-counted forever, and that remained true on every platform after the 10.4 unification. Nobody writes try/finally around a string. Interfaces extend that same courtesy to your own types.
The mechanics, step by step:
procedure Demo;
var
a, b: ILogger;
begin
a := TFileLogger.Create('app.log'); // reference count: 1
b := a; // reference count: 2
b := nil; // reference count: 1
end; // 'a' goes out of scope → count: 0
// → Destroy is called automaticallyAnd as a timeline:
Read it left to right: every assignment to an interface variable ticks the count up, every variable that lets go ticks it down, and the moment nobody holds a reference, the object cleans itself up — deterministically, right then, not "eventually" like a garbage collector. You keep Delphi's predictability and lose the ceremony.
For a fuller picture of how interface references behave — including assignment-compatibility rules — the official docs are good, and Dalija's book (linked above) is the deep end.
Where you've already met interfaces (maybe without noticing)
Interfaces aren't an exotic add-on to Delphi; they're woven through the ecosystem you already use, and it's worth seeing how mainstream they are.
- The RTL itself. Every time you pass a custom comparer to a sort, you're using
IComparer<T>fromSystem.Generics.Defaults— an interface. - COM and Windows automation. Driving Excel or Word from Delphi, shell extensions, OLE — the entire COM model is built on interfaces. It's the historical reason Delphi's interface support is so mature.
- Spring4D. As covered in the collections tour, its entire public collection API is interfaces —
IList<T>,IDictionary<K,V>,IEnumerable<T>— created via factories and freed by reference counting. Once the concepts in this post feel comfortable, that library reads naturally.
That's the practical motivation for learning this material: it's not a niche technique, it's the idiom the best tooling in the ecosystem already speaks.
The sharp edges — three rules that keep you safe
I'd be doing you a disservice to pretend interfaces have no gotchas. There are exactly three big ones, and all three are avoidable with simple habits. Learn these before you refactor anything real.
Rule 1: never hold the same object through both a class and an interface reference
This is the classic beginner accident, so let's look at it squarely:
var
obj: TFileLogger; // class reference — does NOT count
log: ILogger; // interface reference — counts
begin
obj := TFileLogger.Create('app.log');
log := obj; // count: 1
log := nil; // count: 0 → object is DESTROYED here
obj.Log('crash'); // obj now points at freed memory!
end;The object reference obj is invisible to the reference count. When the last interface reference lets go, the object dies — regardless of any class references still pointing at it.
The takeaway from the picture: only the solid arrow keeps the object alive. The dashed one is just a raw pointer with no say in the matter.
Rule 2: reference cycles need a [weak] link
If object A holds an interface reference to B, and B holds one back to A, their counts can never reach zero — both wait for the other forever, and you have a leak. The fix is to make one direction of the cycle uncounted. Since Delphi 10.1 Berlin, the classic compiler supports [weak] and [unsafe] attributes on interface references exactly for this:
type
TChild = class(TInterfacedObject, IChild)
private
[weak] FParent: IParent; // doesn't count — and is set to nil
end; // automatically if the parent dies firstA [weak] reference doesn't increase the count, and Delphi tracks it: if the target is destroyed, the weak reference becomes nil instead of dangling. ([unsafe] also skips counting but without the tracking — it's for rare, expert cases.) The rule of thumb: the "owner" direction is strong, the "back-pointer" direction is [weak]. Parent→child strong, child→parent weak.
Rule 3: components play by their own (older) rules
TComponent descendants — forms, data modules, and everything you drop on them — implement interfaces without reference-counted lifetimes. Their lifetime belongs to the ownership model: the Owner frees them, exactly as it always has. That's a deliberate and sensible design — your form isn't going to vanish because an interface variable went out of scope. Just know that "interfaces = automatic lifetime" applies to TInterfacedObject-style classes, not to the VCL component world.
When not to use interfaces
Now the honest scoping, because a good tool recommendation always says where it stops. My take, from real projects; treat it as a starting point.
- Components and anything with an Owner. As per rule 3, the ownership model already manages those lifetimes elegantly. Interfaces add nothing there — don't fight the framework.
- Working code with clear, local ownership. A
TStringListcreated and freed in the same method withtry/finallyis fine. There is zero need to retrofit interfaces onto code whose ownership story is already obvious. Refactor toward interfaces where you feel the pain (testing, swapping, unclear lifetimes) — not on principle. - Tiny data carriers. For small value-like data, a
recordis often better than either a class or an interface — no heap allocation, no lifetime question at all. - The hottest of hot paths. Reference counting does real (small) work — the count updates are thread-safe atomic operations, and interface method calls are always virtual-style dispatched. For the vast majority of code you will never notice. If you're in a genuinely performance-critical inner loop, measure before committing to either design — the profiler outranks any blog post, including this one.
- A team that isn't on board yet. Consistency within a codebase is worth more than any single technique. Introduce interfaces at natural seams — a new service, a testable boundary — rather than as a big-bang rewrite.
Takeaways
Interfaces are not an advanced curiosity bolted onto Delphi — they're one half of how the language thinks about objects, and in 2026 the story is cleaner than it has ever been.
- An interface is a contract: it declares what, a class provides how. Callers that depend on contracts are easier to test, extend, and change.
- Reference counting is deterministic convenience: interface-held objects free themselves the instant the last reference lets go — the same mechanism you've trusted in strings all along, uniform across all platforms since Delphi 10.4.
- Three habits keep you safe: one reference type per object,
[weak]on back-pointers, and remember components follow the ownership model instead. - Scope it honestly: local
try/finallycode, components, records, and measured hot paths are all places where not using interfaces is the right engineering call. - The ecosystem rewards you: from
IComparer<T>to COM to Spring4D's entirely interface-based collections, this is the idiom the best Delphi code already speaks. For the memory-management deep dive, keep Dalija Prasnikar's book within reach.
A class says how. An interface says what. Learn to separate the two, and both your architecture and your memory management get simpler — one contract at a time.
