Previously in this series:
The Hook: The One-Line Change That Touched Five Files
At what point did I realize my architecture was failing?
It wasn't a production crash. It was a request to add a single permission check to a nested form field.
"Show the wholesale_price field only if the user has enterprise_view permission."
Simple, right? Not in my app. Because to get that permission check into that nested field, I had to:
- Update the
Handlerconstructor to takecurrent_user. - Pass it to the
Formconstructor. - Pass it to the
Collectionconstructor. - Pass it to the
Subformconstructor. - Pass it to the
Elementinstance.
I was manually dragging context through five layers of objects just so a tiny subform could check a single flag.
If I had to pass tenant_id or current_user into one more constructor, I was ready to quit web dev and become a hardware engineer. At least in hardware, wires don't have to be manually passed through every intermediate component.
The Constructor Hell Reality: "Pure" DI is a Trap
I was committed to "pure" dependency injection. I'd read the books. I knew "globals are evil." I knew "helpers are hidden state."
The logic: If an object needs something, it should be passed in the constructor. No exceptions.
The result:
# The "Pure" way to despair
form = ProductForm.new(
object: product,
current_user: current_user,
account: account,
feature_flags: feature_flags,
pricing_tier: pricing_tier,
policy: policy
)
# Deep in the subform...
subform = VariantForm.new(
object: variant,
current_user: current_user, # PASSING
account: account, # PASSING
feature_flags: feature_flags, # PASSING
pricing_tier: pricing_tier # PASSING
)
Every intermediate layer became a context shuttle. These objects didn't use the context; they just carried it for their children.
It was fragile. It was noisy. And it made refactoring impossible—adding one piece of context meant changing five files.
The paradox: In my attempt to make dependencies "explicit," I made the system so rigid it was unmaintainable.
The False Choice: Sledgehammer vs. Spaghetti
I spent years thinking my only options were:
- Constructor Hell: Passing everything down manually (The "Pure" Way).
-
Dirty Shortcuts: Using
Current.useror global helpers (The "Lazy" Way).
I hated both.
- Option 1 made my code unreadable and rigid.
- Option 2 made my code untestable and mysterious.
I was looking for a third way: stop treating context like a constructor argument, and start treating your system like a tree.
Build an explicit ownership tree, and make one rule non-negotiable: every object knows its owner. Then children can request context from above instead of you injecting it through every layer.
This isn't just a theory. The libraries in this series implement this pattern directly:
-
SteelWheel builds the tree (handler owns the request and creates the form with
owner: self) -
ActionForm uses
owner+owner_*to bubble context up the chain -
EasyParams uses the same
owner_*idea inside parameter objects, so validations can ask the handler (or other owners) for context
The Browser Epiphany: Why Does JS Bubble?
Think about how events work in the browser.
You click a button deep in the DOM. You don't have to pass an onClick handler from the <html> tag down through every single div to reach that button.
Instead, the event bubbles up.
The button "screams" that it was clicked, and it bubbles up the hierarchy until some parent element decides to handle it.
The insight: Why do we love the way events bubble up the DOM, yet I spent a decade forcing my Ruby objects to be strictly, painfully top-down?
Why can't my nested form element "scream" its request for context up the hierarchy?
Stop Passing Context. Start Requesting It
I decided to stop passing context and start letting components request it.
Instead of DI-by-constructor, I built a tree and gave every node an owner. Once every object knows who owns it, context can bubble upward on demand.
I used method_missing to implement an ownership chain. If an element doesn't know the answer, it asks its owner. If the owner doesn't know, it asks its owner.
# Instead of passing everything...
form = ProductForm.new(object: product, owner: handler)
# The element just screams its request
element :wholesale_price do
def render?
owner_can_view_wholesale? # "Hey, someone up there, can I do this?"
end
end
The mechanics:
- Element calls
owner_can_view_wholesale?. -
method_missingintercepts the call because of theowner_prefix. - The element strips the prefix and tries
can_view_wholesale?on itsowner(the subform). - Subform doesn't have it? It repeats the same delegation to its owner (the form).
- Form doesn't have it? It repeats again to its owner (the handler).
- Handler has
can_view_wholesale?. Answer returned.
Context bubbles up, not down.
The Tree Duality: Composition Goes Both Ways
This works because your object graph is already a tree.
A Handler contains a Form. A Form contains Elements. That’s normal composition — objects containing other objects.
But here’s the missing mental model: trees support delegation in both directions:
- Downward (Parent → Children): normal orchestration (handler calls form, form iterates elements)
- Upward (Children → Parent): context bubbling (elements ask owners for context)
And the deeper your composition is, the more this pays off.
If your object graph is only 1–2 levels deep, constructor DI feels fine. But once you have real-world nesting—forms inside forms, collections, subforms, elements inside elements—every new piece of context becomes a change to every intermediate constructor.
Owner delegation flips that scaling: deep trees are no longer a liability. They’re exactly what makes bubbling powerful.
We’re taught to think composition is one-way. Parent creates children, parent calls children. Done.
Owner delegation adds the other half: children can ask upward without you manually shuttling context through every intermediate constructor.
# Downward: Normal composition (parent → children)
class ProductHandler < ApplicationHandler
def form
@form ||= ProductForm.new(owner: self) # Handler → Form
end
def on_validation_success
form.elements.each(&:finalize) # Handler → Form → Elements
end
end
# Upward: Context bubbling (children → parent)
class PriceElement < ActionForm::Element
def render?
owner_can_view_pricing? # Element → Form → Handler
end
def currency
owner_current_currency # Element → Form → Handler
end
end
Same tree. Two directions. No constructor hell. No hidden globals.
Transparent Structures: Runtime Specialization Without the Pain
This is the name I use for the broader idea: Transparent Structures.
A structure is “transparent” when you can open any component class at runtime (form, subform, element, params schema) and specialize its behavior in a precise, local way—without rewriting the whole graph or threading new dependencies through every constructor.
Owner delegation is one part of it: it makes context flow up through the structure when a deep leaf needs something.
The owner_ Signal: Magic With Intent
I chose the explicit owner_ prefix for a reason.
I know it’s a "magic" trick. I know some Rubyists hate method_missing. But I needed it to be explicit.
I didn't want mysterious method calls where you don't know where the data comes from. I wanted any developer—including my future self—to see owner_ and immediately understand: "I am delegating this request up the ownership chain."
It’s a signal: "I don't have this context, but my owner does."
It turns a hidden dependency into an explicit request.
The Sharp Edges (and Why It's Still Worth It)
Yes, method_missing is sharp. The trick is to use it with guardrails:
-
Make delegation opt-in: only delegate when the call starts with
owner_. -
Implement
respond_to_missing?: so introspection and tooling don't lie. - Keep the API narrow: you’re not delegating “everything”, you’re delegating explicit requests for context.
-
Fail normally: if the chain can’t answer, let Ruby raise
NoMethodError(or add a custom error message later if you want).
This isn’t “mysterious magic.” It’s a deliberate mechanism for one specific problem: context bubbling.
And it’s not hypothetical — the core idea in ActionForm::Composition is literally:
def method_missing(name, *attrs, **kwargs, &block)
return super unless name.to_s.start_with?("owner_")
owner_method = name.to_s.sub("owner_", "").to_sym
return super unless (handler = owners_chain.detect { |o| o.respond_to?(owner_method) })
handler.public_send(owner_method, *attrs, **kwargs, &block)
end
The Chain of Responsibility: A Multi-Level Power Grid
Passing context through constructors is like manually wiring every single lightbulb in your house directly back to the city's power plant. If you want to add one lamp in the attic, you have to rip open every floorboard to run the wire.
Owner Delegation is a modern electrical grid.
You just plug the lamp into the nearest wall socket (the owner_ call), and the house takes care of "bubbling" that request up to the main breaker for you.
The hierarchy:
- Element: "I need the current user's role."
- Subform: "Don't know, asking the form."
- Form: "Don't know, asking the handler."
- Handler: "Here it is."
Intermediate layers (Subform, Form) don't need to know anything about the current user's role. They just need to know who their owner is.
This is the Chain of Responsibility pattern, applied to composition.
The DX Shift: Vibe Coding Without the Weight
I’m now "vibe coding" without the weight of a thousand manual injections.
When a requirement changes, I don't dread it. I don't have to touch five files to add one permission check. I just call owner_something? and add the method to the handler.
Can I finally admit it? The "Rails Way" stopped being the most productive path for my complex domains a long time ago.
We’ve been taught to fear "magic" and "metaprogramming." But I’ve learned that the magic of structured delegation is far less dangerous than the boilerplate of manual injection.
One keeps your code clean. The other keeps you in the office until 11 PM on a Friday.
Resources
-
Series
- Part 1: The Service Object Graveyard: Why Your POROs Failed and Handlers Won
- Part 2: Constructor Hell and the "Owner" Bubble (this post)
-
Libraries / examples
-
SteelWheel - Handler framework (builds the tree: handler owns the request + creates form with
owner: self) -
ActionForm - Form DSL (implements
owner+owner_*bubbling viaActionForm::Composition) -
EasyParams - Type-safe parameters (implements
owner+owner_*bubbling viaEasyParams::Composition) - Real App - Production example
-
SteelWheel - Handler framework (builds the tree: handler owns the request + creates form with
Next time you find yourself passing a variable through three constructors just to reach a child element:
Ask yourself: Are you building a house, or are you just running miles of redundant wire?
Remember: trees aren't just for pushing commands down. They're for pulling context up.
Maybe it's time to install the grid.
Top comments (0)