Are Web Applications Too Complicated?

Are web applications complicated to build? Do we overcomplicate them when they should be, somehow, simpler? This is a question that I've wondered about for many years. In this post, come on a journey with me, back 25 years, to when I chatted about this with an old mainframe programmer.

Want to learn more about Docker?

Are you tired of hearing how "simple" it is to deploy apps with Docker Compose, because your experience is more one of frustration? Have you read countless blog posts and forum threads that promised to teach you how to deploy apps with Docker Compose, only for one or more essential steps to be missing, outdated, or broken?

Check it out

Back in 2000 or 2001, I was working out on a client site, and worked alongside an old mainframe developer. Over the course of the, likely, brief time that we worked alongside one another, besides generally getting to know one another, we got to talking about the kind of software that we were each working on.

He was writing COBOL, I believe. What it was for, I don't remember, exactly. Me? I was working on a Visual Basic 6 application that integrated with a Microsoft Access database. VB6 and Microsoft Access? Matt, I hear you ask, that's not a web-based application.

And you'd be correct. It wasn't that application that I got talking to him about, because I wanted to work on anything but that application. Rather, the one that I talked to him about was a project for another client. This was one of my first web-based applications in PHP.

It was, as so many were at the time, a tight integration of PHP and MySQL. That application's architecture, I shared with great enthusiasm. I told him how the share-nothing web request model worked, where requests would come to the webserver and requests for dynamic content would be handed off to the PHP runtime.

I told him how PHP would interact with MySQL, querying it for the information that it needed, or to persist some change to said information. I told him how PHP would dynamically render the content which the user saw. Remember how it was all intertwined in those days - in one big file (or two)?

I was super-passionate about how it all worked and how it was a fascinating challenge to keep in your head how everything worked. When I stopped to draw breath, I asked him what he thought. He remarked that it all sounded too complicated for him.

I was floored and didn't know what to say. I just hadn't expected that response. I expected that he would be as excited and enthusiastic as I was.

After I moved past my shock, I asked him how he wrote the applications that he was working on. He pointed to his screen and said that everything he needs was in one, large file. He knew where everything was laid out, and he just needed to go to the relevant section to make a given change.

At the time, I couldn't fathom how old-fashioned and uninteresting that his way of building applications sounded. Honestly, I wondered why anyone would want to write code in that way. If you're not familiar with COBOL, you might be wondering what I'm talking about. Well, while it's been years since I've written any, I did a brief refresher for the purposes of this article.

If you're keen to follow along, install gcobol with your package manager of choice. I'm using Ubuntu 25.10, so here's how I installed it:

sudo apt-get install -y gcobol

Then, in your text editor of choice, hopefully with syntax-highlighting enabled, create a file where you keep code files and paste the following into it:

IDENTIFICATION DIVISION.
PROGRAM-ID. VARS.

DATA DIVISION.
    *> working storage defines variables
    WORKING-STORAGE SECTION.
    *> define a number with a sign, 3 numbers, a decimal, and then
    *> two numbers aafter the decimal. by default it should be 0 filled
    01 FIRST-VAR PIC S9(3)V9(2).
    *> do the same thing as above but actually initialize
    *> to a number -123.45
    01 SECOND-VAR PIC S9(3)V9(2) VALUE -123.45.
    *> defines an alphabetic string and initialize it to abcdef
    01 THIRD-VAR PIC A(6) VALUE 'ABCDEF'.
    *> define an alphanumeric string and initialize it to a121$
    01 FOURTH-VAR PIC X(5) VALUE 'A121$'.
    *> create a grouped variable
    01 GROUP-VAR.
    05 SUBVAR-1 PIC 9(3) VALUE 337.
    *> create 3 alphanumerics, but use less than
    *> the allocated space for each of them
    05 SUBVAR-2 PIC X(15) VALUE 'LALALALA'.
    05 SUBVAR-3 PIC X(15) VALUE 'LALALA'.
    05 SUBVAR-4 PIC X(15) VALUE 'LALALA'.

*> print our variables
PROCEDURE DIVISION.
    DISPLAY "1ST VAR :"FIRST-VAR.
    DISPLAY "2ND VAR :"SECOND-VAR.
    DISPLAY "3RD VAR :"THIRD-VAR.
    DISPLAY "4TH VAR :"FOURTH-VAR.
    DISPLAY "GROUP VAR :"GROUP-VAR.
    STOP RUN.

Now, let's step through the code. There are three sections in the file:

  • Identification Division: The Identification division provides metadata about the program.
  • Data Division: In the Data division, all the variables and data structures used by the program are declared.
  • Procedure Division: The Procedure division contains the code that defines the program’s operations

