One of the defining characteristics of a good programmer is an instinct for keeping implementation details in the correct layer of an application.
That sounds abstract, but it turns out to explain a huge amount of the progress we’ve made in software development over the last twenty-five years.
And nowhere is that clearer than in Perl web development.
Many of us who built web applications during the dotcom boom spent years learning this lesson the hard way.
We wrote CGI programs that:
- parsed HTTP requests
- generated HTML by hand
- connected directly to databases
- embedded SQL inline
- mixed business logic with presentation
- relied on Apache behaviour
- assumed specific filesystem layouts
- and often only worked on one particular server configuration
It all worked. Until it didn’t.
The history of Perl web development is, in many ways, the history of gradually moving implementation details into more appropriate architectural layers.
The Early CGI Years
Early Perl CGI applications were often a single giant script.
You’d open a file and see:
- request handling
- authentication
- HTML generation
- SQL queries
- business logic
- configuration
- deployment assumptions
…all mixed together in a glorious ball of mud.
Something like this:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#!/usr/bin/perl use CGI; use DBI; my $cgi = CGI->new; print $cgi->header; print "<html><body>"; my $dbh = DBI->connect( "dbi:mysql:test", "user", "pass" ); my $sth = $dbh->prepare( "select * from users where id = ?" ); $sth->execute($cgi->param('id')); while (my $row = $sth->fetchrow_hashref) { print "<h1>$row->{name}</h1>"; } print "</body></html>"; |
At the time, this felt perfectly normal.
And to be fair, it was a huge step forward from static HTML sites.
But the design had a fundamental problem:
Everything knew too much about everything else.
The application logic knew:
- how HTTP worked
- how HTML worked
- how Apache launched CGI scripts
- how the database worked
- how the operating system was configured
Every concern leaked into every other concern.
That made systems:
- hard to test
- hard to reuse
- hard to deploy
- hard to scale
- and terrifying to change
The First Big Lesson: Put Logic in Libraries
One of the first signs of a developer maturing is the realisation that application logic should live in reusable modules, not in front-end scripts.
Instead of this:
|
1 2 3 |
if ($user->{status} eq 'gold') { $discount = 0.2; } |
being embedded directly in a CGI script, it becomes:
|
1 |
my $discount = $user->discount_rate; |
That sounds like a small change, but architecturally it’s enormous.
Now the business logic lives in a library.
And once that happens, several good things follow automatically.
Multiple Interfaces Become Possible
If the logic is in modules, then:
- a web front-end
- a CLI tool
- a REST API
- a cron job
- a queue worker
…can all use the same underlying code.
The interface layer becomes thin.
The application itself becomes independent of how users interact with it.
That’s a huge increase in flexibility.
Testing Becomes Easier
Testing CGI scripts was always awkward.
Testing modules is straightforward.
You can instantiate objects, call methods, and inspect results without needing a web server or HTTP requests.
The easier code is to test, the more likely it is to be tested.
And tested code tends to survive longer.
Deployment Becomes Safer
Once the core behaviour is isolated from the interface layer, replacing the interface becomes far less risky.
You can redesign the UI without rewriting the application.
That separation is one of the foundations of maintainable software.
The PSGI Revolution
The next big architectural leap in Perl web development came with PSGI and Plack.
Younger developers may not fully appreciate how painful web deployment used to be.
In the early 2000s, moving an application between hosting environments could require substantial rewrites.
- A CGI application worked one way.
- A mod_perl application worked another way.
- FastCGI had its own quirks.
- Embedded Apache handlers behaved differently again.
Many Perl developers spent years repeatedly rewriting applications simply because deployment environments changed.
That was madness.
The deployment model is an operational concern.
It should not affect application architecture.
PSGI fixed this by defining a standard interface between web applications and web servers.
The core idea was beautifully simple:
A web application is just a function that receives an environment and returns a response.
Once that abstraction existed, applications no longer cared whether they were running:
- as CGI
- under mod_perl
- inside FastCGI
- under Starman
- behind nginx
- on a development laptop
- or inside a cloud container
The deployment details moved down a layer.
Exactly where they belonged.
This was one of the most important architectural improvements Perl web development ever made.
And it reflected a broader truth:
Good abstractions stop lower-level implementation details leaking upward.
The Transitional Era: FatPacker and cpanfile
There was also an interesting intermediate stage between traditional Perl deployments and full containerisation.
For years, one of the hardest parts of deploying Perl applications was dependency management.
You’d move an application to a new server and discover:
- the wrong module version
- missing XS libraries
- incompatible Perl versions
- or an entire dependency tree that worked perfectly on the developer’s machine and nowhere else
Large parts of Perl deployment culture evolved around coping with this problem.
Tools like cpanfile improved things by making dependencies explicit and reproducible.
Instead of vaguely documenting requirements in a README, applications could formally declare:
|
1 2 3 |
requires 'Dancer2'; requires 'DBIx::Class'; requires 'Template'; |
That may seem obvious now, but it was a major improvement in deployment reliability.
Then tools like App::FatPacker went even further by packaging dependencies directly alongside applications.
Instead of relying on the target server’s Perl environment, applications could carry much of their runtime context with them.
These tools didn’t completely solve deployment portability:
- system libraries still mattered
- Perl versions still mattered
- operating system differences still mattered
…but they represented an important shift in thinking.
The industry was gradually realising that:
- deployment environments were part of the application
- reproducibility mattered
- and infrastructure assumptions needed to be controlled
Containers eventually pushed this idea to its logical conclusion by packaging not just Perl dependencies, but the entire runtime environment.
In hindsight, tools like cpanfile and FatPacker were stepping stones toward modern container-based deployment models.
Containers Are the Same Idea Again
Docker and containers are simply the same architectural principle repeated one layer lower.
Before containers, deployments were often fragile and highly environment-specific.
Applications depended on:
- particular Linux distributions
- specific Perl versions
- installed system libraries
- hand-configured servers
- undocumented setup steps
Developers became experts in “works on my machine”.
Operations teams became experts in swearing.
Containers changed the model.
Instead of deploying:
- source code
…you deploy:
- a complete runtime environment
Now the application no longer cares whether it runs:
- on bare metal
- on a VPS
- in Kubernetes
- in ECS
- in Cloud Run
- or on someone’s laptop
Again:
- infrastructure concerns move downward
- application concerns stay upward
The boundaries become cleaner.
The Pattern Repeats Everywhere
Once you notice this pattern, you see it throughout software engineering.
Templates
Template systems separate:
- presentation
from
- application logic
HTML should not contain database code.
Business logic should not contain giant blobs of HTML.
ORMs and Database Layers
DBI separates applications from database engines.
ORMs separate applications from raw SQL structure.
Again:
- implementation details move downward
Configuration
Configuration belongs outside code.
Deployment-specific values should not be embedded in applications.
APIs
Clients should not care whether data comes from:
- PostgreSQL
- Redis
- another service
- a queue
- flat files
- or magic elves
That’s the implementation’s problem.
The Goal Is Not Abstraction for Its Own Sake
Of course, experienced developers also know that abstractions can become ridiculous.
Some abstractions simplify systems.
Others merely hide complexity behind six additional layers of YAML.
Joel Spolsky’s “Law of Leaky Abstractions” remains painfully relevant.
The goal is not abstraction itself.
The goal is to isolate genuinely volatile details.
Good abstractions protect systems from change.
Bad abstractions merely obscure reality.
The Real Skill
The deeper lesson here is that software architecture is largely about deciding:
“What belongs where?”
Experienced developers develop an instinct for:
- which details are likely to change
- which layers should know about which concerns
- and where boundaries should exist
That instinct is often more important than language choice, frameworks, or technology stacks.
And if you spent the early 2000s rewriting CGI applications to run under mod_perl, you probably learned that lesson the hard way.


Excellent article. So how does AI fit into all of this? Well, I believe that this separation of concerns that you have described here is exactly what you need to feed to AI in order to get usable results. The worst thing that you can do is just to tell AI, “Build me this.” Instead, you’re better off asking AI to build each piece and then create the APIs between each piece. We are not yet at the point where we can treat the output of an AI the same way we treat the output of a compiler.