Dynamic Ruby And Hidden Maintenance Costs · Daniela Baron

Ask questions Research chat →

https://danielabaron.me/blog/dynamic-ruby-and-long-term-maintenance/ · scraped

ruby

Attachments

Scraped Content

— 1062 words · 2026-05-19 19:23:48 UTC ·

Excerpt

![](https://prod-files-secure.s3.us-west-2.amazonaws.com/871f1661-80b8-4d0c-ac3b-2adfc6ff4c66/7f6c031e-3fa7-4422-81db-dfa9a0fed5e3/dynamic-ruby-maintenance-karla-hernandez-LrlyZzX6Sws-unsplash.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466WB5HJ236%2F20260519%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20260519T192348Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBMaCXVzLXdlc3QtMiJHMEUCIE77nY17f5WlnLpJMLiJqLdfWRbW7Mf8DSvkO8QWw35WAiEAqjbrCcsLI50UwzEJEkaYmnl%2FlcJO1WNZmDx8ZKtZxYkqiAQI3P%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDB2exVlQt7Rt8MkEiSrcAyyw6f4xCsFP66Ob9BMD6%2BD%2FjOxRQqC8HI%2BBRyn6FjRxdFV%2BGh7hC91rB2C2roPopYqpeNcNGZn4yJcZKA947Eprj91AM2tuq4ktpzQVmzzKJ%2B%2Balo2ZZMDy%2Fh9%2FDE055m%2BJkoYqQbyWdj83Vf3OsPwQTuoD4ArL412BYuQ9QEL4nOy%2BUdVSfFUq0AiJDzxXljgi56NWYScz2VylNPAoRfAEHiwLzn1M21l7cHKO28fZLN%2FGS%2FDpZqZehSak6RWhLuch%2Bb1%2B2l5DUTnGMRABhy0qOPaBkqx2LDlg3CmptIC25TVjgdvh%2BTECVqsrsKAOPULAMGwNSqaldWLi4lf
![](https://prod-files-secure.s3.us-west-2.amazonaws.com/871f1661-80b8-4d0c-ac3b-2adfc6ff4c66/7f6c031e-3fa7-4422-81db-dfa9a0fed5e3/dynamic-ruby-maintenance-karla-hernandez-LrlyZzX6Sws-unsplash.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466WB5HJ236%2F20260519%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20260519T192348Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEBMaCXVzLXdlc3QtMiJHMEUCIE77nY17f5WlnLpJMLiJqLdfWRbW7Mf8DSvkO8QWw35WAiEAqjbrCcsLI50UwzEJEkaYmnl%2FlcJO1WNZmDx8ZKtZxYkqiAQI3P%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARAAGgw2Mzc0MjMxODM4MDUiDB2exVlQt7Rt8MkEiSrcAyyw6f4xCsFP66Ob9BMD6%2BD%2FjOxRQqC8HI%2BBRyn6FjRxdFV%2BGh7hC91rB2C2roPopYqpeNcNGZn4yJcZKA947Eprj91AM2tuq4ktpzQVmzzKJ%2B%2Balo2ZZMDy%2Fh9%2FDE055m%2BJkoYqQbyWdj83Vf3OsPwQTuoD4ArL412BYuQ9QEL4nOy%2BUdVSfFUq0AiJDzxXljgi56NWYScz2VylNPAoRfAEHiwLzn1M21l7cHKO28fZLN%2FGS%2FDpZqZehSak6RWhLuch%2Bb1%2B2l5DUTnGMRABhy0qOPaBkqx2LDlg3CmptIC25TVjgdvh%2BTECVqsrsKAOPULAMGwNSqaldWLi4lf5SeD%2BP5PGeNIVFsyNvkAR7t%2BbWaIA9aKJPovIG5LYITDq1r6AYiRR4%2BdZQsqOnk57gP4pao5ibwIpyDDPDsODgCUgFGWhLDMvVwDDkTj8b21GpD2OZyT6ko97lku55mW59BDGT9K244LWYgiXzq0IF6v4uE4Oyciphuu8TBWTftickSCX%2B5z1EtJKFnk7HcN9SVM%2BUNvJoRkSFiYGeJY9X8brNAGkrbwLwYji8OrAp13dR4XNhC834hKGklXUI7EDB1Kwscdzsp1x7NuHVnkEles8HTcPcBtMqvUWpnwGMMjastAGOqUBR0wX%2FpJFxQlECMn4tsEdTQVutrD6My98vgScpknx8fm%2BHhiekC5Uu0Zfl%2F6zS4kNwu1YNwm4PBGE21sZeN3%2BuZ60NJ%2BY6aEb7FbOztxVa%2B7y4L2XEHCRS6uGx2J2go8UKTmdFK%2FxvCb%2FIEinoL1cAV%2BKIReITKT64uuvXbJFSQZa%2FRZpHug7yzQolAI3AomP4AMqiCu8a939t1q4Da6AQksEjmeU&X-Amz-Signature=20569e6a1c0678c72cfceb0390282dd10ac8e81e4859581a328909eae5f86a0d&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) Ruby makes it easy to write dynamic code, and Rails amplifies this with ActiveSupport conveniences like constantize and classify. When you discover these capabilities, it feels empowering, like you're writing less code that does more. But there's a hidden cost to elegant abstractions in application code, especially on projects that will be maintained by multiple developers over many years. This post explores some code from a project I was maintaining, where a dynamic pattern made the codebase harder to understand. Class names and other details have been changed so I can share them publicly, but the patterns and trade-offs illustrate what I encountered. ## Where Are the Callers? I was investigating a Sidekiq job that accepts a class_name as an argument: ```plain text class DataSyncer include Sidekiq::Worker def perform(class_name, id, options = {}) model = class_name.constantize.find(id) case class_name.constantize.model_name.singular.to_sym when :product # sync product data... when :article # sync article data... else raise ArgumentError.new("#{ self.class.name } does not support class_name: #{ class_name }") end end end ``` The job handles two different model types: Product and Article. But when I searched the codebase for references to DataSyncer, I only found one explicit caller: ```plain text # app/models/product.rb class Product < ApplicationRecord has_many :line_items has_many :orders, through: :line_items after_update_commit do if saved_change_to_tags? DataSyncer.perform_async(self.class.name, id) end end end ``` This raised an immediate question: Why does DataSyncer have a :article handler when only Product appears to call it? My first instinct was that this might be dead code left over from a refactoring. I was tempted to remove the unused :article branch. But something made me pause. The code seemed too intentional to be simply forgotten. There had to be a reason for that flexibility. ## Dynamic Job Dispatcher After some deeper investigation (with a little help from my AI assistant scanning the codebase), I discovered the missing piece. The Article model had a callback that didn't directly reference DataSyncer, but was invoking it indirectly: ```plain text # app/models/article.rb class Article < ApplicationRecord after_create_commit do BackgroundJobDispatcher.new(self.class.name, id).execute end end ``` This led me to the following service class: ```plain text # app/services/background_job_dispatcher.rb class BackgroundJobDispatcher def initialize(class_name, id) @class_name = class_name @id = id end def execute raise NotImplementedError unless dispatchable? sync_attributes.each do |attribute| job_class_for(attribute).perform_async(class_name, id) end end private def dispatchable? class_name == "Article" end def job_class_for(attribute) [attribute, :syncer].join("_").classify.safe_constantize end def sync_attributes %i[metadata data].freeze end attr_reader :class_name, :id end ``` How it works: The BackgroundJobDispatcher uses string manipulation to dynamically resolve job class names: 1. For each attribute in sync_attributes (:metadata and :data) 2. It constructs a string by joining the attribute with :syncer: "metadata_syncer", "data_syncer" 3. ActiveSupport's safe_constantize looks up these classes as constants, and .perform_async is called on them So when an Article is created, it automatically triggers MetadataSyncer and DataSyncer jobs without the Article model ever explicitly naming those classes. From a design perspective, this pattern has some appealing qualities: ## The Cost of Flexibility While this code works perfectly from a technical standpoint, it creates significant friction for long-term maintenance. When I searched for DataSyncer in the codebase, the dynamic dispatch through BackgroundJobDispatcher didn't show up. The connection between Article and DataSyncer was invisible to standard search tools and static analysis. This made it difficult to: Every future developer who encounters this code needs to: These activities require additional mental energy that compounds over time as more developers join the project. ### Limited Reuse In this codebase, BackgroundJobDispatcher is only used by the Article model, which only had two sync operations. The flexibility to handle multiple operations and models exists, but it's never exercised. The abstraction was built for a level of generality that wasn't actually needed. An argument could be made that as long as AI coding assistants can trace these relationships, it doesn't matter if the code is hard for humans to follow. But I'm not fully convinced. Code should be understandable even without machine assistance, because AI service providers can have outages, hit capacity limits, and aren't always reliable when you need them most. And even when they're available, teams may not trust their conclusions for critical debugging. Maintainability still depends on people being able to reason about the system. ## A Simpler Alternative The same functionality could be achieved with two explicit lines in the Article model: ```plain text # app/models/article.rb class Article < ApplicationRecord after_create_commit do MetadataSyncer.perform_async(self.class.name, id) DataSyncer.perform_async(self.class.name, id) end end ``` This version: Yes, if you need to add a third syncer, you add a third line. To me, the increased clarity is well worth the extra line. ## When Dynamic Patterns Make Sense To be clear, there's nothing inherently wrong with the dynamic job dispatcher pattern. It could be useful as: - A documented library or gem that handles job dispatching across multiple projects - A framework-level abstraction where the benefits of the pattern justify the cognitive overhead In those contexts, the investment in understanding the abstraction pays dividends because it's used widely and consistently. But in application code, where the primary goal is to model your specific business logic, explicit is often better than dynamic. ## Lessons for Long-Term Projects This experience reinforced a few principles for me: Optimize for reading, not writing. Code is read far more often than it's written. The few extra seconds it takes to write explicit job calls is dwarfed by the minutes (or hours) future developers will spend understanding dynamic code. Abstractions should pay for themselves. Before creating an abstraction, ask: "Will this be reused enough to justify the cognitive overhead?" If the answer is unclear, err on the side of explicitness. Consider the maintenance context. On projects that will live for years with multiple developers coming and going, predictable patterns are more valuable than elegant ones. The boring code that future on-call you can understand at 2am is better than the clever code that present-you is proud of. What feels like a productivity gain when writing code can become a maintenance burden when others inherit it. Ruby (and Rails) give us powerful tools for abstraction, but on long-lived projects, sometimes the best code is the code that solves the current problem as simply as possible, and doesn't try to be too flexible. I nearly removed a working feature because I couldn't trace its callers through a dynamic abstraction. The next time you're tempted to write one, ask yourself whether the elegance is worth that risk.

Visibility

Visible to everyone

Reading Status

Related Bookmarks

My Note


Saved!

Annotations

Export as Markdown
+ Annotate selection

Add Annotation