Let’s imagine:
It’s 2025. You’re a software developer. You joined this industry a little more than 5 years ago and you enjoy what you do.
You’re building a web-based application with a small team. You crank out features, one after another. Some of them feel kinda useless, but you’re learning a lot and having fun with your team, so you’re not complaining.
The team you’re working on is frontend-heavy. Your frontend is written in a JavaScript framework (it’s React, isn’t it?). A while ago someone on your team introduced you to the magic of React Server Components and how they solve a whole slew of problems with traditional SPAs. Your team decided to adopt server components (you’re on Next.js now, aren’t you?). You style your frontend using Tailwind, maybe you use shadcn/ui for some of their sweet UI components. All of this is pretty standard stuff, and it’s what the folks on Reddit and Hacker News keep raving about.
Hold on.
Maybe I got that wrong.
You’re actually not one of those frontend weirdos. You’re a backend engineer. You’re not one to push pixels. You push data into databases and pull it back out using a plethora of CRUD endpoints. You sing the gospel of Go. You build a nice GraphQL API so those frontend teams don’t bother you with new requirements to tailor your API to their needs all the damn time. On the rare chance that you’ve actually got to serve a frontend from your backend application you decide to serve HTML with HTMX sprinkled on top for extra interactivity. There’s no way you’d touch any of that lousy JavaScript yourself.
You deploy and run your application on Vercel, or you host it with the quirky folks at Fly.io. As your organization grows, you start packing everything into Containers and start deploying to a managed Kubernetes cluster hosted on AWS. You’ve got a bunch of tireless tired SREs at your company who manage all that “k8s” madness for you. On top of that they provide you with nice dashboards showing metrics, logs, and all sorts of events — Observability as they call it. You wonder why they’re always so tired and on the edge, but you’ll leave solving that mystery for another day.
What’s your point, grumpy old fart?
It’s unlikely that I know you or the company you work for. Still there’s a good chance that the strawman I built above isn’t too far off from your day-to-day work. I probably struck some chords and revealed patterns, tools, and frameworks I’d find if I joined your company.
Despite my grumpy undertones, I’m not implying that any of the technologies and patterns I listed above are bad. They aren’t. There’s hardly anything that’s clearly good or bad in software engineering1. These tools and technologies are popular for a reason. They allow engineering teams to be effective and keep on building, shipping and running software reliably. They give you practical abstractions so that you don’t need to worry about what’s beneath. You can focus on building the next big thing instead of getting lost in the weeds of frontends, backends, servers, operations, and keeping things up and running.
When I built and published my first web application a little more than 20 years ago, none of the tools and services I listed above existed. Lately I’ve been thinking about how different it was to build, ship and maintain software at different points in my career as a software developer and how grateful I am for having experienced different stages of our industry. With a plethora of modern software engineering power tools at our finger tips it’s easy to lose sight of the foundations. Writing software like it’s 1999 is still doable (and more fun than ever), but largely underappreciated as software developers quickly fill their developer toolbox with very powerful but highly complex tools as that’s what everyone tells you to do.
This might all sound a lot like a “back in my days” anecdote. Some of that “real programmers use blood, sweat, tears and punch cards” nonsense people have been saying at pretty much any time in software engineering history. A nostalgia-rich plea that you return to building production software without any of the tools we’ve got today. Don’t worry, that’s not what I’m trying to say here. You know how to use modern tooling so it’s smart to use it.
However, I do think that there’s a lot you can learn from purposefully building software the way people did decades ago and I want to encourage you to give it a try.
Take the under-engineering challenge
I’d like to challenge you to deliberately under-engineer a small web application. Get it all the way out there to production, with a public URL people can visit. It doesn’t need to be anything sophisticated. Pick an application that you can build relatively quickly, this whole adventure should take a few hours, tops.
If you need inspiration for an application you could build, how about…
If you don’t like the ideas above, pick one of your own. Don’t overthink it, just have some fun and get started.
The rules of the under-engineering challenge are simple:
- you build a web-app
- you keep the amount of frameworks, libraries, tools and services to an absolute minimum
- you deploy it to a web server that’s not your local machine
- you publish a URL that people can access on the open web
Under-engineer like you mean it
When I say “under-engineering” I mean that you should purposefully avoid anything you can cut out from your tech stack. Aim for radically simple. If you think you can’t go simpler, try harder. If you don’t feel bad for the scrappy nature of your code and infrastructure, you’re might not be trying hard enough or you’re more comfortable with scrappiness than most.
Write static HTML by hand. No templating engines. No static site generators. No frameworks.
If you want to add JavaScript, use vanilla JavaScript. No build tools. No bundlers. No tree-shaking and minification. No TypeScript. Inlining JavaScript in <script>
blocks in your HTML is totally cool. No third-party dependencies like HTMX or Alpine.js or jQuery - the DOM API is plenty.
Write CSS like your great-grandparents did. No Tailwind. No Bootstrap. No PostCSS. Just honest to god <style>
tags (or damn inline style
attributes for all I care!).
If you write a server application, try to use good old server-side rendered HTML instead of relying on single page applications or server-side components that you hydrate on the client. Try to use the smallest amount of frameworks and libraries possible. No third-party dependencies.
Grab your own server. Maybe you’ve got a spare laptop or Raspberry Pi sitting around that you could expose to the internet (for example via dynamic DNS, look at Cloudflare or ngrok). Or you rent a cheap VPS at Hetzner, DigitalOcean or grab a free AWS EC2 instance.
Don’t bother containerizing your application. Don’t mess with terraform
or kubernetes
or docker
. Don’t rely on PaaS providers to do the heavy lifting for you. ssh
into your server, provision it manually by installing packages via apt
. Write config files by hand. Don’t worry about Infrastructure as code. Don’t worry about Continuous Deployment. Copy your application over via scp
, rsync
or sftp
and start it on your server. Hook it up to a DNS record manually and you’re good to go.
To understand how your application is doing in production, you could ssh
into your production box and tail -f
some log files. If your app keeps crashing, bring it back up - maybe you start writing a cron
job or a systemd
service to keep it alive.
Out with the new, in with the old. You get the idea.
I went through this exercise myself as I wrote this blog post. I built a simple “air horn” web app.
It’s silly, it’s simple, and it was super fun to build. All the code lives in a simple
index.html
file Irsync
to a cheap VPS that I installedCaddy
on. You can check out the code in this GitHub repo.
Forget conventional wisdom and see what you learn
Most of us haven’t been programming long enough to remember a time when the comfort of modern tooling didn’t exist. All we know is the cushy and yet mind-blowingly complex world of modern software development. The layers upon layers of abstractions, tools, and “best practices” they hurl at us on all the usual social media channels.
When we’ve been around for a while, it becomes easy to get stuck in our own ways. As we grow as professionals we get comfortable with more and more tools. We create our personal developer toolbox and fill it with tools we know and love. Most often the tools that find their way into our toolbox are not the simplest tools to get the job done but the ones we picked up because they were popular. Once we grok JavaScript frameworks and elaborate container orchestration it’s tempting to use these complex tools even for simple problems out of habit and comfort.
Look, I’m not some minimalist zealot telling you that you’ll only achieve true programming zen if you start saying “no” to all the comforts the modern programming world is giving you. Under-engineering is merely an exercise to challenge your beliefs in a healthy way. Disregard everything you believe to be best practice and common sense, get uncomfortable, and pay attention to what you learn. When you go through this exercise, I promise that you will learn and experience a few insightful things.
- You get a chance to understand the primitives, the basic building blocks that hide beneath all the tools you use daily.
- You start to appreciate the convenience that modern tooling truly offers.
- Equally, you’ll discover that basic building blocks are often good enough and get you further than you expected.
- You might learn that there are hardly any best practices in software development, only trade-offs that depend on circumstances.
- You get an opportunity to replace complicated parts in your developer toolbox with simpler alternatives.
- And of course you get the bragging rights of having shipped something in a completely unconventional way.
Use modern tools, but understand their cost
Under-engineering is a fun exercise. But it’s still just that: an exercise. Please don’t go to work next week and tear down your production Kubernetes cluster because some random guy on the internet said it’s fun to deploy software to your own VPC via ssh
. And if you do, please don’t blame me.
Modern software development ecosystems allow us to build things we couldn’t build decades ago in a fraction of the time. At the same time we often fall for snake-oil, over-engineering, and chase the newest shiny tools for kicks. Being able to understand when exactly modern tooling is pulling its own weight is an important skill for a well-rounded software developer.
While I think it’s healthy to reduce the complexity of your application stack I don’t suggest you reject modern tooling altogether. Software is a fundamental part of modern life. Our users expect our systems to be responsive, reliable, accessible, and pleasant to use. Software development is more than hacking on a system by yourself - it’s a socio-technical problem that requires humans to collaborate closely and find creative solutions to meet ever-changing demands. Pick tools that make you and your team effective and don’t reject modern tools to tickle a weird purist sense superiority. But please be mindful of the additional complexity you introduce.
I encourage you to go radically simple and under-engineer on purpose every now and then. It can help you become a better software engineer, help you re-evaluate the tools you’re relying on, and if nothing else it’s a fun experience.
If you go through this exercise and end up publishing something, I’d love to see it. Hit me up on one of the social channels you see linked at the bottom of this page and show me what you came up with!
Footnotes
-
The biggest gripe I have with modern software engineering organizations is the seemingly default split between frontend, backend, and operations teams, but that’s something for another day. ↩