Project Galahad

Discovering the Delphi way the hard way: my journey from fighting the compiler to embracing it.

There is something fundamentally elegant about Delphi, which is why it is so resilient—and why massive legacy projects are still standing decades later.

I refer to it fondly as the Latin of programming languages.

This post explores a few core types in Project Galahad and how Delphi's pragmatic philosophy shaped their design. It presents an abbreviated version of a longer story, drawing on material from earlier posts.

This project became something of a trial by fire. Pushing Delphi into unfamiliar territory exposed both the remarkable flexibility of the language and the boundaries where it was better to stop pushing. The experience changed the way I think about designing libraries in Delphi, and will shape the reset.


Modern ideas, older roots, Delphi realities

The goal of software architecture is to minimize the human resources required to build and maintain the required system.

Robert C. Martin

Many practices that are now common in software architecture have roots in object-oriented analysis and design, but their most visible modern forms often emerged from the C# and Java worlds. One example is Robert Martin's Clean Architecture, which has influenced many developers and frameworks.

At the same time, many newer practices have emerged from the functional programming tradition, and those ideas are increasingly being absorbed by mainstream languages and frameworks as well.

Delphi sits in an interesting position relative to these developments. It has a rich history and a very pragmatic culture. There are tangible productivity benefits to leveraging RAD and the Delphi ecosystem.

The challenge is to integrate modern principles pragmatically and idiomatically within that rich ecosystem and tradition, to avoid overengineering, and to work within constraints.

To find the Delphi Way of doing things.


A design synthesis

Perfection is unattainable. End of story. Don't waste time chasing it. Instead, aim for 'good enough'—not as an excuse for poor quality, but as a recognition of reality and constraints.

Andrew Hunt & David Thomas

Bringing modern abstractions into Delphi is usually an exercise in balance.

  • preserving the semantic clarity of an abstraction
  • making it behave naturally within Object Pascal

That balance matters. An abstraction can be theoretically faithful, yet still feel awkward in Delphi. Equally, it can be made convenient at the cost of losing its meaning. The compiler will soon let you know if you lose this balance. If you don't find a synthesis, it will feel like wrestling a crocodile.

Instead of fighting the compiler, it's best to think about the problem from a different angle. For example, this beautiful fluent-like code, which emerged when I stopped trying to force a generics-based solution:

var stone := TStoneBuilder.New
    .Weight(RandomRange(1, 5))
    .Texture(lTexture)
    .Sharpness(RandomRange(0, 2))
    .InRoom(lRoom)
    .Build;

Another example is TOption<T>. In languages with algebraic data types, an option type is often just Some(value) or None. In Delphi, record construction and function result handling introduce different constraints. The final shape therefore becomes a compromise: externally it behaves like a normal option type, but internally it adapts to Delphi's record model in order to remain efficient, safe, and ergonomic.

The best approach is to lean into Delphi's strengths: do not force a shape if it is not there. The final expression is very concise as it leverages Result defaulting to none:

function FindFirstStructuralToken: TOption<TToken>;
begin
  // iterate tokens to find structural token
  
  if found then    
    Result.SetSome(token);
end;

One of the recurring lessons I've learned in this project is that good architecture in Delphi cannot simply be imported. It has to be interpreted. Delphi has its own strengths; it is best to embrace them.


Expected outcomes

Only throw an exception when it would be OK for the computer to beep three times.

Eric Lippert (from Rico Mariani)

Taking inspiration from functional languages, and a recent excursion back into C++ with std::expected and std::optional, another important group of types in the library is TOption<T>, TResult<T>, and TStatus.

Taken together, they encourage code that expresses expected outcomes directly: a value may be absent, an operation may succeed or fail, or a process may simply return a status without carrying a value. This makes outcome handling a design time concern rather than a runtime exception.

  • TOption<T> represents the presence or absence of a value
  • TResult<T> represents either a successful value or an error
  • TStatus represents success or failure where no result value is required

