Skip to content

Modern Delphi with Dext: From RAD to Decoupling

“Simplicity is Complicated.” — Rob Pike

If you’ve been programming in Delphi for years, you’ve probably heard terms like “coupling”, “dependency injection”, and “testability”. But what do they really mean in practice? And why should we care if our code has “always worked”?

This article is for you — the Delphi developer who wants to understand these concepts without complicated jargon, and discover how the Dext Framework can transform the way you write code.


The Beauty of RAD: What Delphi Does For You

Section titled “The Beauty of RAD: What Delphi Does For You”

Before talking about “problems”, let’s recognize what Delphi does very well. RAD (Rapid Application Development) is one of the platform’s greatest strengths.

Component Ownership in Delphi

When you drag a TButton onto a TForm, something magical happens behind the scenes:

// Delphi generates this automatically in the .DFM:
object Button1: TButton
Left = 100
Top = 50
Caption = 'Click'
end

The TForm becomes the Owner of the TButton. This means:

  • Automatic memory management: When the Form is destroyed, all child components are destroyed with it.
  • State serialization: The .DFM file saves and restores all properties.
  • Visual design: You see what you’re building in real-time.

For simple and medium applications, this model is perfect:

procedure TForm1.ButtonSaveClick(Sender: TObject);
begin
FDQuery1.SQL.Text := 'INSERT INTO Customers (Name) VALUES (:Name)';
FDQuery1.ParamByName('Name').AsString := EditName.Text;
FDQuery1.ExecSQL;
ShowMessage('Saved successfully!');
end;

It works. It’s quick to write. The customer gets working software in days, not months.


But what happens when your system grows? When you have 50 forms, 200 queries, and need to:

  • Add a new database?
  • Write automated tests?
  • Switch the email sending component?
  • Make the code work on mobile?

Coupling in Traditional RAD

Coupling means that parts of your code are “glued together” — one directly depends on the other.

See this typical example:

unit UFormCustomers;
interface
uses
Vcl.Forms, FireDAC.Comp.Client, Data.DB;
type
TFormCustomers = class(TForm)
FDQuery1: TFDQuery; // <-- Direct dependency
FDConnection1: TFDConnection; // <-- Direct dependency
procedure ButtonSaveClick(Sender: TObject);
end;
implementation
procedure TFormCustomers.ButtonSaveClick(Sender: TObject);
begin
// The Form knows EXACTLY how to save to the database
FDQuery1.Connection := FDConnection1;
FDQuery1.SQL.Text := 'INSERT INTO Customers...';
FDQuery1.ExecSQL;
end;

Problems with this code:

ProblemConsequence
Form knows SQLCan’t reuse logic elsewhere
Form knows FireDACSwitching to dbExpress requires rewriting everything
Logic in event handlerImpossible to test without opening the form
Everything togetherAny change can break something unexpected

Think of coupled code like chewed gum: everything sticks to everything. When you try to pull one part, the others come along.

Decoupled code is like LEGO pieces: each piece has a clear function, fits in a specific place, and can be swapped without destroying the structure.


The Solution: Interfaces and Dependency Injection

Section titled “The Solution: Interfaces and Dependency Injection”

The solution to coupling has two pillars:

  1. Interfaces: Contracts that define WHAT something does, not HOW it does it
  2. Dependency Injection (DI): Instead of creating dependencies, you receive them

Decoupled Architecture with DI

❌ BEFORE (Coupled):

procedure TFormCustomers.Save;
var
Query: TFDQuery;
begin
Query := TFDQuery.Create(nil); // <-- Creates directly
try
Query.Connection := FDConnection1;
Query.SQL.Text := 'INSERT INTO Customers...';
Query.ExecSQL;
finally
Query.Free;
end;
end;

✅ AFTER (Decoupled):

type
ICustomerRepository = interface
['{GUID}']
procedure Save(const Customer: TCustomer);
end;
procedure TCustomerService.Save(const Customer: TCustomer);
begin
FRepository.Save(Customer); // <-- Doesn't know how it works internally
end;

The fundamental difference:

  • Before: The code decides HOW to do it (creates query, connects, executes)
  • After: The code just asks someone to do it (FRepository)

The Dext Framework brings all this modern architecture to Delphi in a simple and elegant way.

Code Reduction with Dext