There are more sections available, but this program only uses the above three. Any line starting with *> is a comment. Otherwise, the DATA DIVISION defines five variables: FIRST-VAR, SECOND-VAR, THIRD-VAR, FOURTH-VAR, and GROUP-VAR. The PROCEDURE DIVISION then prints the value of each one using basic string interpolation. There's not much to it, but it works.

With my limited knowledge of COBOL, I don't know just how much can be accomplished with it. But, if nothing else, while somewhat cumbersome, which is understandable given the state of computing when COBOL was first developed back at the end of the 1950s.

Now, with a simple text editor, the above code would be easy enough to maintain. But, this is hardly indicative of code that you would find in any modern bank or insurance company, I'm thinking. Code that you'd find there would be significantly more involved, which leads me to think that the file sizes would also be notably larger. So, without a decent editor, it could be quite challenging to maintain. But, at least there's one key advantage: you only need to edit a single file.

It wasn't until later that I came to wonder if he had it right, or whether I had just accepted an over-complicated approach to development. I continued to ponder that question for years to come.

Contrast that with web application development. Specifically, here are most of the files in a small web project:

.
├── bin
│   └── clear-config-cache.php
├── codeception.yml
├── composer.json
├── composer.lock
├── compose.yml
├── config
│   ├── autoload
│   │   ├── blog.global.php
│   │   ├── dependencies.global.php
│   │   ├── development.local.php.dist
│   │   ├── local.php.dist
│   │   ├── logger.global.php
│   │   ├── mezzio.global.php
│   │   └── twig.global.php
│   ├── blog.global.php
│   ├── config.php
│   ├── container.php
│   ├── development.config.php.dist
│   ├── pipeline.php
│   └── routes.php
├── COPYRIGHT.md
├── data
│   ├── cache
│   └── logs
│       └── app.log
├── docker
│   ├── apache
│   │   ├── conf-enabled
│   │   │   └── docker-php.conf
│   │   ├── ports.conf
│   │   ├── redirects.txt
│   │   └── sites-enabled
│   │       └── 000-default.conf
│   ├── logrotate
│   │   └── logrotate.d
│   │       └── mezzio-blog
│   └── php
│       ├── conf.d
│       │   ├── error.ini
│       │   ├── error_reporting.ini
│       │   ├── opcache.ini
│       │   └── variable-order.ini
│       └── php.ini-development
├── Dockerfile
├── fly.toml
├── LICENSE.md
├── package.json
├── package-lock.json
├── phpcs.xml.dist
├── phpunit.xml.dist
├── psalm-baseline.xml
├── psalm.xml.dist
├── README.md
├── src
│   ├── App
│   │   ├── src
│   │   │   ├── ConfigProvider.php
│   │   │   ├── Factory
│   │   │   │   └── MarkdownRuntimeLoader.php
│   │   │   ├── Handler
│   │   │   │   ├── HomePageHandlerFactory.php
│   │   │   │   ├── HomePageHandler.php
│   │   │   │   └── PingHandler.php
│   │   │   ├── Listener
│   │   │   │   ├── LoggingErrorListenerDelegatorFactory.php
│   │   │   │   └── LoggingErrorListener.php
│   │   │   ├── Pagination
│   │   │   │   └── Paginator.php
│   │   │   └── Twig
│   │   │       ├── PaginationExtension.php
│   │   │       └── RuntimeLoader.php
│   │   └── templates
│   │       ├── app
│   │       │   └── home-page.html.twig
│   │       ├── blog
│   │       │   └── twig
│   │       │       ├── blog-article.html.twig
│   │       │       ├── blog.html.twig
│   │       │       └── includes
│   │       │           └── pagination.html.twig
│   │       ├── error
│   │       │   ├── 404.html.twig
│   │       │   └── error.html.twig
│   │       ├── layout
│   │       │   ├── blog.html.twig
│   │       │   └── default.html.twig
│   │       └── static-pages
│   │           ├── about.html.twig
│   │           ├── contact.html.twig
│   │           ├── cookiepolicy.html.twig
│   │           ├── disclosurepolicy.html.twig
│   │           ├── portfolio.html.twig
│   │           └── privacy.html.twig
│   └── Assets
│       └── css
│           └── styles.css
├── test
│   └── AppTest
│       ├── Handler
│       │   ├── HomePageHandlerFactoryTest.php
│       │   ├── HomePageHandlerTest.php
│       │   └── PingHandlerTest.php
│       ├── InMemoryContainer.php
│       ├── Pagination
│       │   └── PaginationTest.php
│       └── Twig
│           └── RuntimeLoaderTest.php
├── tests
│   ├── Acceptance
│   │   └── CheckRoutesCest.php
│   ├── Acceptance.suite.yml
│   ├── Functional
│   ├── Functional.suite.yml
│   ├── Support
│   │   ├── AcceptanceTester.php
│   │   ├── Data
│   │   ├── FunctionalTester.php
│   │   ├── _generated
│   │   │   ├── AcceptanceTesterActions.php
│   │   │   ├── FunctionalTesterActions.php
│   │   │   └── UnitTesterActions.php
│   │   ├── Helper
│   │   └── UnitTester.php
│   ├── Unit
│   └── Unit.suite.yml
├── unused-images.txt
└── used-images.txt

