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 flagsInput: keyboard state and edge detectionRenderCore: page lifecycle and primitivesWorld: simulation and collisionUiHud: 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:
- Mode X Part 1: Planar Memory and Pages
- Mode X Part 4: Tilemaps and Streaming
- CONFIG.SYS as Architecture
- The Cost of Unclear Interfaces
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:
- identify one unstable subsystem
- define minimal interface for it
- move internals behind unit boundary
- replace direct global access with explicit calls
- 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.