program HelloDext;
{$APPTYPE CONSOLE}
uses
Dext.Web;
begin
TDextWebHost.CreateDefaultBuilder
.Configure(procedure(App: IApplicationBuilder)
begin
App.MapGet('/hello', procedure(Ctx: IHttpContext)
begin
Ctx.Response.Write('Hello, Dext!');
end);
end)
.Build
.Run;
end.

That’s it. In 15 lines you have an HTTP server running.

The magic of Dext is in the DI container:

TDextWebHost.CreateDefaultBuilder
.ConfigureServices(procedure(Services: IServiceCollection)
begin
// Interface → Implementation
Services.AddScoped<ICustomerRepository, TCustomerRepository>;
Services.AddScoped<ICustomerService, TCustomerService>;
Services.AddSingleton<ILogger, TConsoleLogger>;
end)
.Configure(procedure(App: IApplicationBuilder)
begin
App.MapPost('/customers', procedure(Ctx: IHttpContext)
var
Service: ICustomerService;
begin
// Dext automatically injects the correct service
Service := Ctx.Services.GetRequiredService<ICustomerService>;
Service.Create(Ctx.Request.Body.FromJson<TCustomer>);
Ctx.Response.StatusCode := 201;
end);
end)
.Build
.Run;

The most elegant way is to use constructor injection:

type
TCustomerService = class(TInterfacedObject, ICustomerService)
private
FRepository: ICustomerRepository;
FLogger: ILogger;
public
constructor Create(Repository: ICustomerRepository; Logger: ILogger);
procedure CreateCustomer(const Customer: TCustomer);
end;
constructor TCustomerService.Create(Repository: ICustomerRepository; Logger: ILogger);
begin
FRepository := Repository; // Received, not created
FLogger := Logger; // Received, not created
end;
procedure TCustomerService.CreateCustomer(const Customer: TCustomer);
begin
FLogger.Info('Creating customer: ' + Customer.Name);
FRepository.Save(Customer);
end;

When you register TCustomerService in the container, Dext automatically:

  1. Finds the dependencies (ICustomerRepository, ILogger)
  2. Resolves each one
  3. Passes them to the constructor
  4. Returns the ready instance

The biggest advantage of decoupled code is testability. See how to test TCustomerService without a database:

[TestFixture]
TCustomerServiceTest = class
public
[Test]
procedure Create_ShouldLogAndCallRepository;
end;
procedure TCustomerServiceTest.Create_ShouldLogAndCallRepository;
var
MockRepo: Mock<ICustomerRepository>;
MockLogger: Mock<ILogger>;
Service: ICustomerService;
Customer: TCustomer;
begin
// Arrange: Create mocks
MockRepo := Mock<ICustomerRepository>.Create;
MockLogger := Mock<ILogger>.Create;
// Create service with fake dependencies
Service := TCustomerService.Create(MockRepo.Instance, MockLogger.Instance);
Customer := TCustomer.Create;
Customer.Name := 'John';
// Act: Execute
Service.CreateCustomer(Customer);
// Assert: Verify behavior
MockLogger.Received.Info(Arg.Contains('John'));
MockRepo.Received.Save(Customer);
end;

What this test proves:

  • The service works correctly
  • It logs the action
  • It calls the repository
  • All this without a database, without a server, without a network

Lifetime Scopes: Singleton, Scoped, Transient

Section titled “Lifetime Scopes: Singleton, Scoped, Transient”

Dext automatically manages object lifecycles:

ScopeBehaviorCommon Use
SingletonOne instance for the entire applicationLogger, Configuration, Cache
ScopedOne instance per HTTP requestDbContext, UserSession
TransientNew instance every time you askValidators, Factories
Services.AddSingleton<ILogger, TConsoleLogger>; // Created once
Services.AddScoped<IDbContext, TAppDbContext>; // One per request
Services.AddTransient<IValidator, TValidator>; // Always new

Dext didn’t come to replace RAD — it came to complement it.

Use RAD WhenUse Dext When
Quick prototypesEnterprise applications
Simple CRUDsREST APIs and microservices
Internal toolsCode that needs tests
One person on the projectTeams collaborating

The beauty lies in being able to choose the right tool for each situation. And when you need solid architecture, testability, and maintainability, Dext is ready to help.



Dext Framework — Native performance, modern productivity.