There are view templates, PHP source files, test files (the project uses both PHPUnit and Codeception), there's a small Docker-based configuration, and configuration files for PHP's QA tools, just to name a few.

Which one do you think is easier, simpler, less complicated, or better to work with? Okay, that question is quite subjective, to be fair. What's more, this isn't a fair comparison, as the COBOL code doesn't do near as much as the PHP application. So, let's just state that up front. I don't write COBOL. If I did, I could likely provide a far more sophisticated example, one that might come across as, in its own way, equally as challenging to navigate.

Actually, while writing this post, I came across a source code archive on CMS.gov labelled "Pricer Source Code and Software". At the end of the page is a section labelled "COBOL Source Code (Text Files)" which has a series of ZIP archives of COBOL source code. I'm going to go through some of them and see if they're sufficiently sophisticated. If so, I'll update the earlier COBOL code block with one or more of them.

Anyway, the point that I'm making is that, while COBOL and modern PHP development are quite different, so far at least, I get why someone from a different discipline of software development could see web application development as quite complicated.

Let's approach it another way. At it's most simple, to build production-grade web applications in PHP, you need a webserver and the PHP runtime. To configure them to talk to one another properly, you need some background and experience in systems administration, along with knowledge of configuring PHP. So, you need to be a developer and a systems administrator. You don't need to be fantastic at either, but you need to have some experience with both.

Then, you need to write the application. This could be, as it once was, all contained in one, large, overwhelming file. I'm dating myself here, but I remember the days when PHP and HTML were all contained in one file. It was a mess, but that's how it was. You could say it was something akin to COBOL development at that stage.

Then came the days when we started to transition to the modern approach, where we had one main, bootstrap file, which contained a significant number of include statements to multiple other files. Depending on how your application was written, both of these approaches could have been complex, based on your file, variable, and function naming convention, if you had one.

The only real difference is in how the complexity was organised. Was it all in one file, or was it spread across multiple files. Now, did you write tests for your code? Perhaps you did, but I don't remember how PHP code was tested before PHPUnit came along 25 years ago. But, after it did, and I'm oh so very grateful that it did, you then had more files to maintain. To be fair, if properly understood, test suites help you understand your code, but they are still something that you have to be aware of and maintain. In addition, you have to maintain the configuration file as well (phpunit.xml.dist).

After this separation, you're next likely going to split out the view code from the rest of the backend code, the code that connects to your data sources (such as databases, caching and logging servers, etc). For that, you'll probably use a view layer package such as Twig (I'm completely biased in that respect. I love it). So, now you have a series of template files to stay on top of. Then, you split out all of the functions and classes into separate files, nested under the src directory.

By the time you're done, instead of one file, you likely have around 50+ files. While a lot to keep abreast of, it's far easier to write, maintain, and test each one. However, you now need something to bring them altogether, so that they can be found and instantiated when required. How do you do that? Likely, with a DI (Dependency Injection) container. PHP has a wealth of them, gladly, including laminas-servicemanager and PHP-DI.

This is not an exhaustive consideration of how a modern PHP application is built. But, to someone from a different discipline, I can appreciate how it would come across as complicated. I've not even started talking about packaging and deployment. That's a whole other consideration, requiring (depending on your approach) even more systems administration knowledge.

I can't give a complete comparison with another language, such as Swift, Kotlin, or Rust, whether that be for building an iOS, Android, or systems application. I just don't have the experience necessary to do that. But, from what I have, they're hardly uncomplicated.

So, to bring this to a rapid conclusion, while COBOL is hardly what I would call simple, as it has its own complexity, each other modern language. Now view that through the lens of building a shippable application, regardless of what it is, and each discipline has its own level of complexity, one that someone coming from a different discipline would find, at least at first, challenging.