This provides a practical, but limited form of railway-oriented programming. Computations can be checked, validated, matched, and safely collapsed without depending entirely on exceptions for ordinary control flow. The emphasis is not on monadic purity, but on making intent explicit, improving readability, and reducing avoidable runtime failures.

These types also support a useful functional principle: code is easier to test and reason about when outcomes are expressed explicitly and functions are kept as free of side effects as possible, rather than allowing behaviour to emerge through hidden side effects such as exceptions.

Delphi does not allow a generic record to refer to another specialization of itself within its own declaration. In practice, this means TResult<T> cannot directly declare a method such as:

function TResult<T>.Bind<U>(Func: TFunc<T, TResult<U>>): TResult<U>;

TResultOp exists as a pragmatic workaround. It moves away from a fully fluent style and instead uses function-wrapping for operations that cannot be expressed cleanly on TResult<T> itself.

I originally tried several approaches to keep functions such as Bind inside the pipeline, but the workarounds introduced too much complexity. It proved cleaner to separate the two styles. This is one of those cases where you simply smile at the compiler and whisper, "okay, you win — let's move on."

In retrospect, I think outcome-based types in Delphi should remain simple and practical. The focus should be on the value they provide — making expected failure explicit — rather than trying to coerce Delphi into a railway-oriented programming style.

Result and Maybe are useful tools in their own right; they do not need an entire functional programming model around them to justify their existence.


TScope

If you need more than 3 levels of indentation, you're screwed anyway, and should fix your program.

Linus Torvalds

One of the most practically useful types in the library is TScope. Its purpose is simple: reduce the burden of nested try/finally blocks and make ownership clearer. It tracks owned objects and executes deferred actions on leaving scope. The idea was inspired by Go's defer, and C#'s using declaration.

  • objects can be explicitly owned by the scope
  • cleanup actions can be queued with Defer
  • the code becomes flatter and easier to read
  • objects can be released from the scope for safe return
// instead of nested try..finally blocks:
var scope: TScope;
begin
  // automatically freed when the procedure ends
  var list := scope.Owns(TStringList.Create); 
  var map  := scope.Owns(TDictionary<string, string>.Create);
  
  // optional custom cleanup (Go-style defer)
  scope.Defer(procedure begin Writeln('Closing database connections...'); end);
  
  list.Add('Galahad makes this easy.');

  // more optional custom cleanup, queued after the first deferred block
  scope.Defer(procedure begin Writeln('Closing down COM...'); end);

  // if there is an exception, list is cleaned up; otherwise we want to return it.
  Result := scope.Release(list);
end; 

This is an example of where Delphi may not have a feature, yet offers the capabilities needed to create it.


DynamicObject

Simple things should be simple, complex things should be possible.

Alan Kay

The Base.Dynamic unit takes its inspiration from Ruby's method_missing and C#'s DynamicObject.

It introduces dynamic dispatch into Delphi through IDispatch, OleVariant, RTTI, and MethodMissing. It basically repurposes COM's plumbing making it possible to respond dynamically, opening the door to:

  • Ruby-style method_missing
  • dynamic decorators
  • interception and proxying
  • DSL-style surfaces
  • mocking-style patterns
  • dynamic XML access
  • ActiveRecord-style patterns

Here is a simple example, the car type doesn't exist:

var car := TXml.New('car');

car.id    := 'CAR-1001';
car.year  := 2022;
car.color := 'Blue';
car.reg   := 'AB12 CDE';

car.specs['make']  := 'Toyota';
car.specs['model'] := 'Corolla';

var xml := car.AsPrettyXml;

Base.Xml provides the XML types, including a minimal but useful parser. This unit can be improved with helpful types and abstractions, including adapters.

A similar DSL is planned for Base.Json in a future version of the library.

Here is a more extreme dynamic example, where direct text is sent to a repository:

TDynamicHelper.Invoke(fCustomers, 'FindByDeptIdAndLevel', [ 1, 'M1']);

