The Joy of Under-Engineering

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


An app that chooses what you could have for dinner. Provide a list of your favorite options and let the app pick an option for you. Hard-code the options in the source code if you want to keep it really simple, or store options in a data store if you want to take it up a notch.

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:

  1. you build a web-app
  2. you keep the amount of frameworks, libraries, tools and services to an absolute minimum
  3. you deploy it to a web server that’s not your local machine
  4. 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.

Two developer toolboxes side by side. One with a plethora of modern tools like TailwindCSS, Svelte, NextJS, Docker, Kubernetes, the other one with HTML, JavaScript, CSS and Bash only

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 I rsync to a cheap VPS that I installed Caddy 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

  1. 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. ↩