[HN Gopher] Friendly attributes pattern in Ruby
___________________________________________________________________
Friendly attributes pattern in Ruby
Author : brunosutic
Score : 92 points
Date : 2025-11-02 19:02 UTC (6 days ago)
(HTM) web link (brunosutic.com)
(TXT) w3m dump (brunosutic.com)
| dudeinjapan wrote:
| 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.
| rubyn00bie wrote:
| 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.
| onli wrote:
| 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...
| shevy-java wrote:
| > 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.
| whstl wrote:
| 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.
| molf wrote:
| 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
| dudeinjapan wrote:
| 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.
| shevy-java wrote:
| I don't really like the API design. Perhaps in the rails-world
| this makes sense, but it looks really strange to me.
| Billing::Plan::Factory.find_or_create_by!( name: :pro,
| interval: 1.month, amount: 50 )
|
| 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.
| byroot wrote:
| > 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.
| Lukas_Skywalker wrote:
| 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.
| dagi3d wrote:
| when *everything* is an object this kind of syntax makes
| absolutely sense and is quite convenient
| apsurd wrote:
| my first thought too, as a rubyist.
|
| Yes, "everything is an object" is an essential insight to
| understanding ruby.
| culi wrote:
| 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
| sodapopcan wrote:
| `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.
| codesnik wrote:
| 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.
| kburman wrote:
| 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.
| nkrisc wrote:
| They do say they use this in their real production code.
| kburman wrote:
| Just because it's in their production code doesn't mean it's
| not a ticking time bomb.
| nurettin wrote:
| You are still talking about the three line find_or_create
| in this article, right?
| brunosutic wrote:
| 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:
| expect(billing_pricing_plans).to eq billing_plans(
| 1.month => [:free, :premium, :pro, :enterprise], 1.year
| => [:free, :premium, :pro, :enterprise] )
|
| It asserts what plans the customers see on the pricing page.
| kburman wrote:
| 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...
| Lukas_Skywalker wrote:
| 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: bill = FactoryBot.create(:bill,
| products: []) expect(bill.currency).to eq("USD")
|
| It doesn't cover all possibilities of all currencies, but
| it doesn't need to. It covers the one case it needs to
| test.
| brunosutic wrote:
| 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
| stevoski wrote:
| 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.)
| anamexis wrote:
| 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.
| PufPufPuf wrote:
| As an end user, there's no way I'd pay $50/month for any SaaS.
| stevoski wrote:
| 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.
| dlisboa wrote:
| 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.
| brunosutic wrote:
| 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:
| Billing::Plan.find_or_create_all_by_attrs!( 1.month =>
| {standard: 10, pro: 50, enterprise: 100}, 1.year =>
| {standard: 100, pro: 500, enterprise: 1000} )
|
| This ensures six billing plans are created. That means 6 DB
| queries and 6 Stripe API queries, at a minimum.
| dlisboa wrote:
| > 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.
| radanskoric wrote:
| 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.
| dlisboa wrote:
| 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.
| radanskoric wrote:
| Can you explain why you say they would be better off?
| What else would be a better choice and why?
| whstl wrote:
| 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.
| boredtofears wrote:
| > 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.
| AlphaSite wrote:
| eh. Ruby makes it easy to do the wrong thing. imo.
| boredtofears wrote:
| Maybe, but this isn't one of the ways it does.
| em-bee wrote:
| i find this structure a bit odd. i would have gone for the
| following pattern:
| Billing::Plan.find_or_create_all_by_attrs!(
| standard => {1.month: 10, 1.year: 100}, pro =>
| {1.month: 50, 1.year: 500}, enterprise => {1.month:
| 100, 1.year: 1000} )
| brunosutic wrote:
| 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:
| Billing::Plan.find_or_create_all_by_attrs!(
| standard: {1.month => 10, 1.year => 100}, ...
| )
| whstl wrote:
| 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.
| fny wrote:
| 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.
| obiefernandez wrote:
| Nothing has "killed Ruby on Rails".
|
| Ridiculous comment.
| fny wrote:
| 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...
| cortesoft wrote:
| 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.
| whstl wrote:
| I'm not talking about method dispatch, I'm talking about
| the _" usual response to this complaint in the Ruby/Rails
| community"_.
| some1else wrote:
| 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.
| brunosutic wrote:
| > 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".
| sfgvvxsfccdd wrote:
| 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.
| culi wrote:
| I wouldn't call this a DSL
| brunosutic wrote:
| 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.
| AlphaSite wrote:
| 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.
| drzel wrote:
| plans = { 1.month => {standard: 10, pro: 50,
| enterprise: 100}, 1.year => {standard: 100, pro: 500,
| enterprise: 1000} } plans.each do
| |interval, details| details.each do |name, amount|
| Billing::Plan::Factory.find_or_create_by!(name: , interval:,
| amount:) end end
| bradly wrote:
| 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.
| Footkerchief wrote:
| 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?
| PufPufPuf wrote:
| "This code should look less like it does what it's doing", that's
| the Ruby Way(tm).
| montroser wrote:
| 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.
| jaredcwhite wrote:
| 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.
| montroser wrote:
| 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.
| jaredcwhite wrote:
| whoa
| bradgessler wrote:
| Quite the plot twist there
| radanskoric wrote:
| Woah, this thread escalated quickly.
| isr wrote:
| 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.
| nvader wrote:
| 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.
| culi wrote:
| > 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?
| culi wrote:
| 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 [:red, 1.month, 10] [:red, 1.year,
| 120] [:blue, 1.month, 120] [:blue, 1.year, 300]
|
| ```
|
| Every possible attribute (name, interval, amount) has at least
| two objects that share a value
| brunosutic wrote:
| 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.
___________________________________________________________________
(page generated 2025-11-08 23:01 UTC)