
I love a good side-project. Like most geeks, I have a tendency to go down rabbit holes when faced with problems - give me a minor inconvenience and I’ll happily spend weeks building something far more…
Discussion on Hacker News Discussion on lobste.rs

So when my covers band started having trouble keeping track of our setlists and song notes (“How many times do we repeat the ending?”, “Why did we reject this song again?”…) I decided to build an app. We’d tried various approaches from spreadsheets to chat groups, and nothing seemed to work or provide a frictionless way of capturing notes and planning gigs in a consistent way.

I know, right? Rails. That old thing ? People still use that ? But as I was doing this purely for fun, I decided to forgo the usual stacks-du-jour at $DAYJOB, and go back to my “first love” of Ruby. I also figured it would be a great opportunity to get re-acquainted with the framework that shook things up so much in the early 2000s. I’d been keeping half an eye on it over the years but it’s been a long time since I’ve done anything serious with Rails. The last time I properly sat down with it was probably around the Rails 3-4 era about 13-14 years ago now. Life moved on, I got deep into infrastructure and DevOps work, and Rails faded into the background of my tech stack.
The 2025 Stack Overflow Developer Survey paints a similar picture across the wider developer world as a whole, too. Rails seems to have pretty much fallen out of favour, coming in at #20 underneath the bulk of top-10 JavaScript and ASP.NET frameworks:

And Ruby itself is nowhere near the top 10 languages, sitting just underneath Lua and freaking Assembly language in terms of popularity! I mean, I love me some good ol’ z80 or 68k asm, but come on… For comparison, Javascript is at 66% and Python is at 57.9%.

But I’m a stubborn bastard, and if I find a technology I like, I’ll stick with it particularly for projects where I don’t have to care about what anyone else is using or what the latest trend is. So Ruby never really left me. I’ve always loved it, and given the choice, it’s the first tool I reach for to build something.

yield, and how even complex logic reads almost like English. There’s just this minimal translation required between what I’m thinking and what I type. Sure, I can knock things together in Python, Go, or whatever the flavour of the month is, but I always feel on some level like I’m fighting the language rather than working with it. And of course there was the welcoming, quirky “outsider” community feel with characters like Why the Lucky Stiff and their legendary Poignant Guide To Ruby.
I should point out that my interest (and focus of this blog post) has always been firmly in the “engine room” side of development - the sysadmin, DevOps, back-end infrastructure world. Probably for much the same reason I’ve gravitated towards the bass guitar as my musical instrument of choice. Now, I’m conversant in front-end technologies, having been a “webmaster” since the late 90s when we were all slicing up images in Fireworks, wrestling with table-based layouts and running random stuff from Matt’s Script Archive for our interactive needs.
But the modern world of front-end development - JavaScript frameworks, the build tooling, the CSS hacks - it’s never really captured my imagination in the same way. I can bluff my way in it to a certain extent, and I appreciate it on the level I do with, say, a lot of Jazz: It’s technically impressive and I’m in awe of what a skilled developer can do with it, but it’s just not for me. It’s a necessity, not something I’d do for fun.
While I haven’t built or managed a full Rails codebase in years, I’d never completely left the Rails ecosystem. There’s bits and pieces that are just so useful even if you’re just quickly chucking a quick API together with Sinatra. ActiveSupport for example has been a constant companion in various Ruby projects over the years - it’s just so nice being able to write things like
unless date <= 3.days.from_now
or
if upload_size > 2.megabytes
But sitting down with Rails 8 proper was something else. It’s recognisable, certainly - the MVC structure, the conventions, the generators are all where you’d expect them. Someone with my dusty Rails 3 experience can still find their way around and quickly throw up the basic scaffolding. But under the hood and around the edges, it’s become a very different beast.


Turbo handles things like intercepting link clicks and form submissions, then swapping out the <body> or targeted fragments of the page to give a Single Page App-like snappiness without actually building a SPA. I could then sprinkle in small Stimulus JS controllers to add specific behaviour where needed, like pop-up modals and more dynamic elements. It was pretty impressive how quickly I could build something that felt like a modern application while still using my familiar standard ERB templates and server-side rendered content.
While Stimulus seems to have a smaller developer community than the big JS toolkits/frameworks, there are plenty of great, carefully-written and designed component libraries you can easily drop into your project. For example check out the Stimulus Library and Stimulus Components projects which include some great components that you can tweak or use directly.
This was my first introduction to the vastly simplified JS library bundling tool that seems to have been introduced around the Rails 7 timeframe. Instead of needing a JS runtime, NPM tooling and separate JS bundling/compliation steps (Webpack - again, urgh….), JS components are now managed with the simple importmap command and tooling. So, to make use of one of those components like the modal Dialog pop-up for example, you just run:
$ bin/importmap pin @stimulus-components/dialog
This downloads the package from a JS CDN and adds it to your vendor directory and updates your config/importmap.rb. The package then gets automatically included in your application with the javascript_importmap_tags ERB tag included in the <head> of the default HTML templates. You can see how this gets expanded if you look at the source of any generated page in your browser:

You can then register the controller as per the docs (a few lines in javascript/controllers/index.js which can be skipped for locally-developed controllers as they’re handled by the autoloader) and get using it right away in your view templates. As the docs say: “This frees you from needing Webpack, Yarn, npm, or any other part of the JavaScript toolchain. All you need is the asset pipeline that’s already included in Rails.”
I can’t express how grateful I am for this change. I’m also annoyed with myself for missing out that this was added back in Rails 7. Had I noticed, I probably would have taken it out for a spin far sooner! I have to confess though that beyond the basics, I have somewhat lacking front-end skills (and was quickly developing The Flexbox Rage), so I took bits from various templates & online component generators, and got Claude to generate the rest with some mockups of common screens and components. I then sliced & diced, copied & pasted my way to a usable UI using a sort of customized “UI toolkit” - Rails partials are great for these sorts of re-usable elements.
I have mixed feelings about this. On the one hand, it helped me skip over the frustrating parts of frontend development that I don’t particularly enjoy, so I could focus on the fun backend stuff. It also did produce an objectively better experience far quicker than anything I’d have been able to come up with purely by myself. On the other… I view most AI-generated content such as music, art & poetry (not to mention the typical LinkedIn slop which triggers a visceral reaction in me) to be deeply objectionable. My writing and artistic content on this site is 100% AI-free for that very reason; To my Gen-Xer mind, these are the things that really define what it means to be human and I find it distasteful and unsettling in the extreme to have these expressions created by an algorithm. And yet - for me, coding is a creative endeavour and some of it can definitely be considered art. Am I a hypocrite to use UI components created with help from an AI ? What (if any) is the difference between that and copying from some Bootstrap template or modifying components from a UI library ? I’m going to have to wrestle with this some more, I think.
A slight detour here to explain my workflow and hopefully illustrate why I love Rails so much in the first place. It really shook things up in the early 2000s - before that, most of the web frameworks I’d used (I’m looking at you, Struts…) were massively complex and required endless amounts of XML boilerplate and other configuration to wire things up. Rails threw all that away and introduced the notion of “convention over configuration” and took full advantage of the expressive, succinct coding style enabled by Ruby.