This mechanism relies on COM technologies, so it is limited to Windows applications. The ability to bridge dynamic and static behaviour in this way is a good example of why Delphi remains such a strong platform for Windows desktop development: it has first-class COM support, native language support for Windows messages, direct access to the message pump, and close integration with the Windows API.


Stream

The purpose of a programming language is to help the programmer express their thoughts as clearly as possible.

Niklaus Wirth

Inspired by Java's Stream, this type provides an eager, declarative pipeline for collection processing.

Its intended pattern is simple:

  • ingest
  • transform
  • terminate

It provides filtering, mapping, grouping, set-style operations, partitioning, and reduction.

var spec := TDepartmentIs.Create('IT').AndAlso(TSalaryAbove.Create(68000));

var names := Stream
    .From<TCustomer>(fCustomers)
    .Filter(spec)
    .Map<string>(function(const c: TCustomer): string begin Result := c.Name; end)
    .AsArray;

Much of this is possible because Delphi's generics, anonymous methods, records, and managed types form a capable foundation. Together they make it possible to build expressive pipeline types while retaining memory safety, value semantics, and predictable behaviour.


Boundary adapters

One of the more architectural ideas is the use of boundary adapters.

Types such as TItems<T>, TSource<T>, and TIndex<T> exist to normalize inputs at API boundaries. Instead of multiplying overloads for arrays, lists, enumerables, slices, segments, and sequences, these adapters provide a compact way to accept "something collection-like" in a controlled form.

It makes collection APIs easier to design coherently. Each adapter has a role:

  • TItems<T> is a general detached input adapter
  • TSource<T> is a higher-level adapter for collection-layer types
  • TIndex<T> is a lighter indexed boundary object where full detachment is not always needed

These types reduce API noise and generics bloat.

// supports items, list, enumerable, enumerator, dynamic array arguments
function Concat(const aOther: TItems<T>): TSequence<T> overload;

This is a different approach to solving the problem. It layers an abstraction over existing types. Other solutions pursue consistency by introducing cleaner foundational types.

This is an area that I intend to revisit. The underlying goal remains useful — normalizing awkward API boundaries, reducing bloat, and making intent explicit — but the solution can be simpler.


Slicing and Dicing

Several related collection types have been shaped as an integrated suite.

These four types work together:

  • TSequence<T> — a detached value sequence
  • TSegment<T> — a borrowed readonly contiguous view
  • TSlice<T> — a borrowed mutable contiguous view
  • TSelection<T> — a borrowed ordered non-contiguous projection

Taken together, they express an important distinction:

  • detached values
  • borrowed views
  • arbitrary selections

As a coherent group, they form a useful collection model. A TSequence<T> has stable value semantics, and is used to materialize segments and slices. It also doubles as a lightweight alternative to Stream:

var s := TSequence<integer>
           .From([5, 3, 1, 3, 2, 4, 2, 5, 4])
           .Distinct
           .Sorted
           .Skip(1)
           .Take(3)
           .Reversed;

A TSegment<T> or TSlice<T> is explicitly tied to a source list, providing a range-based view.

A TSelection<T> models sparse picks across one or more lists, which makes it useful for things like selected cells, board diagonals, and similar non-contiguous structures:

{ create the board with a list }

var board := TList<string>.Create;

board.AddRange([
  'X', ' ', 'O',
  ' ', 'X', ' ',
  'O', ' ', ' '
]);

{ create a slice and set X in the last item }

var s := TSlice<string>.From(board, 6, 8);
s[2] := 'X';

{ create a selection and check for a win }

var mainDiagonal := TSelection<string>.Create;

mainDiagonal.Add(board, 0); // top-left
mainDiagonal.Add(board, 4); // center
mainDiagonal.Add(board, 8); // bottom-right

var isWin := mainDiagonal.All('X');

{ a selection is mutable, but it is nice to show a slice example }

TSlice<T> and TSegment<T> were inspired by Rust's Slice and C#'s Span.

