Turbo Pascal Units as Architecture, Not Just Reuse

Turbo Pascal Units as Architecture, Not Just Reuse

Most people first meet Turbo Pascal units as “how to avoid copy-pasting procedures.” That is true and incomplete. In real projects, units are architecture boundaries. They define what the rest of the system is allowed to know, hide what can change, and make refactoring survivable under pressure.

In constrained DOS projects, this was not academic design purity. It was the difference between shipping and debugging forever.

Interface section is a contract surface

A good unit interface exposes minimal, stable operations. It does not leak storage details, timing internals, or helper routines with unclear ownership. You can read the interface as a capability map.

unit RenderCore;

interface
procedure BeginFrame;
procedure DrawSprite(X, Y, Id: Integer);
procedure EndFrame;

implementation
{ internal page selection, clipping, palette handling }
end.

Notice what is missing: page indices, raw VGA register details, sprite memory layout. Those remain private so callers cannot create illegal states casually.

Separation patterns that work

A practical retro project often benefits from explicit layers:

  • SysCfg: startup profile, paths, feature flags
  • Input: keyboard state and edge detection
  • RenderCore: page lifecycle and primitives
  • World: simulation and collision
  • UiHud: overlays independent of camera

Each layer exports what the next layer needs, and no more.

This is still modern architecture wisdom, just with smaller tools.

Compile-time feedback as architecture feedback

One advantage of strong unit boundaries: breakage appears quickly at compile time. If you change a function signature in one interface, all dependent call sites surface immediately. That pressure encourages deliberate changes rather than implicit behavior drift.

When architecture boundaries are vague, breakage tends to become runtime surprises. In DOS-era loops, compile-time certainty was a strategic advantage.

State ownership rules

Global variables are tempting in small projects. They also erase accountability. Better pattern:

  • each unit owns its state
  • mutation happens through explicit procedures
  • read-only queries are exposed as functions
unit FrameClock;

interface
procedure Tick;
function FrameCount: LongInt;

implementation
var
  GFrameCount: LongInt;

procedure Tick;
begin
  Inc(GFrameCount);
end;

function FrameCount: LongInt;
begin
  FrameCount := GFrameCount;
end;
end.

This small discipline scales surprisingly far.

Circular dependencies are architecture warnings

If Unit A needs Unit B and B needs A, the system is signaling a design issue. In Turbo Pascal this becomes obvious quickly because cycles are painful. Use that pain as feedback:

  • extract shared abstractions into Unit C
  • invert direction of calls through callback interfaces
  • move policy decisions up a layer

The language/tooling friction nudges you toward cleaner dependency graphs.

Testing mindset without modern frameworks

Even without a test framework, you can create deterministic validation by small harness units:

  • fixture setup procedure
  • operation call
  • assertion-like result check
  • text output summary

The key is isolating seams through interfaces. If a rendering unit can be called with prepared buffers and predictable state, manual regression checks become cheap and reliable.

Architecture and performance are not enemies

Some developers fear unit boundaries will cost speed. In most DOS-scale projects, the bigger performance wins come from algorithm choice and memory locality, not from collapsing all code into one monolith. Clear units help you identify hot paths accurately and optimize where it matters.

For example, keeping low-level pixel paths inside RenderCore makes targeted optimization straightforward while preserving clean call sites elsewhere.

Cross references in this project

These articles show the same pattern from different angles:

Different domains, same operational truth: explicit boundaries reduce failure ambiguity.

A migration strategy for messy codebases

If you already have a tangled Pascal codebase, do not rewrite everything. Do staged extraction:

  1. identify one unstable subsystem
  2. define minimal interface for it
  3. move internals behind unit boundary
  4. replace direct global access with explicit calls
  5. repeat for next subsystem

This approach keeps software running while architecture improves incrementally.

Turbo Pascal units are sometimes framed as nostalgic language features. They are better understood as practical architecture tools with excellent signal-to-noise ratio. Under constraints, that ratio is everything.

2026-02-22