$ bin/rails generate model Tag label:string color:string band:belongs_to
This resulted in a app/models/tag.rb like this:
class Tag < ApplicationRecord belongs_to :band
end
This automagically fetches the column names and definitions from the database, no other work required! Of course, we usually want to set some validation. There’s all kinds of hooks and additions you can sprinkle here, so if I wanted to validate that for example a valid Hex colour has been set, I could add:
validates :color, presence: true, format: { with: /\A#[0-9a-fA-F]{6}\z/, message: "must be valid hex" }
Then I set up URL routing. While you can later get very specific about which routes to create, a simple starting point is just this one line in config/routes.rb
Which generated the standard RESTful routes automatically:
$ rails routes -c TagsController
Prefix Verb URI Pattern Controller#Action
band_tags GET /bands/:band_id/tags(.:format) tags#index
POST /bands/:band_id/tags(.:format) tags#create
new_band_tag GET /bands/:band_id/tags/new(.:format) tags#new
edit_band_tag GET /bands/:band_id/tags/:id/edit(.:format) tags#edit
band_tag GET /bands/:band_id/tags/:id(.:format) tags#show
PATCH /bands/:band_id/tags/:id(.:format) tags#update
PUT /bands/:band_id/tags/:id(.:format) tags#update
DELETE /bands/:band_id/tags/:id(.:format) tags#destroy
Note all the .format stuff - this lets you respond to different “extensions” with different content type. So in this case, requesting /bands/1/tags/5 would return HTML by default, but I could also request /bands/1/tags/5.json and the controller can be informed that I’m expecting a JSON response.
I tend to use this to quickly flesh out the logic of an application without worrying about the presentation until later. For example, in the Tags controller I started with something like this to fetch a record from the DB and return it as JSON:
class TagsController < ApplicationController # Auth and other stuff skipped for brevity... def show @tag = @band.tags.find(params[:id]) respond_to do |format| format.html # Use ERB template show.html.erb when I implement it format.json { render json: @tag } end end
end
And then I could test my application and logic using the RESTful routes using just plain old curl from my terminal:
$ curl --silent -XGET \ -H "Authorization: Bearer <token>" http://localhost:3000/bands/4/tags/5.json | jq .
{ "id": 5, "band_id": 4, "label": "Bass Change", "color": "#3288bd", "created_at": "2026-01-15T04:42:24.443Z", "updated_at": "2026-01-15T04:42:24.443Z"
}
Once that was all working, I moved on to generating the views as standard ERB templates. Combined with live-reloading and other developer niceities, I could go from idea to working proof-of-concept in a stupidly short amount of time. Plus, there seems to be a gem for just about anything you might want to build or integrate with. Want to import a CSV list of songs ? CSV.parse has you covered. How about generating PDFs for print copies of setlists ?
pdf = Prawn::Document.new do text "I <b>LOVE</b> Ruby", inline_format: true
end
print pdf.render
And so on. Have I mentioned I love Ruby?
I’ve always liked the way Rails lets you enable components and patterns as you scale. You can start small on just SQLite, move to a dedicated database server when traffic demands it, then layer in caching, background jobs and the rest as the need arises.
But the problem there is all the additional infrastructure you need to stand up to support these things. Want caching? Stand up Redis or a Memcache. Need a job queue or scheduled tasks? Redis again. And then there’s the Ruby libraries like Resque or Sidekiq to interact with all that… Working at GitLab, I certainly appreciated Sidekiq for what it does, but for the odd async task in a small app it’s overkill.
This is where the new Solid* libraries (Solid Cache, Solid Queue and Solid Cable) included in Rails 8 really shine. Solid Cache uses a database instead of an in-memory store, the thinking being that modern storage is plenty fast enough for caching purposes. This means you can cache a lot more than you would do with a memory-based store (pretty handy these days in the middle of a RAM shortage!), but you also don’t need another layer such as Redis.
Everything is already setup to make use of this, all you need to do is start using it using standard Rails caching patterns. For example, I make extensive use of fragment caching in ERB templates where entire rendered blocks of HTML are stored in the cache. This can be something simple like caching for a specific time period:
<% cache "time_based", expires_in: 5.minutes do %> <!-- content goes here -->
<% end %>
Or based on a model, so when the model gets updated the cache will be re-generated:
<% cache ["band_dashboard", @band.cache_key_with_version, expires_in: 1.hour] do %> <!-- dashboard content here -->
<% end %>
And sure enough, you can see the results in the SQLite DB using your usual tools. Here’s the table schema:
sqlite> .mode column
sqlite> PRAGMA table_info(solid_cache_entries);
cid name type notnull dflt_value pk
--- ---------- --------------- ------- ---------- --
0 id INTEGER 1 1
1 key blob(1024) 1 0
2 value blob(536870912) 1 0
3 created_at datetime(6) 1 0
4 key_hash integer(8) 1 0
5 byte_size integer(4) 1 0
And we can examine the cache contents:
sqlite> select id,substr(key,1,40),created_at,byte_size from solid_cache_entries;
id substr(key,1,40) created_at byte_size
-- ---------------------------------------- ----------------------- ---------
1 development:views/home/index:09337f42ae0 2026-03-06 09:29:06.237 2034
2 development:views/band_dashboard/bands/4 2026-03-06 09:34:17.591 1990
3 development:views/band_dashboard/bands/4 2026-03-06 17:43:56.357 1992
4 development:views/band_dashboard/bands/4 2026-03-06 17:56:26.855 1992
5 development:views/band_show/bands/4-2026 2026-03-06 18:02:06.766 2244
Note though that the actual cached values are serialized Ruby objects stored as BLOBs, so you can’t easily view/decode them outside of the Rails console.
Solid Queue likewise removes the dependency on Redis to manage background jobs. Just like Solid Cache, it by default will use a database for this task. I also don’t need to start separate processes in my dev environment, all that is required is a simple SOLID_QUEUE_IN_PUMA=1 bundle exec rails server and it runs an in-process queue manager.
Declaring jobs is equally simple:
# app/jobs/my_sample_job.rb
class MySampleJob < ApplicationJob queue_as :default def perform Rails.logger.info "Yup, I still love Ruby..." end
end
And is scheduled in a typically plan-language fashion:
# config/recurring.yml
production: sample_job: class: MySampleJob schedule: every day at 3am
Beautiful! The upshot is that I could start making use of all these features from the get-go, with far less fiddling required, and running entirely off a SQLite database.


That said, Devise is a bit of a beast. The more I look into the auth generators, the more I like the simple understandable philosophy and as I read more about the comparisons, if I was starting all over again I’d probably lean more towards the native Rails option just because honestly it feels like it’d be more fun to hack on. But with things like Auth, there’s a lot to be said for sticking to the beaten path!

Rails used to use SQLite with its default settings, which were optimized for safety and backward compatibility rather than performance. It was great in a development environment, but typically things started to fall apart the moment you tried to use it for production-like load. Specifically, you used to have to tweak various PRAGMA statements:
journal_mode: The default rollback journal meant readers could block writers and vice-versa, so you effectively serialized all database access. This was a major bottleneck and most apps would see frequent SQLITE_BUSY errors start to stack up as a result. Instead, you can switch it to WAL mode which uses a write-ahead journal and allows readers and writers to access the DB concurrently.
synchronous: The default here (FULL) meant SQLite would force a full sync to disk after every transaction. But for most web apps, if you use NORMAL (sync at critical moments) along with the WAL journal, you get much faster write performance albeit with a slight risk of losing the last transaction if you have a crash or power failure. That’s usually acceptable though.
Various other related pragmas which had to be tuned like mmap_size, cache_size and journal_size_limit to make effective use of memory and prevent unlimited growth of the journal, busy_timeout to make sure lock contention didn’t trigger an immediate failure and so on…
All in all, it was a pretty big “laundry list” of things to monitor and tune which only reinforced the notion that SQLite was a toy database unsuitable for production. And it was made more complex because there wasn’t an easy way to set these parameters. So you’d typically have to create an initializer that ran raw SQL pragmas on each new connection:
ActiveSupport::on_load(:active_record_sqlite3adapter) do module SQLitePragmas def configure_connection super execute("PRAGMA journal_mode = WAL") execute("PRAGMA synchronous = NORMAL") execute("PRAGMA mmap_size = 134217728") # etc... end end class ActiveRecord::ConnectionAdapters::SQLite3Adapter prepend SQLitePragmas end end
This was obviously pretty fragile, so most developers I worked with simply never did it, and just followed the pattern of “SQLite on my laptop, big boy pants database for anything else”.
When I checked out Rails 8, I noticed straight away that not only is there now a new pragmas: block available in the database.yml, but the defaults are now also set to sensible values for a production application. The values provided to my fresh Rails app were equivalent to:
production: adapter: sqlite3 database: storage/production.sqlite3 pragmas: journal_mode: wal synchronous: normal mmap_size: 134217728 cache_size: 2000 busy_timeout: 5000 foreign_keys: true journal_size_limit: 67108864
All this makes SQLite a genuinely viable production database for small-to-medium Rails applications and combined with the Solid* components, means it’s not just a local dev or “getting started” convenience!
If you have an older Rails codebase and want to use a similar approach, a neat method of monkey-patching the SQLite adapter to provide a similar pragmas: section in the database configuration is detailed in this great article.

mod_rails) and Litespeed eventually helped by bringing a sort of PHP-like “just copy my code to a remote directory” method of deployment, but I still remember pushing stuff out with non-trivial Capistrano configs or hand-rolled Ansible playbooks to handle deployments, migrations and restarts. And then there were all the extra supporting components that would inevitably be required at each step along the way.
I had to include that old capture of the modrails.com site circa-2008 because a.) I really miss when websites had that kind of character, and b.) that is still a totally sick wildstyle logo 😄
This is why services like Heroku and Pivotal Cloud Foundry thrived back then - they offered a pain-free, albeit opinionated way to handle all this complexity. As the Pivotal haiku put it:
Here is my source code Run it in the cloud for me
I do not care how.
You just did a git push or cf push, vague magic happened, and your code got turned into containers, linked to services and deployed.
These days I prefer to do the building of containers myself. Creating an OCI image as an artifact gives me flexibility over where things run and opens up all kinds of options. Today it might be a simple docker-compose stack on a single VPS, tomorrow it could be scaled out across a Kubernetes cluster via a Helm chart or operator. The container part is straight-foward as Rails creates a Dockerfile in each new application which is pretty much prod-ready. I usually tweak it slightly by adopting a “meta” container approach where I move some of the stuff that changes infrequently like installing gems, running apt-get and so on into an image that the main Dockerfile uses as a base.
You’re of course free to use any method you like to deploy that container, but Rails 8 makes Kamal the new default and it is an absolute joy to use.
I’ve seen some dissenting opinions on this, but bear in mind I’m coming from a place where I’m already building containers for everything anyway. I generally think this is “the way to go” these days and have the rest of the infra like CI/CD pipelines, container registries, monitoring and so on. Plus, given my background, I crank out VMs and cloud hosts with Terraform/Ansible “all day errday”. If you don’t have this stuff already or aren’t happy (or don’t have the time) to manage your own servers remember that Kamal is not a PaaS. It just gets you close to a self-hosted environment that functions very much like a PaaS. Now that Heroku is in a “sustaining engineering model” state, there are several options in the PaaS space you may want to investigate if that’s more up your street. I hear good things about fly.io but hasten to add I haven’t used it myself.
Your Kamal deployment configuration lives in a deploy.yml file where you define your servers by role: web frontends, background job workers and so on:
servers: web: - web.rails.example.com job: hosts: - jobs.rails.example.com cmd: bin/jobs
Or you can point everything to a single host and scale out later. These files can also inherit a base which makes splitting out the differences between environments simple. There’s also handy aliases defined which makes interacting with the containers easy, all that is required is a SSH connection to the remote hosts.
aliases: console: app exec --interactive --reuse "bin/rails console" shell: app exec --interactive --reuse "bash" logs: app logs -f dbc: app exec --interactive --reuse "bin/rails dbconsole --include-password"
When you deploy, Kamal will:
The routing bit is handled by kamal-proxy, a lightweight reverse proxy that sits in front of your application on each web server. When a new version deploys, kamal-proxy handles the zero-downtime switchover: It spins up the new container, health-checks it, then seamlessly cuts traffic over before stopping the old one. I front everything through Nginx (which is also where I do TLS termination) for consistency with the rest of my environment, but kamal-proxy doesn’t require any of that. It can handle your traffic directly and does SSL termination via Let’s Encrypt out of the box.
Secrets are handled sensibly too. Rather than committing credentials to your repo or fiddling with encrypted files, Kamal reads secrets from a .kamal/secrets file that simply points at other sources of secrets. These get injected as environment variables at deploy time, so you can safely handle your registry password, Rails master key, database credentials and so on. You can also pull secrets from external sources like 1Password or AWS SSM if you want something more sophisticated, and the sample file contains examples to get you going.
That’s a lot, but bear in mind it’s all driven by a single command: kamal deploy.
Here’s an Asciinema capture of a real-life manual deploy session including a look at what’s happening on my staging server in my homelab:
I have this triggered by GitLab CI pipelines, with protected branches for each of my environments. So usually, deployment happens after a simple git push or merge request being approved. The upshot is that it feels like that old Heroku magic again, except you own the whole stack and can see exactly what’s happening. A single kamal deploy builds, pushes and rolls out your changes across however many servers you’ve configured. It’s the kind of tooling Rails has needed for years.