These types build naturally on collections already present in the RTL.

This is another area I would like to revisit.

On reflection, I think the problem would be better served by a good foundational abstraction, whilst still respecting the principle of integrating rather than replacing existing types.


When Convenience Becomes Complexity

Another useful lesson came from TConvert, a utility class created to standardize data conversion throughout the codebase. It began with a practical problem.

I wanted date and time values represented consistently as ISO UTC internally, particularly when communicating with APIs and performing calculations, while converting them to local time when presented in the UI. Centralizing those conversions made the rules explicit and kept them consistent.

The idea worked well, so I kept adding conversions.

Eventually TConvert accumulated so many functions that it was no longer obvious which one to use.

This was another lesson in pragmatism.

Sometimes the better library is the one that does less.


Specifications

Business software spends a great deal of time answering questions:

  • Is this order high value?
  • Has payment been confirmed?
  • Does this customer qualify for priority handling?
  • Does this order require special handling?

These rules often begin life as conditional expressions scattered throughout an application. The Specification Pattern gives them a name and a home.

A specification is simply a reusable business rule that answers a yes/no question about a candidate:

ISpecification<T> = interface
    function IsSatisfiedBy(const Candidate: T): Boolean;
end;

Project Galahad extends this idea with composition through AndAlso, OrElse, and NotThis. Small rules can therefore be combined into richer business concepts:

TOrderTotalAtLeastSpecification.Create(1000.00)
    .AndAlso(TPaymentConfirmedSpecification.Create)
    .AndAlso(TPriorityCustomerSpecification.Create)
    .AndAlso(THighRiskOrderSpecification.Create(40).NotThis);

I particularly like the separation this produces:

  • the domain contains entities
  • specifications define predicates
  • services compose those predicates into business meaning

This structure bears an interesting resemblance to the classical Three Acts of the Mind:

  • Domain — Simple Apprehension — the ideas (entities) of the system
  • Specifications — Judgment — logical predicates that affirm or deny qualities of those ideas
  • Services — Reasoning — composing individual judgments into richer determinations

The specification implementation itself worked well. Complexity began to creep in when I integrated specifications with the database abstractions. Specifications could no longer remain simple predicates; SQL builders and supporting machinery were required to translate them into database queries, adding complexity to both sides of the design.

This is something I would approach much more carefully in the future. Infrastructure of this kind needs to be sufficiently well designed that it becomes almost transparent—or not done at all.


Global Functions as Extension Points

One of the hidden powers in Delphi is the ability to add global functions. Delphi doesn't have value deconstruction, nor native support for tuples, but we're not without options:

TLx.Let<integer>(x1, y1, x2, y2, GetSavedWindowCoordinates);

Or taking inspiration from Kotlin's also:

TLx.Let<integer>(a, b, [b, a]);

Currently TLx aggregates "language extensions" because of its support for generics, but simple functions for core primitives are on the horizon, allowing a simpler expression:

Let(x1, y1, x2, y2, [0, 0, 100, 100]);

These examples are for assignment and collection deconstruction, but a policy could be introduced to make this available for general type deconstruction; leveraging an interface contract or even RTTI.


Elegant Coupling

Coupling to stable or pervasive elements is seldom a problem.

Craig Larman

Another good use of global functions is to expose shared application primitives elegantly.

Some types naturally behave as long-lived application services, and in those cases direct access can be acceptable, avoiding unnecessary ceremony. The application event bus in Galahad is a good example:

// function exposing the event bus
function JobsEventBus: TEventBus<TJobEvent>; 

// subscribing
JobsEventBus.Subscribe<TJobsRetrievedEvent>(OnJobsRetrieved);

// publishing
JobsEventBus.Publish<TJobsRetrievedEvent, TJobEvent>(e, group);

Again, it is about being practical.


