I’ve explored DHH’s way of writing Rails applications. His introduction of CurrentAttributes and suppressors of callbacks a few years ago made me want to revisit his Youtube Screencasts on Basecamp 3 and try to really appreciate this approach with an open mind.
I soon understood our fundamental difference in thinking. DHH approaches application code as an interconnected web of rich models. Each model is chock-full of concerns (modules with generalized functionality). Each model’s methods produce ripple effects far and wide across associated models, via numerous callbacks spanning many modules. This approach is somewhat graph-like. You have a graph of rich nodes (by “rich” I mean that they provide the highest level of business functionality), and as you activate one, it activates other nodes at various distances in all directions. These ripple “activations” could be anything, from additional database interactions, to 3rd party API calls, emails and text messages sent out, logging, and lots of other stuff.
This finally made me realize that with such a dynamic way of viewing application behavior, where all business needs are embedded at the very core, it makes sense why one might want to suppress entire classes and categories of callbacks. Those ripple effects are nearly untraceable, and require blanket suppressors from the top. Instead of directing logic, we’re constraining logic that would otherwise spread in all kinds of surprising ways.
This also explains why it’s so difficult to avoid globals in this world. You don’t want to impede your vast ripple effects with such minutia as passing the same data across associations over and over. It’s a waste of time.
I want to say that this is a highly unusual approach, but to be frank, any consistent thought-out approach is unusual in our industry. Thoughtful codebases are unusual. So yes, it’s unusual to have a well-established approach consistently applied to the entire codebase.
I know many people disagree with DHH, but I have not heard them provide a good alternative way of architecting the entire application. I’ve seen rebuttals to individual features of Rails, as well as OOP-obsessed approaches that (to be frank) were even more difficult to follow, but still no holistic explanation on how Rails apps should be written for maximum maintainability.
My biggest problem with DHH’s approach is that in his videos it was really hard for me to follow the story that his code is telling. Since all ripple effects are unapologetically triggered via chains of callbacks, it was difficult to follow so many paths into so many directions (or shall we say, indirections), ending up with so many outcomes. I find that this doesn’t work well with how I think.
My alternative way of building apps is narrative centric. Instead of creating a web of nodes with ripple effects, I want to write short stories, each living in an entry point (whether that’s a controller action, bg job, rake task, or test). Each story has a beginning (the initiating request/call) and an end (the response/output), with side effects in between. I love seeing a complete story where every major plot point is clearly visible at the controller level. If plot points are often repeated in the same order, we can bundle them under a laconically-named function. I love that I can look at every entry point, see exactly what it does, and decide how best to optimize it. I can decide to put various calls into a transaction, group multiple calls into one for a more efficient SQL interaction. I can kick something off into background processing, or introduce an exponential backoff retry for any of the steps involved. I find that with the entry point narrative-centric approach I have the most clarity and flexibility to achieve these optimizations. On the contrary, codebases where “stories” fan out widely through callback chains, don’t lend themselves well to these tweaks. You are never sure what might trigger a particular code path, and whether a particular optimization can be applied for all possible triggers.
To write beautiful short stories, I prefer to focus my attention on building myself the “domain language”. I don’t mean the literal DSL. Rather, a bunch of libraries needed for my entry points to call into, such that entry routines appear clear and concise, yet still tell the complete story. Both Ruby and Rails allow for very expressive code of this kind, as long as your business logic is neatly packed away beneath proper interfaces. This is where I prefer to spend the most time. Write clean domain-driven interfaces, then write beautiful short stories with them.
I wrote this post to explain my decision not to use certain features of Rails.