Purposeful Under-Engineering

Heads up!: This article is still a draft. It will only show up on local environments.

The year is 2024.

You’re a software developer. You joined this industry a little more than 5 years ago and you’re doing quite well.

You’re building a web-based application with a small team. You crank out features, one after another. Some of them are kinda useless, but you’re learning a lot and you’re having a pretty good time overall.

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 talking 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 friendly 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 say. For some reason they’re always grumpy and fighting fires, but luckily that’s none of your business.

What’s your point, grumpy old fart?

How much of the above did I get right? I bet you found yourself at least somewhere in those ranty lines.

Look, I’m not here to tell you that any of the technologies I listed above are bad. They absolutely aren’t. They’re popular in 2024 for a good reason. They allow engineering teams to be effective and keep on building, shipping and running software reliably. It’s none of my business to judge whether or not they make sense in your given circumstances.

The libraries, frameworks, languages, tools, patterns, platforms, and services I mentioned above give you a lot of practical abstractions so that you don’t need to worry about what’s beneath. You can focus on building the next AI-crypto-thing (I’m getting cynical again, huh?) instead of getting lost in the weeds of the complexity of frontends, backends, servers, operations, and keeping things up and running.

I’ve been in this industry for a bit. It’s more than 20 years since I built and published my first website. When I started, none of the tools and services I listed above existed.

This is not a nostalgia-heavy plea plea to return to building software like it’s 1999. I’m not saying you should go back to building software like people did in the dark ages. That’d be silly.

…or would it?

Allow yourself to Under-Engineer for once

I’d like to challenge you to deliberately under-engineer a small web application and get it all the way out there to production, with a public URL that actual 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 from a list of your favorite options. Just hard-code the options to keep it simple.

If you don’t like the ideas above, pick one of your own. Don’t overthink it, just have some fun.

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 it so that people can access it on the open web (you share a URL, they open it and see the magic)

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 again. If you don’t feel bad about how scrappy you’re building things, you’re probably not trying hard enough (or you’re more comfortable with scrappiness than most).

Write static HTML by hand, from scratch. No templating language. No static site generators. No frameworks.

If you want to add JavaScript, use vanilla JavaScript. No build tools. No bundlers. No TypeScript. Inlining JavaScript in <script> blocks in your HTML is totally cool. No third-party dependencies like HTMX or jQuery - the DOM API is plenty.

Write CSS like your great-grandparents did. Or would’ve done if it were around back then. No Tailwind. No Bootstrap. 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.

Get your own server. Maybe you’ve got a spare laptop or raspberry sitting around that you could expose to the internet (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 to containerize your application. Don’t mess with terraform or kubernetes or docker. Don’t rely on PaaS providers to do the heavy lifting for you. Provision your server manually via ssh, install packages via apt right there, 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.

You hopefully get the idea at this point.

Scrap all the conventions and see what you learn

If you’ve been around for long enough, you’ll know what most of what I’m suggesting above feels like. You’ve been there, you’ve done that. You experienced the pain of not having comfortable tools first hand. You know that there are good reasons why we don’t build software like that in this day and age.

But chances are you’ve never been through this experience. All you know is the cushy and yet mind-blowingly complex world of modern software development. The layers upon layers of abstractions, services, “best practices” that you they tell throw at you on YouTube and Reddit.

Or maybe it’s been ages since you’ve last gone really really simple. You started using heavy JS frameworks and elaborate container orchestration even for the simplest setups because you’ve grown so familiar with it. That’s smart, genuinely, but you might have forgotten what it was like to start with the bare essentials.

Look, I’m not some purist, 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. But if you truly go through this exercise, I promise that you will learn and experience a few insightful things:

  • you get a chance to understand (and maybe appreciate) the primitives, the basic building blocks that hide underneath all the tools you usually use
  • you start to appreciate the convenience that modern tooling truly offers
  • equally, you’ll discover that sometimes basic building blocks are actually be good enough
  • you might learn that there are hardly any best practices in software development, only trade-offs that depend on circumstances
  • you get a chance to challenge your own beliefs
  • you might be surprised how far a radically simple way of building things gets you - and how can even apply this idea way beyond silly app ideas you got from a rando’s blog
  • and of course you get the bragging rights of having shipped something in a completely unhinged way

Use modern tools but challenge your beliefs

Purposeful under-engineering is a fun exercise. It’ll help you challenge long-held beliefs and learn a few things while you’re having fun. However, it’s still just that: an exercise. Please don’t go to work on Monday and tear down your production Kubernetes cluster because someone on the internet said it’s fun to deploy to your own VPC via ssh. And if you do, don’t blame me.

I’m not here to say that modern software development is bad. I’m not here to talk badly about frameworks, libraries, patterns and technologies that are widely used in the industry today - but I do challenge you to challenge your own beliefs.

While I think it’s healthy to reduce the complexity of your application stack that doesn’t mean you shouldn’t use modern tooling. Software is a fundamental pillar of modern life. Our users expect our systems to be responsive, reliable, accessible, and pleasant to use. Software development is more than hacking away 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.

I believe that going radically simple, under-engineering on purpose every now and then, can help you become a better software engineer, and I encourage you to give it a try.