Data Abstractions

  • Where does transaction scope live?
  • How do we avoid global database singletons?
  • How do we support multiple providers in the future?
  • How do we ensure deterministic startup order?
  • How do we integrate with DataModule-managed connections?
  • How do we remain simple without becoming a full-blown ORM?

Base.Data is the provider-agnostic answer to those questions. It separates database contexts from active sessions, supports startup hooks for post-connection policy, provides an ambient model for transaction boundaries, and adds lightweight persistence features driven by demand: entity mapping, repositories, specification-to-SQL translation, migrations, and DataModule connection configuration.

At the repository boundary, that looks like this:

IJobRepository = interface(IDbSet<IJob, TJob>)
  ['{9C132F99-B4FF-4C86-83B8-26268A26490F}']
end;

The data abstractions in Galahad have proven effective, but this is another area I would like to revisit. In particular, the configuration and bootstrapping code is more rigid than I would now like. I intend to look more closely at the patterns used by ASP.NET Core, where dependency injection, configuration, and application composition are treated as first-class concerns.

The goal is not to reproduce ASP.NET Core in Delphi, but to learn from the flexibility of its approach and find a simpler, more idiomatic way to achieve something similar.


Dependency Management

In modern architectures, the dependency injection container is often a foundational component. It supports the SOLID Dependency Inversion Principle and fits naturally within Clean Architecture.

In Delphi, there are two challenges:

  1. building the abstraction well
  2. deciding where it should be applied

The container is capable and disciplined, but its lifetime model is currently simple: singleton or transient.

Scoped lifetimes need deeper consideration, because client-side applications do not present a natural scope as clearly as server-side systems do. In the past, thread-static storage offered a boundary, but asynchronous messaging blurs that line. Delphi's thread-bound task model still makes this workable, but I think it is better to let the right abstraction emerge naturally through usage.

The container supports both service registration and module composition:

// add a root composition module
ApplicationBuilder.AddModule<TApplicationModule> 
  
// this in turn can add other modules  
procedure TApplicationModule.RegisterServices(const c: TContainer);
begin
  c.AddModule<TServiceModule>;
  c.AddModule<TDataServiceModule>;
  c.AddModule<TUseCaseModule>;
end;

// these can add further modules, or in this case, related services
procedure TDataServiceModule.RegisterServices(const c: TContainer);
begin
  c.Add<IJobRepository, TJobRepository>;
  c.Add<ISearchRepository, TSearchRepository>;
end;

It also supports aliases, allowing different layers to see narrower views of the same registered type:

// the application settings has the widest window
c.Add<IApplicationSettings, TApplicationSettings>(Singleton);

// and there's an alias for foundational code
c.AddAlias<ISettings, IApplicationSettings>;

Because Delphi links away unreferenced code, automatic attribute-based registration is impractical. It would be helpful if attributed types could be marked as referenced for linking purposes.

The second difficulty relates to applying constructor injection in the UI layer.


Embrace Conventions

When in Rome, do as the Romans do.

St Ambrose

There are four common ways to surface dependencies:

  1. constructor injection
  2. metadata
  3. configuration
  4. conventions

Delphi is a RAD tool, and much of its work is done at design time through components, forms, frames, controls, and third-party libraries. In these cases, constructor injection is often not a natural fit. Delphi, however, is rich in conventions, and those conventions can be very effective for managing dependencies.

The DataModule is a foundational powerhouse in Delphi and it is pervasive. It is the natural place to hold and coordinate connections, data sources, and related controls. In some respects, it mirrors a domain aggregate—but as a highly-prevalent, first-class citizen of the UI layer.

That symmetry can be useful, and it can be turned into a practical convention.

For example, if there is a JobDataModule, the form can ask that DataModule for a JobRepository. This is a simple and easily understood convention.

fJobRepository := JobDataModule.GetJobRepository;

In that role, the DataModule is allowed to use the Service Locator pattern. In this context, it is better understood as a pragmatic convention than as an antipattern. This removes the need to engineer a more complex solution simply to force dependency injection where it does not fit naturally.


Units as vertical slices