What I find appealing about the “magic” of Ruby might feel opaque and confusing to you. If you like expressive code and come from a Perl “There Is More Than One Way To Do It” background, I imagine you’ll love it. But I’ve come to realise that choice of tools (vi vs emacs vs vscode - FIGHT!) can be a very personal matter and often reflect far more of how our own minds work. Particularly so when it comes down to something like language and framework choice: These are the lowest layers that are responsible for turning your thoughts and ideas into executable code.
As a matter of taste, Ruby lines up more or less exactly with my sense of aesthetics about what a good system should be. But it is certainly an acquired taste, and that’s the biggest downside. Remember the survey results from the top of this article ? There’s no denying that Ruby and Rails’ appeal has become…. “more selective” over the years - to coin another phrase, this time from Spinal Tap.
It’s used in a lot of places that don’t make a lot of noise about it (some might surprise you), and there are still plenty of big established names like Shopify, Soundcloud and Basecamp running on Rails. Oh and GitHub, although I’m not sure we should shout about that anymore… But. While the Stack Overflow survey isn’t necessarily an accurate barometer of developer opinion, the positions of Ruby and Rails do show it’s fallen from grace in recent times. Anecdotally, I find a lot of documentation or guides that haven’t been updated for several years and the same goes for a lot of gems, plugins and other projects. Banners like this are becoming more and more common:

