Boring Technology
In an era where software development constantly churns out new trends, I think it’s worth revisiting that classic essay—Choose Boring Technology.
In development, we often complain that our language choices are outdated: that PHP legacy code should be completely rewritten in Rust, Golang, or whatever language comes to mind; that using Postgres or MySQL for databases is hopelessly behind the times, and we need Mongo, DynamoDB, Cassandra, or Neo4j to handle high traffic.
Usually, the thought that “introducing a new technology” will solve existing problems mostly means that the developers haven’t thought things through clearly enough. Realizing this, however, takes time—from a wide-eyed youth whose eyes sparkled at every new shiny tech to a seasoned, world-weary middle-aged uncle.
If you tell this to developers who don’t understand, they’ll just dismiss it as the mindset of old folks who don’t want to learn new things. But it’s precisely because you’ve seen the ups and downs—and reached that third level of insight where mountains are once again mountains, and waters are once again waters—that you realize boring technology is often the best technology.
By nature, we dislike being bored. Boredom implies stagnation and a lack of novelty. But in the software world, “boring” stands for stability, maturity, and battle-tested reliability. The excitement of cutting-edge tech is often paid for with the high cost of maintenance.
Driven by relentless promotion from Vercel, many SaaS products now use Next.js for full-stack development. Combined with LLMs and the advantages of Vercel’s own platform, you can ship products quickly and deploy painlessly. Yet, it also made me realize the sheer complexity it introduces.
Things like writing React components require understanding the nuances between frontend and backend; choosing between Server Actions, Server Components, and Client Components—all these trade-offs are made in the name of achieving what they claim to be a better UX.
That’s not to say Vercel is bad. Its integration with Next.js is remarkably smooth: it supports SSG and SSR, as well as pure static generation. For SSR, it automatically converts server-dependent Next.js functions into Edge Functions, handles automated deployments and rollbacks, and provides decent logging and alerting systems. These are genuinely helpful features.
However, it’s not without its drawbacks. As requirements gradually become more complex, just selecting libraries becomes an utter headache:
- Permission and role management
- Form validation
- Implementation of mechanisms like JWT
- Database migration management
- WebSockets or SSE
- Background jobs, scheduled jobs, cron jobs, or any asynchronous processing mechanism
Or realizing the behavioral discrepancies between Edge Functions and local development, leading to code that behaves completely differently in production.
You could view this as the freedom of choice. But for early-stage companies or developers, spending time stumbling through these pitfalls—while perhaps an interesting learning experience—is simply not a cost-effective choice when the goal is to ship a product. Many older frameworks solved these issues long ago; Django, Laravel, and Ruby on Rails already have built-in solutions for everything mentioned above.
Take my current work as an example. Because it involves a considerable amount of image processing, I needed to optimize for CPU-bound tasks and eventually chose AWS SQS. But everything else is implemented using Django’s built-in features. While Django’s async support isn’t the best, it has undoubtedly saved a ton of time. Deployment is another area that demands careful attention, because it’s so easy to overcomplicate.
Now, let’s talk about what I would look for if the choice were up to me. I believe a “boring” framework should possess the following essentials:
- Built-in, comprehensive database adapters and migration mechanisms: This is easily overlooked, but having migrations is absolutely critical.
- Though sometimes a double-edged sword (e.g., when a DB lacks native UUID support, requiring you to generate UUIDs on the application side before writing to the database).
- Building on the above, a convenient and flexible ORM: Writing models in Django or Ruby on Rails feels exceptionally smooth. At the same time, it must support raw SQL queries when necessary. I know many developers dislike ORMs for their abstractions and rigid interfaces, but bluntly dismissing the convenience and maintainability they provide is simply unrealistic.
- Authentication and permission control mechanisms: At a minimum, it should allow permission handling at the API layer, for example (using Django):
# Permissions are enforced when the request comes in, based on settings
class MyProfileAPIView(APIView):
permission_classes = [IsAuthenticated, xxxGroupPermission]
- Integrated background job mechanisms: Like Celery, which is often paired with Django, or ActiveJob in Ruby on Rails.
- Built-in caching support: Ideally with adapters that let you seamlessly switch between in-memory caches and Redis.
- Abstractions for WebSockets: Such as Django Channels or Ruby on Rails’ Action Cable.
- Because sometimes, WebSocket requirements just pop up out of nowhere.
- Ease of running locally.
- A built-in admin dashboard generator: Simply define the models and UI options to generate administrative pages.
class File(MyModel):
device_id = models.CharField(
max_length=255,
verbose_name="設備ID",
null=True,
blank=True
)
admin.site.register(File, FileAdmin)
With just this code, Django automatically generates the corresponding interface, allowing you to modify database values directly and make basic customizations. Honestly, this is a feature I came to appreciate as immensely important over time.
If you ask developers to build an admin panel from scratch, tech stack selection alone takes a good chunk of time. If you want a decoupled frontend and backend, that means spending even more time wiring up the UI, APIs, and the database. By contrast, even though Django’s generated pages look basic, being able to save 90% of the work just by declaring models—while guaranteeing correctness—is an extraordinarily convenient feature.
Every framework has its limitations and flaws to some degree. That said, I believe a “good boring framework” should exhibit these seven characteristics:
- ✅ Native support for asynchronous operations or lightweight concurrency (green threads / coroutines)
- 🧱 Solid database integration and migration mechanisms
- 🧩 A pleasant, non-mandatory ORM that allows raw SQL whenever needed
- 🔐 Built-in authentication and permission control
- ⚙️ Integrated background jobs or scheduling mechanisms (e.g., Celery, ActiveJob)
- 🪄 Auto-generated admin UI (e.g., Django Admin)
- 💾 Comprehensive caching and WebSocket support
Flashy technologies turn heads, but the ones that truly weather the storm will always be the boring, dependable choices.
Postscript
In his Rails World Keynote, DHH mentioned that modern web development has become absurdly overcomplicated. In the past, you simply dragged files over SFTP and you were live; today, running a CI/CD pipeline takes 15 minutes. While I don’t agree with DHH on everything, I think almost every web developer has, at some point, felt that things have become “unnecessarily complex.”
Related Posts
- How to Prepare for Software Engineering Interviews Preparing for tech interviews has always been challenging. One of the most frustrating parts is the heavily one-sided power dynamic. As a candidate, it's hard to gauge their actual standards, and it's easy to spiral into self-doubt afterward. Let's talk about it.
- Words That Changed My Outlook on Life Feynman, Chaplin, and the movie Hyakumeter: from an insecure boy to someone capable of helping others, sharing the reflections and life philosophies that shaped me most.
- N Benefits of an Independent Website Why spend time running your own blog in an era dominated by short-form video and social media? Here are my thoughts after nearly a decade of blogging.
- Reflections on Fitness and Weight Training Sharing my thoughts and journey with weight training over the past period.