Unless there is a real intention to distribute code on different servers, keep all the code that implements a single conceptual object in the same MODULE, if not the same object.

Eric Evans

The Delphi unit is a meaningful architectural scope, and a very natural home for a vertical slice.

In DDD terms, a vertical slice groups all the artifacts related to a domain concept, such as Customer, together, instead of separating them into technical buckets such as Entities, Repositories, and Specifications. Code is organized by feature and concept, not merely by type.

This groups related behaviour and helps avoid a proliferation of disconnected classes.

Project Galahad's database ambitions are minimal, so units as vertical slices are a good fit. For larger applications, file-based vertical slices may be more practical. A good rule is to default to the unit as the meaningful scope, and let real complexity drive further separation.


Sometimes Old is Gold

Facade defines a higher-level interface that makes the subsystem easier to use.

the Gang of Four

Another lesson reinforced by this work is that older styles of abstraction still have real value in Delphi.

A façade, or what might now be called an old-fashioned API, can be a powerful way to simplify UI code, preventing forms from becoming tangled with application logic, and keeping coordination in the right place. Not every dependency needs to be pushed directly into every consumer, and not every design becomes better by being made more theoretically pure.

var count := JobsFacade.ImportLatestJobs;

Sometimes the best solution is a small, clear surface that hides complexity and presents only what the caller actually needs. The goal is always simplicity, clarity, and maintainability.

This approach harkens back to the traditions of desktop development, where it remains a natural fit.


Integrate and augment

I have made this letter longer than usual because I have not had time to make it shorter.

Blaise Pascal

The project's principles gradually settled into:

  • integrate
  • augment
  • simplicity
  • clarity
  • minimalism
  • opt-in
  • opinionated

Delphi has strong foundational types in the RTL, and it is supported by excellent third-party components, frameworks, libraries, and ORMs that solve existing problems very well.

The purpose has become clear: do not compete, assist.

It's about filling the gaps, not replacing the foundations.

The challenge with opt-in is coupling:

  1. opt-in requires loose coupling
  2. the value of a library is in the synergy of its types

A practical solution is to collapse core types into a single unit, reducing individual unit dependencies.

// opinionated architecture
var app := ApplicationBuilder
                .AddModule<TApplicationModule>
                .LoadSettings<IAppSettings>
                .ConfigureDatabase
                .PerformMigrations
                .Build;

app.Execute;

A related tension is this:

  1. opt-in requires loose coupling
  2. opinionated architecture requires determination and coupling

This tension is most obvious in the application builder, which currently pulls in host composition, database, and migration concerns. Vertical substacks may be the solution.


Shorter Feedback Loops

One of the more practical lessons in this work was psychological.

Long periods of coding without feedback are exhausting. Building a framework in Delphi can be draining; you hit all the edges, and it often feels like a fight against the metal.

Without something concrete pushing back, it also becomes harder to tell whether the design is improving or simply drifting upward into abstraction.

Pivoting to small demonstration applications was one of the most pragmatic moves in the project. Their constrained scope made meaningful progress possible. Useful ideas proved themselves quickly, awkward edges became visible sooner, and weaker abstractions were exposed before they had time to harden. Just as importantly, the work became fun and energising.

That discipline means leaving some things undone.

For example, IDbSet<T> does not yet expose a delete method. Adding one would be easy, but the current demonstration only requires insert and update, so deletion remains out of scope.


The Architecture of Pragmatism

Before Delphi, you always had to make a choice: do I go for the performance of a native code compiler, or the ease of use of a visual development environment? What programmers really want is all of the above, in one package. That's what we set out to do.

Anders Hejlsberg, EXE Magazine Interview

