This is...not for me. It follows a big pattern in Ruby/Rails culture where to understand the code, you first have to understand the magic. And, it's never all that obvious where to go to try and understand the magic, because the magic itself has been imported by magic.
I once was hackathoning with a colleague who was trying to get me excited about Rails, and he said, "look how great this is -- if you want the idea of '1 day', you can just write `1.day`!". I opened up a irb to try it out, and it didn't work. We were both confused for a bit until he figured out that it was a Rails thing, not a Ruby thing. That Rails globally punches date/time methods into integers, which he thought was cool, and I thought was abhorrent. I asked, "okay, if I came across this code, how would I be able to know that it came from Rails?" He said, there wasn't any way to really trace a method to its source definition, you just kinda have to know, and I decided this whole thing was too much of a conflict with my mental model for how humans and code and computers should work together.
1.method(:day).source_location <= your friend was wrong shrug
Look, I'm not a big fan of all of Rails' monkeypatching. That's why I don't use Rails anymore, I use other Ruby frameworks like Bridgetown, Roda, and Hanami. But there's definitely a way to dive into the "magic" and find out what's going on.
you monkeypatch over core methods like that they put you in jail right away no trial, no nothing. Metaprogrammers... we have a special jail for metaprogrammers. You're global variabling? Right to jail. You're using for loops instead of iterators? To jail right away. You're type checking by class... Jail. Abusing eval? Jail. You're using global variables like a drunken sailor? You right to Jail. You overuse class variables Jail. You underuse class variables believe it or not jail. You use too many tests also Jail, overuse underuse. You use define method and you could have created the methods explicitly? Believe it or not, jail right away. We have the best code monkeys in the world because of jail.
how does this work if i import two different gems that both monkeypatch the same classes?
Even if you can see the source, it still seems difficult to understand where the monkeypatch came from if you have transitive dependencies and whatnot.
No, he is in fact correct. Rails used to be full of code such as the following, though I now see that they actually annotate code generated at Rails startup runtime with the file and the line of at least the call to the code generator [0].
There is even a fucking issue on the ruby-lang issue tracker from two years ago griping about this dark corner of the language [1].
No matter though, I don't expect average laymen to delve into this level of detail nor assess the veracity of statements at this layer.
Most Rubyists at this point are at the level of "terminal dumbfuck," and are indistinguishable from a bartender who stumbled into a 16 week bootcamp for blockchain development. The one silver lining of the great AI Happening is that these people will be put back into their place.
irb(main):001\* class Foo
irb(main):002" class_eval <<-RB
irb(main):003" def foobar()
irb(main):004" puts "foobar"
irb(main):005" end
irb(main):006\* RB
irb(main):007> end
=> :foobar
irb(main):008> Foo.new
=> #<Foo:0x00007ff5c1a8c190>
irb(main):009> Foo.new.method(:foobar)
=> #<Method: Foo#foobar() (eval at (irb):2):1>
irb(main):010> Foo.new.method(:foobar).source_location
=> ["(eval at (irb):2)", 1]
That's one thing which ruby unfortunately did not adopt from Smalltalk. In Smalltalk (at least, in the dialects I'm familiar with), the "method categories" metadata is used to signal that we're adding new methods (or overwriting existing ones) to classes that are outside the scope of this package (ie: classes you didn't create as part of your app).
That way, it's easy to trace, forwards (from package to all the methods it introduces) & backwards (from method to package), who introduced a method, where, and why.
Other than that, I think a lot of this aversion to "ruby magic" is a bit overblown. The ability to cleanly remold any part of the system with minimal friction, to suit the app you're building right now - that's a KEY part of what makes it special.
Its like all these polemics warning wannabe lispers away from using macros. Lisp, Smalltalk, and ruby, all give you very powerful shotguns to express your creative ideas. If you can't stop blowing your own foot off, then pick a different language with a different paradigm.
I think this is the pattern I would reach for as well, separating the data from the execution. Being declarative about the plans (either with a config file, db backend, or simply a PORO) allows the plans themselves to be agnostic to how they are used and leaves you room to write a clean API for their creation without mixing in their definition.
Also ActiveSupport has Object#with_options which has a similar intent, but I rarely ever see it used in codebases.
Exactly. Use a fancy expressive structure if you want, but don't try to abstract away the mapping between that and the general-purpose code that it relies on. "Each domain has its own rules"? How would I even know where to look for those?
When I say Ruby is inefficient it’s not just the language, it’s stuff like this. I don’t fault the author but this kind of stuff is endemic.
This way of handling attributes is monumentally less efficient than just using keyword attributes, which are optimized by the runtime.
Unfortunately you’ll find this is every Ruby code base: tiny readability improvements that are performing allocations and wasting cycles for no real reason other than looking better.
I’ve certainly done that and it’s expected, efficient code looks “weird”. A regular “each” loop that looks complicated will be transformed into multiple array method chaining, allocating the same array many times. If you don’t do it someone else will.
The usual response to this complaint in the Ruby/Rails community is that optimizing for nanoseconds, or even milliseconds doesn't matter when the same operation also involves multiple database queries or API calls.
This attitude towards wastefulness is how you have web apps that could run in a single machine but struggle to run in a server cluster.
And after a couple years even Postgres is struggling because the amount of queries is too massive because of abstractions that don’t lend themselves to optimization.
Also it’s how you have codebases that could be maintained by two or three suddenly needing dozens because the testing suite needs hours to run and people even celebrate when there’s no tests in sight.
Just anecdotal personal experience. But I saw this happening inside at least 4 successful companies that started with Rails but didn’t care about those problems, and ended up wanting/having to move to something else.
The reality is most companies and products never blow bast the point of needed to ditch Rails. The argument made at the time was scaling horizontally is cheaper than hiring new devs, and you probably will never need to scale that much horizontally.
Test suite bloat is a different problem that stems from the lack of incremental typing which I think is what ultimately killed Ruby and Rails.
Any big Rails codebase can be a nightmare to grok unless people have been diligent about documenting what different methods return.
> The usual response to this complaint in the Ruby/Rails community is that optimizing for nanoseconds, or even milliseconds doesn't matter when the same operation also involves multiple database queries or API calls
The problem with that logic is that it’s pervasive: people have that same attitude everywhere even if no IO is being done. That’s how we get multi gigabyte processes.
The whole language (and Rails) also pushes you towards a less efficient path. For instance you’re probably iterating over those six plans and inserting them individually in the DB. Another approach would’ve been to accumulate all of them in memory then build and perform a single query. That’s not something people really consider because it’s “micro” optimization and makes the code look worse. But if you miss out on hundreds of these micro optimizations then you get a worse system.
In a general sense optimizing Ruby is indeed futile: any optimization is dwarfed by just choosing a different language.
I say all this as someone who has worked with it for two decades, I like the language, it’s just laughably inefficient.
Have you used it over the last few years? It has it been rapidly improving, mainly because Shopify put a team full time on it. It doesn’t take a lot of people to optimize a VM/interpreter it just has to be the right people.
And the question is always “fast enough for what?” Different languages are more suitable for different types of projects. I wouldn’t code a rendering engine in Ruby but for web apps it’s amazing.
Yes, every web app I’ve worked on the past ~18 years has been with Rails. I’ve seen it all except an efficient app. Sure, Ruby and Rails never bankrupted these companies but they’d all have been better off with something else. Certain cloud bills would’ve been much smaller for sure.
Those optimizations to the VM are just very workload specific and become less relevant today when you’re using containers and fractional CPU/mem. It also doesn’t take much for a dev to write the wrong code and make them irrelevant again. Even if you get everything right you’re leaving so much performance on the table it feels like crumbs.
For small web apps Rails is fine though. I just never worked on one. The issue is perhaps no one threw the code away when it got big.
Just want to reiterate what the sibling commenter said, it's dead on with my experience.
Static typing would be the main thing teams would've been better off with. I was big on dynamic languages, love Clojure/LISPs and still work with Ruby and JS today, but you just can't trust 100 developers with it. Last company I worked for I ran the dev team and did some bug analysis: conservatively 60% of bugs were things a simple static type system would've caught.
Very few business logic bugs. We had loads of tests but these simple bugs still popped up. Someone in team A would change a method's return type, find and replace in the codebase, but miss some obscure case from team D. Rinse and repeat. Nothing complicated, just discipline but you can't trust discipline on a 500k LoC codebase and a language with no guardrails.
Performance would've been the other main advantage of static typing. While most people think their Rails app will be IO-bound forever that's really downplaying their product. In actuality every company that mildly succeeds will start to acquire CPU-bound workloads and it'll come a point where they are the bottleneck. One might argue that it is at this point you ditch Ruby but in reality no one really wants to run a polyglot company: it's hard to hire, hard to fill in gaps, hard to manage and evaluate talent.
People underestimate the impact of performance on the bottom line these days with phrases like "memory is cheap; devs are not". Like the sibling commenter put it the monthly cloud bill on that last company would've paid about 20 dev salaries. Most of that was for the app servers. That for an app that served about 500 req/sec at peak. You can imagine the unnecessary pressure that puts on the company's finances.
Better choices would've been Go, Rust, even something on the JVM.
The thing about "it's IO bound anyway so who cares" is that it forces you to scale the app much earlier.
At a company I worked, a single Golang instance (there was backup) was able to handle every single request by itself during peak hours, and do authentication, partial authorization, request enrichment, fraud detection, rate limiting and routing to the appropriate microservice. I'm not saying it was a good idea to have a custom Ingress/Proxy app but it's what we had.
By contrast, the mesh of Rails applications required a few hundred machines during peak time to serve the same number of requests, and none of it was CPU-heavy. It was DB heavy!
If it had been a Golang or JVM or Rust app it would require a much smaller fleet to serve.
Rails apps can get very expensive server wise because the “IO is slow anyways” attitude means more servers will be needed to serve the same amount of requests. For a specific bad case I worked at, the cloud bill was the same cost of 15 senior developers. And it was an app without external users (I was actually responsible for the external parts of it, it was isolated and not in Rails).
Excessive abstraction at the ORM can also make it extremely difficult to optimize db queries, so each user request can trigger way more DB queries than necessary, and this will require more db power. I have seen this happening over and over due to abstraction layers such as Trailblazer, but anything that is too layered clean-code style will cause issues and requires constant observation. And refactoring is made difficult due to “magic”. Even LLMs might find it too much.
Another problem with the slowness is that it slows down local development too. The biggest test suite I ever saw took 2 hours to run in a 60-machine cluster, so 120 hours of CI. Impossible to run locally, so major refactoring was borderline impossible without a huge feedback cycle.
The solution for the slow development ends up being hiring more developers, of course, with each one responsible for a smaller part of the app. In other companies these kind of features I saw would be written by people over days, not by team over months.
The terseness of both Ruby and Rails is also IMO countered by the culture of turning 10-line methods into bigger classes and using methods and instance variables instead of local variables. So it also hurts both readability (because now you have 5x more lines than needed) but also hurts optimization and stresses the garbage collection. If you know this, you know. I have seen this in code from North+Latin American, European and Japanese companies, so it’s not isolated cases. If you don’t know I can provide examples.
I have seen this happening with other tech too, of course, but with Rails it happens much much faster IME.
It is also 100% preventable, of course, however a lot of advice on how to prevent these problems will clash with Ruby/Rails traditions and culture.
These are just examples out of personal experience, but definitely not isolated cases IMO.
> For instance you’re probably iterating over those six plans and inserting them individually in the DB. Another approach would’ve been to accumulate all of them in memory then build and perform a single query. That’s not something people really consider because it’s “micro” optimization and makes the code look worse.
This same pitfall exists in every language. This has nothing to do with Ruby.
This is a perfect example of something that looks good in a demo but fails in a real product. Business logic and 'packages' are never this clean or simple.
Putting this kind of type-based 'magic' in the code is a bad decision that will bite you very soon. It optimizes for being 'cute' rather than being clear and maintainable, and that's a trade-off that almost never pays off.
That link describes billing problems of a neobank... I mean, yes, there's a big gap between my test helpers and financial institution's problems - to the point it's not related at all.
But, in principle I agree billing, even the simple SaaS stuff, is much harder than most people expect it to be in 2025. My product (linked in the original article) is based completely on Stripe Billing - and it is still very hard to avoid all the footguns.
For people wondering, I even have an example how wrong it can go: I "audited" a successful SaaS I know uses custom Stripe billing. I paid $30 for a starter plan, but was able to "upgrade" to $2k plain for free. Here's the full video: https://www.youtube.com/watch?v=YuXp7V4nanU
Feels odd that two feature-equivalent plans are segregated with neighboring duplicates into monthly and yearly branches. I would consider monthly Enterprise & yearly Enterprise the same plan, with modified cost & billing frequency.
> I would consider monthly Enterprise & yearly Enterprise the same plan, with modified cost & billing frequency.
How would you then call the objects that store costs and billing frequency? :)
Here's what Stripe uses:
- Product: "describes the goods or services". This is where you define a (plan) name and features.
- Price: defines the amount, currency, and (optional) billing interval. Since interval is optional, Prices can be used to define both recurring, and one-off purchases.
Technically, using Prices for recurring, and one-off payments is a brilliant idea. The problem is, no one refers to recurring payments as "prices". Everyone calls a "$50 per year" option a "plan".
It is not only the verbosity or use of trailing '!' in a method
for no real reason, IMO, but also things such as "1.month". I
understand that rails thrives as a DSL, but to me having a method
such as .month on an Integer, is simply wrong. Same with
HashWithIndifferentAccess - I understand the point, to not have
to care whether a key is a String or a Symbol, but it is simply
the wrong way to think about this. People who use HashWithIndifferentAccess
do not understand Symbols.
The exclamation mark has a reason: if the newly created records fails validations, an exception is raised. Without the exclamation mark, the error is silenced (and the method returns a falsey value). This is a convention across Rails.
Ruby itself mostly uses it for mutating methods (e.g. #gsub("a", "b") replaces the character a with b in a string and returns a new string, but #gsub!("a", "b") mutates the original.
> I understand that rails thrives as a DSL, but to me having a method such as .month on an Integer, is simply wrong
It's not that different from `1.times` or `90.chr` which are vanilla Ruby.
> HashWithIndifferentAccess
HashWithIndifferentAccess was an unfortunate necessity to avoid DOS attacks when Symbols used to be immortal. There's no longer a reason to use it today, except for backward compatibility.
`1 + 1` in Ruby is syntactic sugar for `1.+(1)`. Nothing wrong about it at all, it's just different from what you're apparently used to. This type of thing isn't even unique to Ruby or even OOP.
This is a typical API design in Ruby, but the post is about a somewhat novel API design than what you're pointing out. To address some of your points:
The exclamation mark is a convention. It is used whenever a method could possibly result in an exception being raised. Sometimes it's instead used for non-idempotent methods.
"3.days", etc are Rails things. A lot of non-Rubyists don't like it but once you use it for long enough you tend to really grow to it.
As for HashWithIndifferentAccess, yes this is generally acknowledged as a mistake in Ruby's design and is rarely used in my experience. Originally, all Ruby hashes were HWIA. When they finally realized this was a design mistake they had to create HWIA for some level of backwards compatibility
So the underlying assumption is that there is always at least one attribute that serves as a "discriminator" between the billing plans, right? Is it possible to represent something like this then?
Your input would work exactly as you wrote it if passed to `Billing::Plan.find_or_create_all_by_attrs!`, just add commas at the end of lines.
If you want to make it even shorter, you have a few options - it really just comes down to preference:
# Option 1. my personal favorite, follows structure of
# intervals and plans on a pricing page.
1.month => {red: 10, blue: 120},
1.year => {red: 120, blue: 300}
# Option 2. this is fine too
red: {1.month => 10, 1.year => 120},
blue: {1.month => 120, 1.year => 300}
# Option 3. possible and works, but hurts my brain, NOT recommended
10 => {red: 1.month},
120 => {red: 1.year, blue: 1.month},
300 => {blue: 1.year}
> there is always at least one attribute that serves as a "discriminator" between the billing plans, right
Just a note: if you try to create two plans with the same attributes, that would error because of ActiveRecord uniqueness validations (and DB constraints). No point in having multiple identical plans.
Haters gonna hate. My take: DSLs are a useful way to make code easier to read, and more importantly easier to write correctly. Exploring this space and sharing your learnings is useful and valuable.
Ruby is a language that optimizes for the local maxima at the cost of the global maxima.
Now every library, company or code base has its own pattern and you have to learn its pit falls. Better to learn once, cry once and just deal with it imo.
As they say, good enough is the enemy of perfection.
Article author here - thank you for putting it this way. This is exactly the attitude I wanted to convey: it's something I tried and really liked for this specific use case. I shared because I hope it might inspire others.
"Friendly Attributes" is not the "new way", not to be used "everywhere now", does not "apply to all scenarios".
If you like it, maybe you'll use it once in the next five years when the opportunity arises.
I'll add another cautionary word in with everyone else who is panning this implementation.
This is just using operator overloading to determine keywords, but it locks you out of ever using the same type twice in your signature. Notice that :usd turns into a name. What?
This is cute, but has no place in a professional software interface.
Yikes. This means that you’ll have 1000 micro-DSLs sprinkled all over your codebase, which will become unreadable and lead to confusion/accidents. Better to stick with good ol’ key-value labelling.
This is a philosophy. One which many people that write Ruby subscribe to. The fundamental idea is: create a DSL that makes it very easy to implement your application. It is what made Rails different when it was created: it is a DSL that makes expressing web applications easy.
I don't know its history well enough, but it seems to originate from Lisp. PG wrote about it before [1].
It can result in code that is extremely easy to read and reason about. It can also be incredibly messy. I have seen lots of examples of both over the years.
It is the polar opposite of Go's philosophy (be explicit & favour predictability across all codebases over expressiveness).
Yeah, this is honestly the sort of thing I grew to hate in Ruby. It looks cute, but all it does is create more cruft. Good ol’ boring keys are just fine, expressive enough, and are very unlikely to cause problems. This feels like it’s attempting to solve a problem that does not exist.
> this is honestly the sort of thing I grew to hate in Ruby
But nobody forces you to use a DSL such as rails, so I am not
sure why ruby should be hated for this when it is a rails dev
who does that.
The blog has much more to do with rails than ruby; such API
design is really strange.
I don't think this design causes problems as such, but it is
too verbose and way too ugly. To me it seems that they are
just shuffling data structures around; that could even be
solved via yaml files.
Off-topic, but unlike the example pricing plans, don’t make your SaaS’s “standard” plan $10/month. If you want a place to start, start with $50/month.
Or, as Patrick McKenzie used to tell us over and over, “charge more”.
(Yes, yes, I know some situations, customers, product, thinking, etc are different. But with broad brushstrokes, my advice is to not even entertain such a low price.)
This is such a broad generalization as to be useless. I use several pieces of software that are around $10/month which there’s no way in hell I would pay $50 for.
Which leads me to another piece of advice: don’t do B2C. Sell to businesses who will be far more willing to pay higher prices, will churn at a lower rate, and will - in general - require less support.
This is...not for me. It follows a big pattern in Ruby/Rails culture where to understand the code, you first have to understand the magic. And, it's never all that obvious where to go to try and understand the magic, because the magic itself has been imported by magic.
I once was hackathoning with a colleague who was trying to get me excited about Rails, and he said, "look how great this is -- if you want the idea of '1 day', you can just write `1.day`!". I opened up a irb to try it out, and it didn't work. We were both confused for a bit until he figured out that it was a Rails thing, not a Ruby thing. That Rails globally punches date/time methods into integers, which he thought was cool, and I thought was abhorrent. I asked, "okay, if I came across this code, how would I be able to know that it came from Rails?" He said, there wasn't any way to really trace a method to its source definition, you just kinda have to know, and I decided this whole thing was too much of a conflict with my mental model for how humans and code and computers should work together.
1.method(:day).source_location <= your friend was wrong shrug
Look, I'm not a big fan of all of Rails' monkeypatching. That's why I don't use Rails anymore, I use other Ruby frameworks like Bridgetown, Roda, and Hanami. But there's definitely a way to dive into the "magic" and find out what's going on.
Ah, that's good to know! Yeah, he was wrong about a lot of stuff, so that adds up. He ended up in jail for a stint.
you monkeypatch over core methods like that they put you in jail right away no trial, no nothing. Metaprogrammers... we have a special jail for metaprogrammers. You're global variabling? Right to jail. You're using for loops instead of iterators? To jail right away. You're type checking by class... Jail. Abusing eval? Jail. You're using global variables like a drunken sailor? You right to Jail. You overuse class variables Jail. You underuse class variables believe it or not jail. You use too many tests also Jail, overuse underuse. You use define method and you could have created the methods explicitly? Believe it or not, jail right away. We have the best code monkeys in the world because of jail.
Woah, this thread escalated quickly.
whoa
Quite the plot twist there
how does this work if i import two different gems that both monkeypatch the same classes?
Even if you can see the source, it still seems difficult to understand where the monkeypatch came from if you have transitive dependencies and whatnot.
The last one wins
No, he is in fact correct. Rails used to be full of code such as the following, though I now see that they actually annotate code generated at Rails startup runtime with the file and the line of at least the call to the code generator [0].
There is even a fucking issue on the ruby-lang issue tracker from two years ago griping about this dark corner of the language [1].
No matter though, I don't expect average laymen to delve into this level of detail nor assess the veracity of statements at this layer.
Most Rubyists at this point are at the level of "terminal dumbfuck," and are indistinguishable from a bartender who stumbled into a 16 week bootcamp for blockchain development. The one silver lining of the great AI Happening is that these people will be put back into their place.
[0] https://github.com/rails/rails/blob/9b054c66d4fc5ec526cff356...[1] https://bugs.ruby-lang.org/issues/19755
That's one thing which ruby unfortunately did not adopt from Smalltalk. In Smalltalk (at least, in the dialects I'm familiar with), the "method categories" metadata is used to signal that we're adding new methods (or overwriting existing ones) to classes that are outside the scope of this package (ie: classes you didn't create as part of your app).
That way, it's easy to trace, forwards (from package to all the methods it introduces) & backwards (from method to package), who introduced a method, where, and why.
Other than that, I think a lot of this aversion to "ruby magic" is a bit overblown. The ability to cleanly remold any part of the system with minimal friction, to suit the app you're building right now - that's a KEY part of what makes it special.
Its like all these polemics warning wannabe lispers away from using macros. Lisp, Smalltalk, and ruby, all give you very powerful shotguns to express your creative ideas. If you can't stop blowing your own foot off, then pick a different language with a different paradigm.
I think this is the pattern I would reach for as well, separating the data from the execution. Being declarative about the plans (either with a config file, db backend, or simply a PORO) allows the plans themselves to be agnostic to how they are used and leaves you room to write a clean API for their creation without mixing in their definition.
Also ActiveSupport has Object#with_options which has a similar intent, but I rarely ever see it used in codebases.
Exactly. Use a fancy expressive structure if you want, but don't try to abstract away the mapping between that and the general-purpose code that it relies on. "Each domain has its own rules"? How would I even know where to look for those?
When I say Ruby is inefficient it’s not just the language, it’s stuff like this. I don’t fault the author but this kind of stuff is endemic.
This way of handling attributes is monumentally less efficient than just using keyword attributes, which are optimized by the runtime.
Unfortunately you’ll find this is every Ruby code base: tiny readability improvements that are performing allocations and wasting cycles for no real reason other than looking better.
I’ve certainly done that and it’s expected, efficient code looks “weird”. A regular “each” loop that looks complicated will be transformed into multiple array method chaining, allocating the same array many times. If you don’t do it someone else will.
The usual response to this complaint in the Ruby/Rails community is that optimizing for nanoseconds, or even milliseconds doesn't matter when the same operation also involves multiple database queries or API calls.
Let's take this example from the article:
This ensures six billing plans are created. That means 6 DB queries and 6 Stripe API queries, at a minimum.This attitude towards wastefulness is how you have web apps that could run in a single machine but struggle to run in a server cluster.
And after a couple years even Postgres is struggling because the amount of queries is too massive because of abstractions that don’t lend themselves to optimization.
Also it’s how you have codebases that could be maintained by two or three suddenly needing dozens because the testing suite needs hours to run and people even celebrate when there’s no tests in sight.
Just anecdotal personal experience. But I saw this happening inside at least 4 successful companies that started with Rails but didn’t care about those problems, and ended up wanting/having to move to something else.
Again, though, these bottlenecks are because of how the system queries the database, not how methods are dispatched.
I agree on the ORM abstractions causing huge performance issues, but it has nothing to do with Ruby’s dynamic method declarations.
I'm not talking about method dispatch, I'm talking about the "usual response to this complaint in the Ruby/Rails community".
The reality is most companies and products never blow bast the point of needed to ditch Rails. The argument made at the time was scaling horizontally is cheaper than hiring new devs, and you probably will never need to scale that much horizontally.
Test suite bloat is a different problem that stems from the lack of incremental typing which I think is what ultimately killed Ruby and Rails.
Any big Rails codebase can be a nightmare to grok unless people have been diligent about documenting what different methods return.
Nothing has “killed Ruby on Rails”.
Ridiculous comment.
I'm honored to have you of all people make that comment.
But with all due respect the excitement and job market for Ruby isn't anything close to what it used to be:
[0]: https://trends.google.com/trends/explore?date=all&q=%2Fm%2F0...
> The usual response to this complaint in the Ruby/Rails community is that optimizing for nanoseconds, or even milliseconds doesn't matter when the same operation also involves multiple database queries or API calls
The problem with that logic is that it’s pervasive: people have that same attitude everywhere even if no IO is being done. That’s how we get multi gigabyte processes.
The whole language (and Rails) also pushes you towards a less efficient path. For instance you’re probably iterating over those six plans and inserting them individually in the DB. Another approach would’ve been to accumulate all of them in memory then build and perform a single query. That’s not something people really consider because it’s “micro” optimization and makes the code look worse. But if you miss out on hundreds of these micro optimizations then you get a worse system.
In a general sense optimizing Ruby is indeed futile: any optimization is dwarfed by just choosing a different language.
I say all this as someone who has worked with it for two decades, I like the language, it’s just laughably inefficient.
Have you used it over the last few years? It has it been rapidly improving, mainly because Shopify put a team full time on it. It doesn’t take a lot of people to optimize a VM/interpreter it just has to be the right people.
And the question is always “fast enough for what?” Different languages are more suitable for different types of projects. I wouldn’t code a rendering engine in Ruby but for web apps it’s amazing.
Yes, every web app I’ve worked on the past ~18 years has been with Rails. I’ve seen it all except an efficient app. Sure, Ruby and Rails never bankrupted these companies but they’d all have been better off with something else. Certain cloud bills would’ve been much smaller for sure.
Those optimizations to the VM are just very workload specific and become less relevant today when you’re using containers and fractional CPU/mem. It also doesn’t take much for a dev to write the wrong code and make them irrelevant again. Even if you get everything right you’re leaving so much performance on the table it feels like crumbs.
For small web apps Rails is fine though. I just never worked on one. The issue is perhaps no one threw the code away when it got big.
Can you explain why you say they would be better off? What else would be a better choice and why?
Just want to reiterate what the sibling commenter said, it's dead on with my experience.
Static typing would be the main thing teams would've been better off with. I was big on dynamic languages, love Clojure/LISPs and still work with Ruby and JS today, but you just can't trust 100 developers with it. Last company I worked for I ran the dev team and did some bug analysis: conservatively 60% of bugs were things a simple static type system would've caught.
Very few business logic bugs. We had loads of tests but these simple bugs still popped up. Someone in team A would change a method's return type, find and replace in the codebase, but miss some obscure case from team D. Rinse and repeat. Nothing complicated, just discipline but you can't trust discipline on a 500k LoC codebase and a language with no guardrails.
Performance would've been the other main advantage of static typing. While most people think their Rails app will be IO-bound forever that's really downplaying their product. In actuality every company that mildly succeeds will start to acquire CPU-bound workloads and it'll come a point where they are the bottleneck. One might argue that it is at this point you ditch Ruby but in reality no one really wants to run a polyglot company: it's hard to hire, hard to fill in gaps, hard to manage and evaluate talent.
People underestimate the impact of performance on the bottom line these days with phrases like "memory is cheap; devs are not". Like the sibling commenter put it the monthly cloud bill on that last company would've paid about 20 dev salaries. Most of that was for the app servers. That for an app that served about 500 req/sec at peak. You can imagine the unnecessary pressure that puts on the company's finances.
Better choices would've been Go, Rust, even something on the JVM.
Yep.
The thing about "it's IO bound anyway so who cares" is that it forces you to scale the app much earlier.
At a company I worked, a single Golang instance (there was backup) was able to handle every single request by itself during peak hours, and do authentication, partial authorization, request enrichment, fraud detection, rate limiting and routing to the appropriate microservice. I'm not saying it was a good idea to have a custom Ingress/Proxy app but it's what we had.
By contrast, the mesh of Rails applications required a few hundred machines during peak time to serve the same number of requests, and none of it was CPU-heavy. It was DB heavy!
If it had been a Golang or JVM or Rust app it would require a much smaller fleet to serve.
Not GP but I can answer:
Rails apps can get very expensive server wise because the “IO is slow anyways” attitude means more servers will be needed to serve the same amount of requests. For a specific bad case I worked at, the cloud bill was the same cost of 15 senior developers. And it was an app without external users (I was actually responsible for the external parts of it, it was isolated and not in Rails).
Excessive abstraction at the ORM can also make it extremely difficult to optimize db queries, so each user request can trigger way more DB queries than necessary, and this will require more db power. I have seen this happening over and over due to abstraction layers such as Trailblazer, but anything that is too layered clean-code style will cause issues and requires constant observation. And refactoring is made difficult due to “magic”. Even LLMs might find it too much.
Another problem with the slowness is that it slows down local development too. The biggest test suite I ever saw took 2 hours to run in a 60-machine cluster, so 120 hours of CI. Impossible to run locally, so major refactoring was borderline impossible without a huge feedback cycle.
The solution for the slow development ends up being hiring more developers, of course, with each one responsible for a smaller part of the app. In other companies these kind of features I saw would be written by people over days, not by team over months.
The terseness of both Ruby and Rails is also IMO countered by the culture of turning 10-line methods into bigger classes and using methods and instance variables instead of local variables. So it also hurts both readability (because now you have 5x more lines than needed) but also hurts optimization and stresses the garbage collection. If you know this, you know. I have seen this in code from North+Latin American, European and Japanese companies, so it’s not isolated cases. If you don’t know I can provide examples.
I have seen this happening with other tech too, of course, but with Rails it happens much much faster IME.
It is also 100% preventable, of course, however a lot of advice on how to prevent these problems will clash with Ruby/Rails traditions and culture.
These are just examples out of personal experience, but definitely not isolated cases IMO.
> For instance you’re probably iterating over those six plans and inserting them individually in the DB. Another approach would’ve been to accumulate all of them in memory then build and perform a single query. That’s not something people really consider because it’s “micro” optimization and makes the code look worse.
This same pitfall exists in every language. This has nothing to do with Ruby.
eh. Ruby makes it easy to do the wrong thing. imo.
Maybe, but this isn’t one of the ways it does.
i find this structure a bit odd. i would have gone for the following pattern:
This is supported and would work with no implementation changes. The "Friendly Attributes" idea is very flexible.
Just a small Ruby syntax correction for your example:
This is a perfect example of something that looks good in a demo but fails in a real product. Business logic and 'packages' are never this clean or simple.
Putting this kind of type-based 'magic' in the code is a bad decision that will bite you very soon. It optimizes for being 'cute' rather than being clear and maintainable, and that's a trade-off that almost never pays off.
Hi, I'm the author of the article and the software library. I confirm I actually do use the examples from the article in my code.
Here's the example that runs in hundreds of integration tests:
It asserts what plans the customers see on the pricing page.There's a massive gap between that pattern and the real-world complexity of billing. It's too much to cover in a comment, but this link explains the actual nightmare - https://www.getlago.com/blog/why-billing-systems-are-a-night...
That link describes billing problems of a neobank... I mean, yes, there's a big gap between my test helpers and financial institution's problems - to the point it's not related at all.
But, in principle I agree billing, even the simple SaaS stuff, is much harder than most people expect it to be in 2025. My product (linked in the original article) is based completely on Stripe Billing - and it is still very hard to avoid all the footguns.
For people wondering, I even have an example how wrong it can go: I "audited" a successful SaaS I know uses custom Stripe billing. I paid $30 for a starter plan, but was able to "upgrade" to $2k plain for free. Here's the full video: https://www.youtube.com/watch?v=YuXp7V4nanU
We can't really tell that without knowing where the code is used, no? It's not hard to imagine a test that checks the following:
It doesn't cover all possibilities of all currencies, but it doesn't need to. It covers the one case it needs to test.They do say they use this in their real production code.
Just because it's in their production code doesn't mean it's not a ticking time bomb.
You are still talking about the three line find_or_create in this article, right?
Feels odd that two feature-equivalent plans are segregated with neighboring duplicates into monthly and yearly branches. I would consider monthly Enterprise & yearly Enterprise the same plan, with modified cost & billing frequency.
> I would consider monthly Enterprise & yearly Enterprise the same plan, with modified cost & billing frequency.
How would you then call the objects that store costs and billing frequency? :)
Here's what Stripe uses:
- Product: "describes the goods or services". This is where you define a (plan) name and features.
- Price: defines the amount, currency, and (optional) billing interval. Since interval is optional, Prices can be used to define both recurring, and one-off purchases.
Technically, using Prices for recurring, and one-off payments is a brilliant idea. The problem is, no one refers to recurring payments as "prices". Everyone calls a "$50 per year" option a "plan".
I don't really like the API design. Perhaps in the rails-world this makes sense, but it looks really strange to me.
It is not only the verbosity or use of trailing '!' in a method for no real reason, IMO, but also things such as "1.month". I understand that rails thrives as a DSL, but to me having a method such as .month on an Integer, is simply wrong. Same with HashWithIndifferentAccess - I understand the point, to not have to care whether a key is a String or a Symbol, but it is simply the wrong way to think about this. People who use HashWithIndifferentAccess do not understand Symbols.The exclamation mark has a reason: if the newly created records fails validations, an exception is raised. Without the exclamation mark, the error is silenced (and the method returns a falsey value). This is a convention across Rails.
Ruby itself mostly uses it for mutating methods (e.g. #gsub("a", "b") replaces the character a with b in a string and returns a new string, but #gsub!("a", "b") mutates the original.
> I understand that rails thrives as a DSL, but to me having a method such as .month on an Integer, is simply wrong
It's not that different from `1.times` or `90.chr` which are vanilla Ruby.
> HashWithIndifferentAccess
HashWithIndifferentAccess was an unfortunate necessity to avoid DOS attacks when Symbols used to be immortal. There's no longer a reason to use it today, except for backward compatibility.
when *everything* is an object this kind of syntax makes absolutely sense and is quite convenient
my first thought too, as a rubyist.
Yes, "everything is an object" is an essential insight to understanding ruby.
`1 + 1` in Ruby is syntactic sugar for `1.+(1)`. Nothing wrong about it at all, it's just different from what you're apparently used to. This type of thing isn't even unique to Ruby or even OOP.
This is a typical API design in Ruby, but the post is about a somewhat novel API design than what you're pointing out. To address some of your points:
The exclamation mark is a convention. It is used whenever a method could possibly result in an exception being raised. Sometimes it's instead used for non-idempotent methods.
"3.days", etc are Rails things. A lot of non-Rubyists don't like it but once you use it for long enough you tend to really grow to it.
As for HashWithIndifferentAccess, yes this is generally acknowledged as a mistake in Ruby's design and is rarely used in my experience. Originally, all Ruby hashes were HWIA. When they finally realized this was a design mistake they had to create HWIA for some level of backwards compatibility
"This code should look less like it does what it's doing", that's the Ruby Way™.
this is very unnecessary. Arrays and maps transformations are really easy and concise in core ruby already, one line of map, to_h or whatever.
So the underlying assumption is that there is always at least one attribute that serves as a "discriminator" between the billing plans, right? Is it possible to represent something like this then?
```rb
```Every possible attribute (name, interval, amount) has at least two objects that share a value
Your input would work exactly as you wrote it if passed to `Billing::Plan.find_or_create_all_by_attrs!`, just add commas at the end of lines.
If you want to make it even shorter, you have a few options - it really just comes down to preference:
> there is always at least one attribute that serves as a "discriminator" between the billing plans, rightJust a note: if you try to create two plans with the same attributes, that would error because of ActiveRecord uniqueness validations (and DB constraints). No point in having multiple identical plans.
Haters gonna hate. My take: DSLs are a useful way to make code easier to read, and more importantly easier to write correctly. Exploring this space and sharing your learnings is useful and valuable.
Ruby is a language that optimizes for the local maxima at the cost of the global maxima.
Now every library, company or code base has its own pattern and you have to learn its pit falls. Better to learn once, cry once and just deal with it imo.
As they say, good enough is the enemy of perfection.
Article author here - thank you for putting it this way. This is exactly the attitude I wanted to convey: it's something I tried and really liked for this specific use case. I shared because I hope it might inspire others.
"Friendly Attributes" is not the "new way", not to be used "everywhere now", does not "apply to all scenarios".
If you like it, maybe you'll use it once in the next five years when the opportunity arises.
I wouldn't call this a DSL
I'll add another cautionary word in with everyone else who is panning this implementation.
This is just using operator overloading to determine keywords, but it locks you out of ever using the same type twice in your signature. Notice that :usd turns into a name. What?
This is cute, but has no place in a professional software interface.
> but it locks you out of ever using the same type twice in your signature.
I don't see how you drew that conclusion. It seems to me the author provided several examples of this not being the case. Care to elucidate?
Yikes. This means that you’ll have 1000 micro-DSLs sprinkled all over your codebase, which will become unreadable and lead to confusion/accidents. Better to stick with good ol’ key-value labelling.
This is a philosophy. One which many people that write Ruby subscribe to. The fundamental idea is: create a DSL that makes it very easy to implement your application. It is what made Rails different when it was created: it is a DSL that makes expressing web applications easy.
I don't know its history well enough, but it seems to originate from Lisp. PG wrote about it before [1].
It can result in code that is extremely easy to read and reason about. It can also be incredibly messy. I have seen lots of examples of both over the years.
It is the polar opposite of Go's philosophy (be explicit & favour predictability across all codebases over expressiveness).
[1]: https://paulgraham.com/progbot.html
If there is one DSL which is a central abstraction of one’s entire app, used in 100s of places—this is fine.
If there is a DSL such as Rails’ URL routing, which will be the same in every app—this is also fine.
When one makes 100s of micro-DSLs for object creation, that are only ever used in one or two places—this is pure madness.
Yeah, this is honestly the sort of thing I grew to hate in Ruby. It looks cute, but all it does is create more cruft. Good ol’ boring keys are just fine, expressive enough, and are very unlikely to cause problems. This feels like it’s attempting to solve a problem that does not exist.
The advantage is the amount of code minimized and not using a generic factory pattern. But that probably can be achieved with a bit less magic...
> this is honestly the sort of thing I grew to hate in Ruby
But nobody forces you to use a DSL such as rails, so I am not sure why ruby should be hated for this when it is a rails dev who does that.
The blog has much more to do with rails than ruby; such API design is really strange.
I don't think this design causes problems as such, but it is too verbose and way too ugly. To me it seems that they are just shuffling data structures around; that could even be solved via yaml files.
Working in a team means you are kind of forced to use what the team wants.
Of course you can try to convince them otherwise, or just be an asshole and mass-refactor to remove the DSLs.
But this kind of code is part of Ruby’s culture now.
The simple answer for anyone that doesn’t like this style is to leave Rails and Ruby for people who enjoy it.
It’s fine to hate it and want to distance yourself from it.
Off-topic, but unlike the example pricing plans, don’t make your SaaS’s “standard” plan $10/month. If you want a place to start, start with $50/month.
Or, as Patrick McKenzie used to tell us over and over, “charge more”.
(Yes, yes, I know some situations, customers, product, thinking, etc are different. But with broad brushstrokes, my advice is to not even entertain such a low price.)
This is such a broad generalization as to be useless. I use several pieces of software that are around $10/month which there’s no way in hell I would pay $50 for.
As an end user, there's no way I'd pay $50/month for any SaaS.
Lots of people feel the same.
Which leads me to another piece of advice: don’t do B2C. Sell to businesses who will be far more willing to pay higher prices, will churn at a lower rate, and will - in general - require less support.