· 6 min read

Boring Technology

# Random Talk
This article was auto-translated from Chinese. Some nuances may be lost in translation.

In this era of ever-changing software development, I think we can revisit this classic article together—Choose Boring Technology, choosing boring technology.

In development, we often complain that our language choice is outdated; legacy PHP spaghetti code should be completely rewritten in Rust or Golang or any programming language you can think of. Using Postgres or MySQL for the database is too old-fashioned; we need Mongo, DynamoDB, Cassendra, Neo4j to handle high traffic.

Usually, the idea that “introducing some new technology” can solve existing problems mostly means the development hasn’t been thought through carefully enough. But it usually takes time to truly realize this—from a young person whose eyes light up at new technologies to a middle-aged man with a greasy face.

When you say this to other developers who don’t understand, they’ll just think it’s the mentality of old people who don’t want to learn anything new. But precisely because you’ve witnessed all the rises and falls, and have entered the third realm of “seeing mountains as mountains, seeing water as water,” you know that boring technology is often the best technology.

We are naturally averse to boredom, because boredom means stagnation and a lack of novelty. But in the software world, “boring” means stability, maturity, and battle-tested in production. The excitement of technology is often paid for with operational pain.

Under Vercel’s constant promotion, many SaaS services use Next.js for full-stack development. Although combining it with LLMs and Vercel’s own platform advantages can help you launch products quickly and deploy them painlessly, it also made me realize the complexity it brings.

For example, writing React components requires understanding the complexity of the front-end/back-end boundary; choosing between Server Actions / Server Component / Client Component—these are all trade-offs made to achieve what they claim is a better UX.

It’s not that Vercel is bad. Its integration with Next.js is quite good, and it supports SSG / SSR as well as pure static generation. The SSR version automatically turns the functions Next.js needs on a server into Edge Functions, with automatic deployment, rollback, usable logs, and alerting systems. These are all very helpful features.

But it’s not without drawbacks either. As requirements become more complex, even choosing libraries becomes quite annoying:

  • Permission management
  • Form validation
  • JWT and other mechanism implementations
  • Database migration management
  • Websocket or SSE
  • Background Job / Scheduled Job / Cron Job, or any asynchronous processing mechanism

Or you may realize that Edge Functions behave differently from local development, causing some code to behave completely differently.

This can also be understood as freedom of choice, but for startups or developers just getting started, spending time stepping into these pitfalls may be an interesting experience, yet it is not a high-CP-value choice for companies that need to ship products. Many frameworks solved these problems very early on; for example, Django / Laravel / Ruby On Rails all have the mechanisms mentioned above.

For the work I’m doing now, because it involves a lot of image processing, I needed to optimize for CPU-bound tasks and eventually chose AWS SQS to solve that part. The rest is implemented using Django’s built-in features. Although Django’s support for async is not that strong, it really did save me a lot of time. Deployment is also something that requires high attention, because it’s very easy to overcomplicate it.


Next, let’s talk about how I would choose if it were up to me. I think a boring framework should have the following requirements:

  • Built-in and complete Database Adapter and Migration mechanism: this is easy to overlook, but having migration is crucial
    • Sometimes this is a double-edged sword. For example, some databases do not have a uuid implementation, so you need to generate the uuid in the application and write it to the database.
  • As above, a convenient and flexible ORM: models in Django and Ruby On Rails are very pleasant to write. At the same time, it should also have the ability to run Raw SQL Query when necessary. I know many developers dislike the abstraction and lack of flexibility brought by ORM, but it’s unrealistic to simply ignore the convenience and maintainability it provides.
  • Login and permission control mechanisms: at minimum, it should be able to control permissions at the API level, for example (using Django as an example)
# When a request comes in, permissions are controlled according to the configuration
class MyProfileAPIView(APIView):
    permission_classes = [IsAuthenticated, xxxGroupPermission]
  • Integrated Background Job mechanism: such as Celery commonly used with Django, or ActiveJob in Ruby On Rails
  • Whether it supports Cache: preferably with adapters, allowing free switching between in-memory and Redis
  • Whether it supports Websocket abstraction: for example, Django’s channel; Ruby On Rails’ Action Cable
    • Sometimes Websocket requirements just suddenly appear
  • Whether it is easy to run locally
  • Built-in admin generation mechanism: as long as the Model and UI options are defined, an admin page can be generated
class File(MyModel):
    device_id = models.CharField(
        max_length=255,
        verbose_name="Device ID",
        null=True,
        blank=True
    )
    
admin.site.register(File, FileAdmin)

Writing it this way, Django will automatically generate the corresponding page, allowing you to modify values in the database and also perform simple customization. Honestly, this is a function I later discovered to be extremely important.

If developers have to build the admin themselves, just the technology selection alone takes time. If they also want a front-end/back-end separation, that means they have to spend even more time integrating the UI, API, and database. Compared with Django, even though its pages are plain, being able to save 90% of the work directly through declaring Models while also guaranteeing correctness is an extremely convenient feature.

Every framework has, to some extent, its limitations or shortcomings. However, I think a “good boring framework” should have the following seven characteristics:

  1. ✅ Native support for async or lightweight concurrency (green thread / coroutine)
  2. 🧱 Stable database integration and Migration mechanism**
  3. 🧩 A useful but non-mandatory ORM, with freedom to use Raw SQL
  4. 🔐 Built-in login and permission control mechanisms
  5. ⚙️ Integrated Background Job or scheduling mechanisms (Celery / ActiveJob)
  6. 🪄 Automatically generated admin UI (for example, Django Admin)
  7. 💾 Comprehensive Cache and Websocket support

Gorgeous technologies can attract attention, but the ones that truly withstand storms are always the boring, reliable choices.

Afterword

At Rails World, DHH mentioned in his Keynote that the current state of web development is simply too complicated. In the past, you could just drag things over via SFTP and deploy immediately; now just running a CI/CD pipeline takes 15 minutes. Although I don’t fully agree with DHH’s view, I think most web developers have, to some extent, had the thought that things have become “overly complex.”