Search deeply enough for Delphi's DNA, and you discover that much of what can appear old-fashioned or unusual elsewhere makes remarkable sense when viewed through the lens of native Windows desktop development:

  • A Language Tuned for the Desktop: While Object Pascal is a vast, general-purpose language with deep roots in Pascal, Delphi evolved in close alignment with desktop development. Features such as published RTTI, IDispatch, Variant, COM-based interfaces, the published and message keywords, the owner pattern, properties, events, flexible constructor resolution, TClass, and class-level polymorphism all reflect a language and runtime shaped for components, visual designers, object streaming, event-driven programming, and close cooperation with the operating system.
  • An RTL Shaped for RAD: The RTL is engineered for interaction. From TPersistent and visual property streaming, to virtual hooks on TObject such as AfterConstruction, to TBasicAction and TComponent in System.Classes, to TList exposing notification hooks, the RTL reflects a system shaped around component-driven development. Delphi bakes extension seams into foundational types because the ecosystem expects interaction, observation, design-time tooling, and UI-adjacent behavior to matter.
  • The Compiler's Efficiency: A fast compiler is a prerequisite for a RAD tool, and Delphi's native compiler has long been part of that appeal. Its fast edit-compile-run cycle, relatively direct compilation model, and production of native binaries make for an efficient development experience.
  • System Integration: Delphi does not merely abstract over Windows; it works in close cooperation with it. The VCL gives developers productive high-level abstractions without hiding the underlying platform. Window handles, Windows messages, COM and Automation, shell services, native controls, Win32/Win64 APIs, and WinRT remain readily accessible. A form can handle a Windows message directly; a control can expose its HWND; application code can call a Windows API when the framework does not provide the required functionality. Modern VCL continues this relationship with Per-Monitor V2 High DPI support, WebView2 integration through TEdgeBrowser, refreshed Windows and WinRT API mappings, and support for contemporary Windows deployment and platform capabilities. The abstraction is deliberately permeable: use the framework where it helps, and reach through to Windows when necessary.

That specialization has become unexpectedly valuable. With .NET Framework now in maintenance mode, UWP having been sunset as Microsoft's strategic application platform, and Microsoft once again investing heavily in the Win32 API, Delphi finds itself in a remarkably strong position. The close relationship with Windows that once appeared to risk making Delphi obsolete has instead become one of its greatest strengths.

The VCL was built for Windows, and decades of continued refinement have deepened rather than erased that specialization. As long as native Windows desktop development remains important, Delphi's accumulated investment in the platform remains an extraordinary asset.

This is the architecture of pragmatism.


The Delphi Way

The formal specification of the Delphi language is simple: Delphi is whatever the compiler accepts.

Danny Thorpe

I began this project trying to force modern engineering abstractions into Delphi verbatim; I ended up discovering what I believe is Delphi's core philosophy and a principle to guide implementation decisions.

Throughout this process, I have come to treasure Delphi's managed types. They are hidden gems. The more they form the backbone of an application, the more solid and dependable the foundation tends to become. Strings, interfaces, dynamic arrays, and records are worth their weight in gold.

In my opinion, the single word that best sums up the Delphi way is pragmatism.

Writing clean code in Delphi requires you to be pragmatic; to innovate, you must be both idiomatic and practical. That focus on simplicity and pragmatism explains why Delphi remains relevant to this day.


Project Galahad — An Update

Since writing this article, I have decided not to pursue Project Galahad any further.

That does not mean I consider the project a failure. Quite the opposite. Project Galahad became the cauldron in which I learned some hard-fought lessons about Delphi, its strengths, its boundaries, and the importance of designing with the language rather than against it.

I intend to take those lessons forward into a new library. It will be less ambitious, smaller, simpler, and more pragmatic. Rather than trying to introduce broad abstractions from other ecosystems, it will concentrate on solving useful problems in ways that feel natural in Delphi.

In other words, I intend to follow the principles described in this article more closely: integrate rather than replace, augment rather than compete, and respect the compiler as a good friend rather than something to be fought.

Project Galahad served its purpose. It was a titanic struggle, but worth every ounce of effort.

What comes next will be built from the lessons it taught me — the Delphi Way.

Project Galahad is dead. Long live Project Galahad.