And I find that most gems follow a similar downward trend of activity. Take Devise for example. Plotting a graph of releases shows a pattern I see around a lot of Rails-adjacent projects. Big spikes or projects launched around the Rails “glory years” and then slowly trailing off into maintenance mode:

Apart from a spike in 2016 where it appears there was a bunch of activity around the v4 release, it’s been pretty quiet since then. The optimist might say that’s because by this point, most of these projects are simply “done”. These are really mature, reliable projects with around 2 decades of history running mission critical, high traffic websites. At what point are there simply no more features to add ?
But let’s look at the flipside. Rails on the other hand actually seems to be picking up steam and has been remarkably consistent since the big “boom” of Rails 3.0 in 2010:

Despite the changing trends of the day, it’s consistently shipped releases every single year since it hit the bigtime. If anything, Rails is a rare example of an OSS project that’s grown into its release cadence rather than burning out. Whether it can still find an audience amongst new developers is an open question but I’m glad there are obviously a few more stubborn bastards like myself refusing to let go of what is clearly, for us, a very good thing. I probably could eventually build things almost as fast in another language or framework, but I doubt I’d be smiling as much while I did so.
If you’ve made it this far, congratulations and “thanks for coming to my TED talk” / protracted rant! I’m guessing something has piqued your curiosity, and if so, I highly recommend taking Rails out for a spin. Work through the tutorial, build something cool, and above all enjoy yourself while you’re at it - because at the end of the day, that’s what it’s all about. Sure, there are more popular frameworks that’ll make a bigger splash on your resume. But as I said at the start, sometimes it’s worth doing things just for the sheer hell of it.
Have Fun!
❤️
I love Rails, but after working for a few places with huge Rails codebases and then several other places with .NET and other frameworks with actual typing, I just can't go back to Rails for anything that isn't a personal project.
Working with a large codebase with an untyped codebase is just a nightmare, even with powerful IDEs like RubyMine that are able to cover some of the paint points. I wonder how good Sorbet is these days, though, especially the RoR experience
Rust/Loco is unironically the most interesting framework right now.
Loco follows up the Rails formula pretty closely, and makes easier to learn Rust by taking care of a load of boilerplate code.
Concur on most interesting! I really hope it works out, but am cautious.
It is surprising to me seeing the rust web backend scene; many libraries, server frameworks, and users, but they are all Flask-analogs, without the benefit of the reasonably-robust ecosystem Flask has. My suspicion is that people are using them for micro-services and not websites/webapps, but I haven't been able to get a straight answer on this about how people are using these tools. I.e. even though rust is my favorite overall language and see no reason it couldn't be used for web work, I still use Django.
Axos, Axum, Rocket, Diesel etc, are all IMO not in the same league as Django. My understanding is that addressing this is Loco's Raison d'etre.
Another aspect of the Rust web ecosystem: It's almost fully gone Async.
It's quite a gap really.
I'll say this, coding agents make the lack of a "batteries included" framework like rails or Django somewhat less daunting.
But "convention over code" and having a default structure / shape for projects is extremely helpful and you feel it when it's missing.
For my last small project I looked at Loco but ended up passing on it because I felt like adoption wasn't great yet. I really hope it takes off, though.
What is it about large untyped codebases that make it a nightmare?
Anything can return anything and you only realize it at runtime is a massive headache. When you can't keep the entire code base in your head it becomes a liability.
I never used Ruby, but Python code bases love mixing in strings that are actually enums and overloading functions that accept all kinds of types. You just had to hope that the documentation was correct to avoid a crash.
Java 1.7 to Python feels very freeing from all the boilerplate. Kotlin, or any other modern language with a well designed standard library, to Python just feels like a bunch of extra mental work and test to write to just avoid brackets.
If you make a change to the return types of a function for example you have to manually find all of the different references to that function and fix the code to handle the change. Since there are no compile time errors it's hard to know that you got everything and haven't just caused a bug.
Is that a common issue? I guess I'm having a hard time imagining a scenario that would (a) come up often and (b) be a pain to fix.
Yes, and the downsides cascade. Because making any change is inherently risky you're kind of forced not to make changes, and instead pile on. So technical debt just grows, and the code becomes harder and harder to reason about. I have this same problem in PHP although it's mostly solved in PHP 8. But touching legacy code is incredibly involved.
Especially with duck-typing, you might also assume that a function that previously returned true-false will work if it now returns a String or nil. Semantically they’re similar, but String conveys more information (did something, here’s details vs did(n’t) do something).
But if someone is actually relying on literal true/false instead of truthiness, you now have a bug.
I say this as a Ruby evangelist and apologist, who deeply loves the language and who’s used it professionally and still uses it for virtually all of my personal projects.
The best perspective I've seen is that statically typed enforcement is basically a unit test done at compile time.
It makes coming up to speed on an existing codebase a slog because you have to trace through everything back to its source. Oh, and because there are magic methods and properties galore, your normal introspection tools in e.g. RubyMine get frequently stymied.
It's not just the untyped problems, the runtime definitions of functions, properties, etc make it nearly impossible to debug unless you have the state of your production data locally. (Or you ssh into your prod server and open up a REPL, load the state and introspect everything there). Good luck debugging locally in a nice IDE. It's a horrific nightmare. I use to love Ruby until I had to debug it live.
Same. Also became a .net developer after almost 20 years of Ruby/Rails.
Nowadays C# is anyways much more expressive than before. Meanwhile Ruby is still very slow.
Not to mention how poorly maintained are most Rails projects. People have been "vibe coding" forever.
A well-organized and maintained Rails app is great though. I'd definitely consider working with it again, but it really depends on what company it is.
I worked with Rails a lot. In my experience, every rails dev who is fanatical about how much they love Rails, also has little to no experience with strong types. Of the ones who later try types, they no longer love Rails. Personally I quit Rails entirely because of lack of types. No, RBS and Sorbet are not even close to good enough.
Also, every enterprise rails app I've seen (seven, to date) has been really poorly written/architected in a way that other backends just weren't. Even the fairly new ones felt like legacy code already.
Ruby is a strongly typed language. I think you are confusing strong typing with static typing.
[dead]
I’ve worked in two places now with Ruby Sorbet servers. Ruby always drives me nuts how things are just in-scope and we don’t know why or where they came from.
I certainly wouldn’t want to go back to working in dynamic languages without typing on top. That takes too much brain power, I’m too old for that now.
I would say Sorbet seems more “basic” than something like Typescript. It handles function calls matching signatures, potential nulls, making sure properties actually exist, that kind of thing. Whereas TS can get quite abstract, and at times you’re fighting to convince it that a value actually is the type you say it is.
TS is very powerful and expressive, to the point that it’s possible to do computation within type code. I’m not convinced I always need that power, or that it’s always more help than hindrance.
> Ruby always drives me nuts how things are just in-scope and we don’t know why or where they came from.
irb(main):005:0> Foo.new.method(:bar).source_location => ["tmp/test.rb", 5]
[dead]
You appear to be shadow banned. Letting you know since I didn't see anything egregious on a quick scan. Maybe contact HN and plead your case.
I vouched for your reply below, and to answer in the meantime:
Yes, it's runtime, but that only matters if your code can't be initialized without unacceptable side effects.
In which case you don't have a functioning test suite either, and have much larger problems.
Otherwise, just load the code you struggle to figure out into irb, or pry, or a simple test script, and print out source-location.
If that is impossible (aside from the fact that codebase is broken beyond all reason), the marginally harder solution is to use ruby-lsp[1] and look up the definitions.
This is only hard if you insist on refusing to use the available - and built in, in the case of source_location - tooling.
> I certainly wouldn’t want to go back to working in dynamic languages without typing on top. That takes too much brain power, I’m too old for that now.
> and at times you’re fighting to convince it that a value actually is the type you say it is.
Might just be allocating that brain power to the same task but calling it a different thing.
Are you hand coding?
Are we that far gone that "hand coding" is a term now? I hope there's an /s missing
I hope "hand coding" is an antonym for "convention coding" or something.
I’m guessing hand coding means, not vibe coding.
Did you use AI? .. Nah I hand coded it.
Real programmers use butterflies. https://xkcd.com/378/
Doesn't matter because LLMs also benefit greatly from typed code bases in that they can run the type checker and fix the problems themselves on a loop.
I haven’t seen much discussion about this point other than “llm handle languages x y and z because there’s a lot of training data”. Watching Terence Tau using llm for writing proofs in Lean was a real eye opener in this regard.
Both Claude and Codex handle Ruby just fine.
Thanks for such public confirming there is a lot of more us. I’m just tired hearing how great ideas will save our overblown pseudo-microservice architecture and I’m also running into some projects during evening that just solve problems without use STOA, unnecessary solutions and architectures.
I’m not into RoR, because I was mainly PHP rescuer in the beginning of my career, but they both are just problem solvers. Sit down, write minimal (in case of PHP not so cool looking) code and proceed to next task.
I've just started using RoR for a live greenfield project since New Year.
Honestly, breath of fresh air.
It's the closest I've come to that old school "in the box" desktop development experience you used to get from building desktop software with Visual Studio or IntelliJ IDEA or NetBeans or Eclipse or any of the other IDEs of the 90s/00s (I never used Delphi or VB but I imagine in some sense they were even moreso than the ones I've listed, which are the ones I used), only it's web development.
For me web development has always felt like a frustrating ordeal of keeping track of 10,000 moving parts that add noise and cognitive load and distract you from fixing the actual problems you're interested in solving. This means the baseline ancillary workload is always frustratingly high. I.e., there's too much yak-shaving.
Whereas Rails seems to drag that all the way down to a level where it feels more similar to the minimal yak-shaving needed to (at least superficially) build, run, and distribute desktop software. Not that this is without its challenges, because every deployment environment is a little different in the desktop world, but the day to day developer experience is much lower friction that modern web development in general.
Also, no sodding TypeScript to deal with. I hate TypeScript: an ugly, verbose, boilerplatey abomination that takes one of the nicest and most fun features of JavaScript (duck typing) and simply bins it off. Awful.
I have my complaints about the JS/TS ecosystem, but I'm surprised to see a comment that it's verbose and "boilerplatey."
Could you elaborate on that?
If you're solving problems rails is best at, it's borderline magical.
The troubles arise when you get to huge codebases or complicated frontend patterns that aren't ideal for SSR / hotwire.
Also, it's impossible to separate Rails from DHH, whose xenophobic politics are unfortunately front and center.
TS doesn't "bin off" duck typing, it's a fundamentally structural type system. It's statically analyzed ducks, all the way down - when nominal behavior is preferred, people have to bend over backwards. Either you are using the wrong vocabulary or I don't think you've bothered to actually learn Typescript. In any case, it's the programming language that successfully brought high-level type system concepts like type algebra, conditional types, etc. to their widest audiences, and it deserves a ton of credit for that. The idea that JS and Ruby and Python and PHP developers would be having fairly deep conversations about how best to model data in a type system was laughable not that long ago.
> Either you are using the wrong vocabulary or I don't think you've bothered to actually learn Typescript.
All right, fine: TypeScript uses structural typing which is if you like a specialisation of duck typing but, whatever, compared with JS's unadorned duck typing it still leads to embellishment of the resulting code in ways that I don't enjoy.
I've been using TypeScript across different projects at different companies since 2013 and I've absolutely given it an honest go... but I just don't like it. I even allowed its use at a mid-size company where I was CTO because it fit well with React and a sensible person picks their battles, but I still didn't like it.
I'm now in the very privileged position where I don't have to use it, and I don't even have to allow it a foot in the door.
Now I'm sure that won't last forever, and I'll have to work with TypeScript again. I'll do it - because I'm a professional - but I'm still entitled to an opinion, and that opinion remains that I don't like the language. After 13 years of use I feel pretty confident my opinion has settled and is unlikely to change. I find it deeply unenjoyable to work with. BUT the plus side is that in the era of LLMs perhaps I no longer need to worry so much about have to deal with it directly when it eventually does impinge upon my professional life again.
I found it not just to lead to embellishment, but (1) the problems it did flag mostly would be caught by minimal testing; whereas (2) it regularly missed deeper problems. For an example of the latter: using TanStack (React Query) api caching, you have different data shapes for infinite scroll vs non infinite scroll. There were circumstances were an app confused them. Typescript had nothing to say. Nominal typing easily handles these cases and, ime, caught more actual problems.
But we need contracts that go way further what static typing provides. If they add dependant types + ability to enforce the types at runtime so that you can use it on various inputs, then maybe it will be truly useful.
FTA:
> There’s just this minimal translation required between what I’m thinking and what I type
That's really the essence of Ruby for me.
What is STOA standing for here, please?
Likely a typo of State Of The Art.
We've been running Rails apps in production continuously since 2007. If you treat software as anything other than completely disposable, it's been a no-brainer for the entire 19+ years I've been paying attention (not despite its age, but because of it).
The premise that you get meaningful efficiencies from JavaScript on the back-end just because you have to use it on the front-end has been pretty thoroughly debunked at this point. Instead you mostly get a larger blast radius when the front-end ecosystem has its monthly identity crisis. OP's "stacks-du-jour" and programming language "flavour of the month" framing is exactly right. A shocking amount of web software architecture is just following fashion trends dressed up as technical decision-making.
Most of the churn in tech stack isn't driven by engineering requirements, it's driven by résumé optimization and Hacker News anxiety. Rails has quietly been powering serious businesses the whole time. Does anyone think NPM's 3.1 million packages enable more functionality than RubyGems' 190,000 packages?
We've also been running Rails in production for 15+ years (since 2011) in two companies and it has been serving us greatly. Hiring is tough, but I definitely believe the stack makes up for it due to the productivity gains.
In late 2025 we decided to migrate one of them to Inertia. Public facing pages is already done, and we're 80% through migrating the logged in area (it's a huge app). We choose Vue.js.
It's amazing how powerful this stack is and how little you have to change in the backend.
I'm surprised hiring is tough. The job market is such trash rn and I feel there are a lot of Rubyists, or ex-Rubists interested in returning to it, around. Maybe not? (Edit: spelling)
> Maybe not?
Because there are fewer and fewer ruby/rails people available.
It is the simplest explanation - and the one that makes the most sense, too.
Well ya, I'm just saying I'm surprised considering the current job market. I moved on from Rails about 5 years ago now, but have 9 years experience under my belt and still keep up a bit with new things and play with them once in a while. And yet I've applied for several Rails positions in the past few years and always get an outright rejection.
> Does anyone think NPM's 3.1 million packages enable more functionality than RubyGems' 190,000 packages?
It means that there are many more people using NPM.
That means more users. More users is almost always better, for any language.
Also many of those gems on rubygems are dead since decades, literally. Probably also for NPM. We can not just compare the numbers without analysis.