<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: andriy-baran</title>
    <description>The latest articles on DEV Community by andriy-baran (@andriybaran).</description>
    <link>https://dev.to/andriybaran</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F130782%2Fac5f02a4-ddec-41be-8150-7c8d1db0dc8f.jpeg</url>
      <title>DEV Community: andriy-baran</title>
      <link>https://dev.to/andriybaran</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/andriybaran"/>
    <language>en</language>
    <item>
      <title>FormBuilder Proxy vs Template Method</title>
      <dc:creator>andriy-baran</dc:creator>
      <pubDate>Wed, 14 Jan 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/andriybaran/formbuilder-proxy-vs-template-method-2blp</link>
      <guid>https://dev.to/andriybaran/formbuilder-proxy-vs-template-method-2blp</guid>
      <description>&lt;p&gt;&lt;strong&gt;Previously in this series:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 1&lt;/strong&gt;: &lt;a href="https://dev.to/andriybaran/the-service-object-graveyard-why-your-poros-failed-and-handlers-won-2a8j"&gt;The Service Object Graveyard: Why Your POROs Failed and Handlers Won&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 2&lt;/strong&gt;: &lt;a href="https://dev.to/andriybaran/constructor-hell-replacing-dependency-injection-with-chain-of-responsibility-in-ruby-5epk"&gt;Constructor Hell and the "Owner" Bubble: Stealing from Browser JS&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Pattern Conflict
&lt;/h2&gt;

&lt;p&gt;Take the paradox: Rails gives us &lt;code&gt;ActionView::Helpers::FormBuilder&lt;/code&gt; - a thing literally named "Builder" - and yet complex forms still collapse into partial soup, param drift, and "where do errors render?" archaeology.&lt;/p&gt;

&lt;p&gt;So let's pressure-test the premise.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rails FormBuilder&lt;/strong&gt; is a &lt;em&gt;Proxy&lt;/em&gt;: a yielded object that forwards calls to view helpers (a toolbox you operate).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ActionForm (with Phlex)&lt;/strong&gt; is &lt;em&gt;Template Method&lt;/em&gt;: a rendering algorithm with named steps (an assembly line you override).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference is not "syntax." It's &lt;strong&gt;ownership of the rendering process&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Rails even says the quiet part out loud: FormBuilder "can be thought of as serving as a proxy for the methods in the FormHelper module" (&lt;a href="https://api.rubyonrails.org/v8.0/classes/ActionView/Helpers/FormBuilder.html" rel="noopener noreferrer"&gt;Rails 8 FormBuilder docs&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  1) The Pattern Conflict: Proxy vs Skeleton
&lt;/h2&gt;

&lt;p&gt;Why did we spend a decade treating the form as a &lt;strong&gt;proxy&lt;/strong&gt; for a model object instead of a &lt;strong&gt;skeleton of a rendering algorithm&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;Because the ERB world is document-first.&lt;/p&gt;

&lt;p&gt;You write a template; Rails yields a toolbox into it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt; &lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="vi"&gt;@person&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkbox&lt;/span&gt; &lt;span class="ss"&gt;:admin&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The template owns sequencing, composition, and integration. The builder helps you emit tags, but it doesn't define the &lt;em&gt;algorithm&lt;/em&gt; of "how a form is rendered."&lt;/p&gt;

&lt;h2&gt;
  
  
  2) The Source of Truth: Who Owns the Contract?
&lt;/h2&gt;

&lt;p&gt;If a Rails &lt;code&gt;FormBuilder&lt;/code&gt; is a proxy for helper methods, who owns the "data contract"?&lt;/p&gt;

&lt;p&gt;In practice it gets split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model&lt;/strong&gt; owns validations (sometimes).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controller&lt;/strong&gt; owns permitted params (always, if you use strong params).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;View&lt;/strong&gt; owns what fields exist (implicitly, by emitting tags).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's a three-body problem. When complex domains enter, the contract becomes a moving target: the UI can ask for fields the controller doesn't permit; the controller can permit keys the UI doesn't render; the model can validate attributes the UI never sends.&lt;/p&gt;

&lt;h2&gt;
  
  
  3) The Algorithm vs the Toolbox
&lt;/h2&gt;

&lt;p&gt;Why is a rigid rendering flow more resilient than yielding a toolbox?&lt;/p&gt;

&lt;p&gt;Because "toolbox" means &lt;strong&gt;every view is forced to re-invent ordering&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;where labels render&lt;/li&gt;
&lt;li&gt;where errors render&lt;/li&gt;
&lt;li&gt;how wrappers are structured&lt;/li&gt;
&lt;li&gt;what happens for nested collections&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With &lt;strong&gt;Template Method&lt;/strong&gt;, the base class owns the skeleton:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;view_template&lt;/span&gt;
  &lt;span class="n"&gt;render_form&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;render_elements&lt;/span&gt;
    &lt;span class="n"&gt;render_submit&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structure is an algorithm. And the steps have names.&lt;/p&gt;

&lt;p&gt;This is what Phlex enables: a view is not a string with holes - it is a Ruby object with a &lt;code&gt;view_template&lt;/code&gt; entrypoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  4) Surgical Overrides: Override a Step, Not a Document
&lt;/h2&gt;

&lt;p&gt;How does Template Method let you override a single step—like &lt;code&gt;render_label&lt;/code&gt; - without subclassing a FormBuilder and juggling glue like &lt;code&gt;objectify_options&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Rails' extension story is: subclass &lt;code&gt;FormBuilder&lt;/code&gt;, add helper methods, and be careful to call &lt;code&gt;objectify_options&lt;/code&gt; so the model binding isn't silently severed (&lt;a href="https://api.rubyonrails.org/v8.0/classes/ActionView/Helpers/FormBuilder.html" rel="noopener noreferrer"&gt;Rails 8 FormBuilder docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Template Method flips the problem: you don't add more tools; you override the algorithm.&lt;/p&gt;

&lt;p&gt;Example: suppress all labels (or change their wrapper) without touching every field call site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NoLabelForm&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ProductForm&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# intentionally empty&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The form stays a form. The rendering skeleton stays intact. Only one step changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  5) The "Params Drift" Paradox
&lt;/h2&gt;

&lt;p&gt;If FormBuilder is associated with a model, why is it so easy for parameters to drift away from the view definition?&lt;/p&gt;

&lt;p&gt;Because "associated" is not "authoritative."&lt;/p&gt;

&lt;p&gt;FormBuilder helps generate field names for an object. It does not produce the contract the controller permits. The contract is still written elsewhere, by hand, often long after the UI evolved.&lt;/p&gt;

&lt;p&gt;ActionForm takes a different stance: &lt;strong&gt;the contract is derived from the form's declared elements&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's the key inversion: instead of the UI being a projection of controller params, params become a projection of the UI's declared structure. (And because elements can be conditional—&lt;code&gt;render?&lt;/code&gt;—it can generate a schema from the &lt;em&gt;rendered&lt;/em&gt; structure when needed.)&lt;/p&gt;

&lt;h3&gt;
  
  
  The Form Elements DSL
&lt;/h3&gt;

&lt;p&gt;How do you declare elements? ActionForm provides a DSL that defines both the UI structure and the data contract in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductForm&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionForm&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="ss"&gt;type: :text&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="ss"&gt;type: :string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="ss"&gt;:price&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="ss"&gt;type: :number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;step: &lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="ss"&gt;type: :decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;numericality: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;greater_than: &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="ss"&gt;:description&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="ss"&gt;type: :textarea&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;rows: &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="ss"&gt;type: :string&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The DSL does three things at once:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Declares the input&lt;/strong&gt; (&lt;code&gt;input&lt;/code&gt;) - what HTML attributes and type the field needs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Declares the output&lt;/strong&gt; (&lt;code&gt;output&lt;/code&gt;) - what type and validations the parameter contract expects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creates the element class&lt;/strong&gt; - a Ruby object that can answer questions like &lt;code&gt;render?&lt;/code&gt;, &lt;code&gt;readonly?&lt;/code&gt;, &lt;code&gt;disabled?&lt;/code&gt;, or &lt;code&gt;value&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is why "drift" becomes impossible: the form definition &lt;em&gt;is&lt;/em&gt; the contract. The controller doesn't manually permit fields—it receives an &lt;code&gt;EasyParams&lt;/code&gt; object generated from the form's &lt;code&gt;output&lt;/code&gt; declarations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Element Methods: Behavior as Code
&lt;/h3&gt;

&lt;p&gt;Elements aren't just data structures—they're Ruby objects with methods you can override. This is where Template Method shines: you override behavior, not markup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;render?&lt;/code&gt;&lt;/strong&gt; - Controls whether the element should be rendered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="ss"&gt;:admin_field&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="ss"&gt;type: :text&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render?&lt;/span&gt;
    &lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;admin?&lt;/span&gt;  &lt;span class="c1"&gt;# Only render for admin users&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;readonly?&lt;/code&gt;&lt;/strong&gt; - Controls whether the element is readonly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="ss"&gt;type: :email&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;readonly?&lt;/span&gt;
    &lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verified?&lt;/span&gt;  &lt;span class="c1"&gt;# Readonly if email is verified&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;disabled?&lt;/code&gt;&lt;/strong&gt; - Controls whether the element is disabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="ss"&gt;:username&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="ss"&gt;type: :text&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;disabled?&lt;/span&gt;
    &lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;persisted?&lt;/span&gt;  &lt;span class="c1"&gt;# Disable for existing records&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;detached?&lt;/code&gt;&lt;/strong&gt; - Indicates if the element is detached from the object (uses static values):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="ss"&gt;:static_field&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="ss"&gt;type: :text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="s2"&gt;"Static Value"&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;detached?&lt;/span&gt;
    &lt;span class="kp"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;# This element doesn't bind to object values&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/strong&gt; - Access element metadata for conditional rendering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="ss"&gt;:priority_field&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="ss"&gt;type: :text&lt;/span&gt;
  &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="ss"&gt;priority: &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;section: &lt;/span&gt;&lt;span class="s2"&gt;"important"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# In your rendering algorithm:&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;render_label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;render_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;render_inline_errors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:errors&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tags&lt;/code&gt; hash automatically includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;:input&lt;/code&gt; - The input type (e.g., &lt;code&gt;:text&lt;/code&gt;, &lt;code&gt;:email&lt;/code&gt;, &lt;code&gt;:select&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;:output&lt;/code&gt; - The output type (e.g., &lt;code&gt;:string&lt;/code&gt;, &lt;code&gt;:integer&lt;/code&gt;, &lt;code&gt;:bool&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;:options&lt;/code&gt; - &lt;code&gt;true&lt;/code&gt; if the element has options (for selects/radios)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;:errors&lt;/code&gt; - &lt;code&gt;true&lt;/code&gt;/&lt;code&gt;false&lt;/code&gt; based on validation state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These methods integrate with the rendering algorithm: &lt;code&gt;render_elements&lt;/code&gt; can filter with &lt;code&gt;select(&amp;amp;:render?)&lt;/code&gt;, and &lt;code&gt;render_input&lt;/code&gt; can check &lt;code&gt;readonly?&lt;/code&gt; to add the &lt;code&gt;readonly&lt;/code&gt; attribute. The element becomes a first-class participant in the rendering process, not just a data bag.&lt;/p&gt;

&lt;h2&gt;
  
  
  6) Composition vs Helpers
&lt;/h2&gt;

&lt;p&gt;Why did we favor helpers for so long when Phlex-based composition lets UI elements be Ruby objects?&lt;/p&gt;

&lt;p&gt;Because templates make composition look like copy/paste:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;partials&lt;/li&gt;
&lt;li&gt;locals&lt;/li&gt;
&lt;li&gt;&lt;code&gt;content_for&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;"just one more &lt;code&gt;if&lt;/code&gt;"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Composition (Phlex) makes it look like programming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;objects&lt;/li&gt;
&lt;li&gt;methods&lt;/li&gt;
&lt;li&gt;inheritance&lt;/li&gt;
&lt;li&gt;tests against behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important shift: UI parts can have &lt;strong&gt;state&lt;/strong&gt;, &lt;strong&gt;methods&lt;/strong&gt;, and &lt;strong&gt;boundaries&lt;/strong&gt;. They can answer questions like "should I render?" without hiding that logic in template conditionals.&lt;/p&gt;

&lt;h2&gt;
  
  
  7) Handling Nesting: From Manual Chore to Structured Algorithm
&lt;/h2&gt;

&lt;p&gt;Why is &lt;code&gt;accepts_nested_attributes_for&lt;/code&gt; + templates so painful?&lt;/p&gt;

&lt;p&gt;Because the UI needs a repeatable mechanism for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rendering each child&lt;/li&gt;
&lt;li&gt;rendering a "new child" template&lt;/li&gt;
&lt;li&gt;maintaining correct naming/indexing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a toolbox world, you build that by convention and JS glue.&lt;/p&gt;

&lt;p&gt;In an algorithm world, the base class can define the lifecycle: build instances, render instances, and even render a "NEW_RECORD" template consistently. Nesting becomes a first-class step, not a copy/pasted ritual.&lt;/p&gt;

&lt;h2&gt;
  
  
  8) Error Integration: Errors as a Built-In Phase
&lt;/h2&gt;

&lt;p&gt;In proxy-based systems, error handling is typically "extra."&lt;/p&gt;

&lt;p&gt;You decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;where to show inline errors&lt;/li&gt;
&lt;li&gt;how to style them&lt;/li&gt;
&lt;li&gt;how to aggregate them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Template Method makes errors a phase in the algorithm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;render_label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;render_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;render_inline_errors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:errors&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's not just convenience. It's enforcement: error rendering is now a &lt;em&gt;default behavior of the skeleton&lt;/em&gt;, not an optional add-on scattered across templates.&lt;/p&gt;

&lt;h2&gt;
  
  
  9) The 6-Year Verdict
&lt;/h2&gt;

&lt;p&gt;Is the FormBuilder proxy "enough" for simple CRUD? Yes.&lt;/p&gt;

&lt;p&gt;But complex domains don't need more tools. They need a &lt;strong&gt;formal rendering algorithm&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stable steps&lt;/li&gt;
&lt;li&gt;named override points&lt;/li&gt;
&lt;li&gt;consistent nesting and error phases&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The "Industrial Assembly Line" Metaphor
&lt;/h3&gt;

&lt;p&gt;Rails FormBuilder is a high-end &lt;strong&gt;toolbox&lt;/strong&gt; (Proxy). It gives you the wrenches, but you decide the order of assembly every time you enter the garage.&lt;/p&gt;

&lt;p&gt;ActionForm (with Phlex) is the &lt;strong&gt;assembly line&lt;/strong&gt; (Template Method). The skeleton of the process is built into the floor. You can swap specific parts (override steps), but the line enforces sequencing: chassis before engine, inspection before shipping.&lt;/p&gt;

&lt;p&gt;That's the difference between "UI as emitted strings" and "UI as an algorithm."&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Libraries / examples&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/andriy-baran/steel_wheel" rel="noopener noreferrer"&gt;SteelWheel&lt;/a&gt; - Handler framework&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/andriy-baran/action_form" rel="noopener noreferrer"&gt;ActionForm&lt;/a&gt; - Form DSL&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/andriy-baran/easy_params" rel="noopener noreferrer"&gt;EasyParams&lt;/a&gt; - Type-safe parameters&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/andriy-baran/sips" rel="noopener noreferrer"&gt;Real App&lt;/a&gt; - Production example&lt;/li&gt;
&lt;li&gt;&lt;a href="https://api.rubyonrails.org/v8.0/classes/ActionView/Helpers/FormBuilder.html" rel="noopener noreferrer"&gt;Rails 8 FormBuilder docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Ask yourself:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Are you shipping forms, or are you rebuilding the assembly line every time you render a field?&lt;/p&gt;

&lt;p&gt;If the domain is complex, maybe the right move isn't "more helpers." Maybe it's &lt;strong&gt;ownership of the algorithm&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>phlex</category>
      <category>designpatterns</category>
    </item>
    <item>
      <title>Constructor Hell: Replacing Dependency Injection with Chain of Responsibility in Ruby</title>
      <dc:creator>andriy-baran</dc:creator>
      <pubDate>Fri, 09 Jan 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/andriybaran/constructor-hell-replacing-dependency-injection-with-chain-of-responsibility-in-ruby-5epk</link>
      <guid>https://dev.to/andriybaran/constructor-hell-replacing-dependency-injection-with-chain-of-responsibility-in-ruby-5epk</guid>
      <description>&lt;p&gt;&lt;strong&gt;Previously in this series:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 1&lt;/strong&gt;: &lt;a href="https://dev.to/andriybaran/the-service-object-graveyard-why-your-poros-failed-and-handlers-won-2a8j"&gt;The Service Object Graveyard: Why Your POROs Failed and Handlers Won&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Hook: The One-Line Change That Touched Five Files
&lt;/h2&gt;

&lt;p&gt;At what point did I realize my architecture was failing?&lt;/p&gt;

&lt;p&gt;It wasn't a production crash. It was a request to add a single permission check to a nested form field.&lt;/p&gt;

&lt;p&gt;"Show the &lt;code&gt;wholesale_price&lt;/code&gt; field only if the user has &lt;code&gt;enterprise_view&lt;/code&gt; permission."&lt;/p&gt;

&lt;p&gt;Simple, right? Not in my app. Because to get that permission check into that nested field, I had to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Update the &lt;code&gt;Handler&lt;/code&gt; constructor to take &lt;code&gt;current_user&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Pass it to the &lt;code&gt;Form&lt;/code&gt; constructor.&lt;/li&gt;
&lt;li&gt;Pass it to the &lt;code&gt;Collection&lt;/code&gt; constructor.&lt;/li&gt;
&lt;li&gt;Pass it to the &lt;code&gt;Subform&lt;/code&gt; constructor.&lt;/li&gt;
&lt;li&gt;Pass it to the &lt;code&gt;Element&lt;/code&gt; instance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;I was manually dragging context through five layers of objects just so a tiny subform could check a single flag.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If I had to pass &lt;code&gt;tenant_id&lt;/code&gt; or &lt;code&gt;current_user&lt;/code&gt; 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Constructor Hell Reality: "Pure" DI is a Trap
&lt;/h2&gt;

&lt;p&gt;I was committed to "pure" dependency injection. I'd read the books. I knew "globals are evil." I knew "helpers are hidden state."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The logic:&lt;/strong&gt; If an object needs something, it should be passed in the constructor. No exceptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The "Pure" way to despair&lt;/span&gt;
&lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ProductForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;object: &lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;current_user: &lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;account: &lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;feature_flags: &lt;/span&gt;&lt;span class="n"&gt;feature_flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;pricing_tier: &lt;/span&gt;&lt;span class="n"&gt;pricing_tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;policy: &lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Deep in the subform...&lt;/span&gt;
&lt;span class="n"&gt;subform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;VariantForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;object: &lt;/span&gt;&lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;current_user: &lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# PASSING&lt;/span&gt;
  &lt;span class="ss"&gt;account: &lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="c1"&gt;# PASSING&lt;/span&gt;
  &lt;span class="ss"&gt;feature_flags: &lt;/span&gt;&lt;span class="n"&gt;feature_flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# PASSING&lt;/span&gt;
  &lt;span class="ss"&gt;pricing_tier: &lt;/span&gt;&lt;span class="n"&gt;pricing_tier&lt;/span&gt;       &lt;span class="c1"&gt;# PASSING&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every intermediate layer became a &lt;strong&gt;context shuttle&lt;/strong&gt;. These objects didn't &lt;em&gt;use&lt;/em&gt; the context; they just &lt;em&gt;carried&lt;/em&gt; it for their children.&lt;/p&gt;

&lt;p&gt;It was fragile. It was noisy. And it made refactoring impossible—adding one piece of context meant changing five files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The paradox:&lt;/strong&gt; In my attempt to make dependencies "explicit," I made the system so rigid it was unmaintainable.&lt;/p&gt;




&lt;h2&gt;
  
  
  The False Choice: Sledgehammer vs. Spaghetti
&lt;/h2&gt;

&lt;p&gt;I spent years thinking my only options were:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Constructor Hell&lt;/strong&gt;: Passing everything down manually (The "Pure" Way).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dirty Shortcuts&lt;/strong&gt;: Using &lt;code&gt;Current.user&lt;/code&gt; or global helpers (The "Lazy" Way).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I hated both.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Option 1&lt;/strong&gt; made my code unreadable and rigid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Option 2&lt;/strong&gt; made my code untestable and mysterious.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I was looking for a third way: &lt;strong&gt;stop treating context like a constructor argument, and start treating your system like a tree.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Build an explicit ownership tree, and make one rule non-negotiable: &lt;strong&gt;every object knows its &lt;code&gt;owner&lt;/code&gt;.&lt;/strong&gt; Then children can request context from above instead of you injecting it through every layer.&lt;/p&gt;

&lt;p&gt;This isn't just a theory. The libraries in this series implement this pattern directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SteelWheel&lt;/strong&gt; builds the tree (handler owns the request and creates the form with &lt;code&gt;owner: self&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ActionForm&lt;/strong&gt; uses &lt;code&gt;owner&lt;/code&gt; + &lt;code&gt;owner_*&lt;/code&gt; to bubble context up the chain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EasyParams&lt;/strong&gt; uses the same &lt;code&gt;owner_*&lt;/code&gt; idea inside parameter objects, so validations can ask the handler (or other owners) for context&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Browser Epiphany: Why Does JS Bubble?
&lt;/h2&gt;

&lt;p&gt;Think about how events work in the browser.&lt;/p&gt;

&lt;p&gt;You click a button deep in the DOM. You don't have to pass an &lt;code&gt;onClick&lt;/code&gt; handler from the &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; tag down through every single &lt;code&gt;div&lt;/code&gt; to reach that button.&lt;/p&gt;

&lt;p&gt;Instead, the event &lt;strong&gt;bubbles up&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The button "screams" that it was clicked, and it bubbles up the hierarchy until some parent element decides to handle it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The insight:&lt;/strong&gt; 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?&lt;/p&gt;

&lt;p&gt;Why can't my nested form element "scream" its request for context up the hierarchy?&lt;/p&gt;




&lt;h2&gt;
  
  
  Stop Passing Context. Start Requesting It
&lt;/h2&gt;

&lt;p&gt;I decided to stop &lt;em&gt;passing&lt;/em&gt; context and start letting components &lt;em&gt;request&lt;/em&gt; it.&lt;/p&gt;

&lt;p&gt;Instead of DI-by-constructor, I built a tree and gave every node an &lt;code&gt;owner&lt;/code&gt;. Once every object knows who owns it, &lt;strong&gt;context can bubble upward&lt;/strong&gt; on demand.&lt;/p&gt;

&lt;p&gt;I used &lt;code&gt;method_missing&lt;/code&gt; 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 &lt;em&gt;its&lt;/em&gt; owner.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Instead of passing everything...&lt;/span&gt;
&lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ProductForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;object: &lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;owner: &lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# The element just screams its request&lt;/span&gt;
&lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="ss"&gt;:wholesale_price&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render?&lt;/span&gt;
    &lt;span class="n"&gt;owner_can_view_wholesale?&lt;/span&gt;  &lt;span class="c1"&gt;# "Hey, someone up there, can I do this?"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The mechanics:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Element calls &lt;code&gt;owner_can_view_wholesale?&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;method_missing&lt;/code&gt; intercepts the call because of the &lt;code&gt;owner_&lt;/code&gt; prefix.&lt;/li&gt;
&lt;li&gt;The element strips the prefix and tries &lt;code&gt;can_view_wholesale?&lt;/code&gt; on its &lt;code&gt;owner&lt;/code&gt; (the subform).&lt;/li&gt;
&lt;li&gt;Subform doesn't have it? It repeats the same delegation to &lt;em&gt;its&lt;/em&gt; owner (the form).&lt;/li&gt;
&lt;li&gt;Form doesn't have it? It repeats again to &lt;em&gt;its&lt;/em&gt; owner (the handler).&lt;/li&gt;
&lt;li&gt;Handler has &lt;code&gt;can_view_wholesale?&lt;/code&gt;. Answer returned.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Context bubbles up, not down.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Tree Duality: Composition Goes Both Ways
&lt;/h2&gt;

&lt;p&gt;This works because your object graph is already a tree.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;Handler&lt;/code&gt; contains a &lt;code&gt;Form&lt;/code&gt;. A &lt;code&gt;Form&lt;/code&gt; contains &lt;code&gt;Elements&lt;/code&gt;. That’s normal composition — objects containing other objects.&lt;/p&gt;

&lt;p&gt;But here’s the missing mental model: &lt;strong&gt;trees support delegation in both directions&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Downward (Parent → Children)&lt;/strong&gt;: normal orchestration (handler calls form, form iterates elements)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upward (Children → Parent)&lt;/strong&gt;: context bubbling (elements ask owners for context)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the deeper your composition is, the more this pays off.&lt;/p&gt;

&lt;p&gt;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—&lt;strong&gt;every new piece of context becomes a change to every intermediate constructor&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Owner delegation flips that scaling: deep trees are no longer a liability. They’re exactly what makes bubbling powerful.&lt;/p&gt;

&lt;p&gt;We’re taught to think composition is one-way. Parent creates children, parent calls children. Done.&lt;/p&gt;

&lt;p&gt;Owner delegation adds the other half: children can &lt;em&gt;ask upward&lt;/em&gt; without you manually shuttling context through every intermediate constructor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Downward: Normal composition (parent → children)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductHandler&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationHandler&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;form&lt;/span&gt;
    &lt;span class="vi"&gt;@form&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="no"&gt;ProductForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;owner: &lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Handler → Form&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_validation_success&lt;/span&gt;
    &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:finalize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Handler → Form → Elements&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Upward: Context bubbling (children → parent)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PriceElement&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionForm&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Element&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render?&lt;/span&gt;
    &lt;span class="n"&gt;owner_can_view_pricing?&lt;/span&gt;  &lt;span class="c1"&gt;# Element → Form → Handler&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;currency&lt;/span&gt;
    &lt;span class="n"&gt;owner_current_currency&lt;/span&gt;  &lt;span class="c1"&gt;# Element → Form → Handler&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Same tree. Two directions. No constructor hell. No hidden globals.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Transparent Structures: Runtime Specialization Without the Pain
&lt;/h2&gt;

&lt;p&gt;This is the name I use for the broader idea: &lt;strong&gt;Transparent Structures&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A structure is “transparent” when you can &lt;strong&gt;open any component class at runtime&lt;/strong&gt; (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.&lt;/p&gt;

&lt;p&gt;Owner delegation is one part of it: it makes context flow &lt;em&gt;up&lt;/em&gt; through the structure when a deep leaf needs something.&lt;/p&gt;




&lt;h2&gt;
  
  
  The &lt;code&gt;owner_&lt;/code&gt; Signal: Magic With Intent
&lt;/h2&gt;

&lt;p&gt;I chose the explicit &lt;code&gt;owner_&lt;/code&gt; prefix for a reason.&lt;/p&gt;

&lt;p&gt;I know it’s a "magic" trick. I know some Rubyists hate &lt;code&gt;method_missing&lt;/code&gt;. But I needed it to be &lt;strong&gt;explicit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;owner_&lt;/code&gt; and immediately understand: &lt;strong&gt;"I am delegating this request up the ownership chain."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s a signal: "I don't have this context, but my owner does."&lt;/p&gt;

&lt;p&gt;It turns a hidden dependency into an explicit request.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Sharp Edges (and Why It's Still Worth It)
&lt;/h2&gt;

&lt;p&gt;Yes, &lt;code&gt;method_missing&lt;/code&gt; is sharp. The trick is to use it with guardrails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Make delegation opt-in&lt;/strong&gt;: only delegate when the call starts with &lt;code&gt;owner_&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement &lt;code&gt;respond_to_missing?&lt;/code&gt;&lt;/strong&gt;: so introspection and tooling don't lie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the API narrow&lt;/strong&gt;: you’re not delegating “everything”, you’re delegating &lt;em&gt;explicit requests for context&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail normally&lt;/strong&gt;: if the chain can’t answer, let Ruby raise &lt;code&gt;NoMethodError&lt;/code&gt; (or add a custom error message later if you want).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn’t “mysterious magic.” It’s a deliberate mechanism for one specific problem: &lt;strong&gt;context bubbling&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And it’s not hypothetical — the core idea in &lt;code&gt;ActionForm::Composition&lt;/code&gt; is literally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;method_missing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_with?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"owner_"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;owner_method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"owner_"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_sym&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;owners_chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respond_to?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owner_method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;public_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owner_method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Chain of Responsibility: A Multi-Level Power Grid
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Owner Delegation is a modern electrical grid.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You just plug the lamp into the nearest wall socket (the &lt;code&gt;owner_&lt;/code&gt; call), and the house takes care of "bubbling" that request up to the main breaker for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hierarchy:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Element&lt;/strong&gt;: "I need the current user's role."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subform&lt;/strong&gt;: "Don't know, asking the form."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Form&lt;/strong&gt;: "Don't know, asking the handler."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handler&lt;/strong&gt;: "Here it is."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;Chain of Responsibility&lt;/strong&gt; pattern, applied to composition.&lt;/p&gt;




&lt;h2&gt;
  
  
  The DX Shift: Vibe Coding Without the Weight
&lt;/h2&gt;

&lt;p&gt;I’m now "vibe coding" without the weight of a thousand manual injections.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;owner_something?&lt;/code&gt; and add the method to the handler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I finally admit it?&lt;/strong&gt; The "Rails Way" stopped being the most productive path for my complex domains a long time ago.&lt;/p&gt;

&lt;p&gt;We’ve been taught to fear "magic" and "metaprogramming." But I’ve learned that the &lt;strong&gt;magic of structured delegation&lt;/strong&gt; is far less dangerous than the &lt;strong&gt;boilerplate of manual injection&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One keeps your code clean. The other keeps you in the office until 11 PM on a Friday.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 1&lt;/strong&gt;: &lt;a href="https://dev.to/andriybaran/the-service-object-graveyard-why-your-poros-failed-and-handlers-won-2a8j"&gt;The Service Object Graveyard: Why Your POROs Failed and Handlers Won&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 2&lt;/strong&gt;: Constructor Hell and the "Owner" Bubble (this post)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Libraries / examples&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/andriy-baran/steel_wheel" rel="noopener noreferrer"&gt;SteelWheel&lt;/a&gt; - Handler framework (builds the tree: handler owns the request + creates form with &lt;code&gt;owner: self&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/andriy-baran/action_form" rel="noopener noreferrer"&gt;ActionForm&lt;/a&gt; - Form DSL (implements &lt;code&gt;owner&lt;/code&gt; + &lt;code&gt;owner_*&lt;/code&gt; bubbling via &lt;code&gt;ActionForm::Composition&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/andriy-baran/easy_params" rel="noopener noreferrer"&gt;EasyParams&lt;/a&gt; - Type-safe parameters (implements &lt;code&gt;owner&lt;/code&gt; + &lt;code&gt;owner_*&lt;/code&gt; bubbling via &lt;code&gt;EasyParams::Composition&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/andriy-baran/sips" rel="noopener noreferrer"&gt;Real App&lt;/a&gt; - Production example&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Next time you find yourself passing a variable through three constructors just to reach a child element:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ask yourself: Are you building a house, or are you just running miles of redundant wire?&lt;/p&gt;

&lt;p&gt;Remember: trees aren't just for pushing commands down. They're for pulling context up.&lt;/p&gt;

&lt;p&gt;Maybe it's time to install the grid.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>designpatterns</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The Service Object Graveyard: Why Your POROs Failed and Handlers Won</title>
      <dc:creator>andriy-baran</dc:creator>
      <pubDate>Wed, 07 Jan 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/andriybaran/the-service-object-graveyard-why-your-poros-failed-and-handlers-won-2a8j</link>
      <guid>https://dev.to/andriybaran/the-service-object-graveyard-why-your-poros-failed-and-handlers-won-2a8j</guid>
      <description>&lt;h2&gt;
  
  
  The Hook: A PORO Mustache on a Fat Controller
&lt;/h2&gt;

&lt;p&gt;I spent years stuffing logic into &lt;code&gt;app/services&lt;/code&gt;. Felt productive. Felt like I was doing "proper architecture."&lt;/p&gt;

&lt;p&gt;Then I looked at what I'd actually built:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/services/products/create_service.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Products::CreateService&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;
    &lt;span class="vi"&gt;@current_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Unauthorized"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;authorized?&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Invalid params"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;valid_params?&lt;/span&gt;

    &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;
      &lt;span class="n"&gt;add_to_stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;notify_team&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;log_creation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authorized?&lt;/span&gt;
    &lt;span class="vi"&gt;@current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;can_create_products?&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;valid_params?&lt;/span&gt;
    &lt;span class="c1"&gt;# ... 50 lines of manual validation&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;product_params&lt;/span&gt;
    &lt;span class="vi"&gt;@params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ← Still manual, still drifts&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_to_stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ... 30 lines&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;notify_team&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ... 20 lines&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_creation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ... 15 lines&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;OpenStruct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;success?: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;product: &lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;OpenStruct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;success?: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# app/controllers/products_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Products&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CreateService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success?&lt;/span&gt;
      &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;product&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="vi"&gt;@errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What did I actually achieve?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I moved a 500-line controller method into a 400-line service object. Put a mustache on it. Called it "architecture."&lt;/p&gt;

&lt;p&gt;The controller was thinner. But everything else—params drift, scattered validation, implicit contracts, duplication between &lt;code&gt;new&lt;/code&gt; and &lt;code&gt;create&lt;/code&gt;—remained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'd hidden the problem. Not solved it.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Vibe Coding Reality: We Walked Halfway and Stopped
&lt;/h2&gt;

&lt;p&gt;Let's examine what happened.&lt;/p&gt;

&lt;p&gt;Around 2015, the Rails community realized: "Fat controllers are bad." Service objects emerged as the solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The promise:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Thin controllers&lt;/li&gt;
&lt;li&gt;Testable business logic&lt;/li&gt;
&lt;li&gt;Clear separation of concerns&lt;/li&gt;
&lt;li&gt;Reusable operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What we actually built:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Business logic extracted&lt;/li&gt;
&lt;li&gt;❌ Params still manual (&lt;code&gt;permit&lt;/code&gt; still drifts)&lt;/li&gt;
&lt;li&gt;❌ Validation still scattered (model? service? both?)&lt;/li&gt;
&lt;li&gt;❌ No standard interface (what does &lt;code&gt;call&lt;/code&gt; return?)&lt;/li&gt;
&lt;li&gt;❌ No structured error handling (strings? hashes? objects?)&lt;/li&gt;
&lt;li&gt;❌ No form integration (still writing ERB)&lt;/li&gt;
&lt;li&gt;❌ No action grouping (&lt;code&gt;new&lt;/code&gt; and &lt;code&gt;create&lt;/code&gt; still separate)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We built the framing for a modern house—then stopped. Threw a tarp over it. Called it "good enough."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I lived under that tarp for years&lt;/strong&gt;, getting wet every time it rained, calling it "minimalism."&lt;/p&gt;

&lt;p&gt;Take this paradox: Service objects were supposed to make code clearer. But I still couldn't answer basic questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What data format does this operation expect? (Implicit)&lt;/li&gt;
&lt;li&gt;Where does validation happen? (Scattered)&lt;/li&gt;
&lt;li&gt;How do I handle different error types? (Manual conditionals everywhere)&lt;/li&gt;
&lt;li&gt;Why am I duplicating setup between &lt;code&gt;new&lt;/code&gt; and &lt;code&gt;create&lt;/code&gt;? (¯\&lt;em&gt;(ツ)&lt;/em&gt;/¯)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The community walked halfway toward "Thin Controllers" but forgot to build the infrastructure to support them.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Sanity Tax: Settling for Params Drift
&lt;/h2&gt;

&lt;p&gt;Here's the tax I paid every month:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Saturday, 11:47 PM. Production bug.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;User reports profile not saving. I investigate. They filled out the form: name, email, phone, bio. Clicked save. Success message. But only email saved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The archaeology begins:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check the &lt;strong&gt;view&lt;/strong&gt; (&lt;code&gt;_form.html.erb&lt;/code&gt;) → &lt;code&gt;phone&lt;/code&gt; and &lt;code&gt;bio&lt;/code&gt; fields exist ✓&lt;/li&gt;
&lt;li&gt;Check the &lt;strong&gt;model&lt;/strong&gt; (&lt;code&gt;User&lt;/code&gt;) → validations for &lt;code&gt;phone&lt;/code&gt; and &lt;code&gt;bio&lt;/code&gt; exist ✓&lt;/li&gt;
&lt;li&gt;Check the &lt;strong&gt;service object&lt;/strong&gt; (&lt;code&gt;UpdateUserService&lt;/code&gt;) → validation logic for &lt;code&gt;phone&lt;/code&gt; and &lt;code&gt;bio&lt;/code&gt; exists ✓&lt;/li&gt;
&lt;li&gt;Check the &lt;strong&gt;controller&lt;/strong&gt; (&lt;code&gt;params.permit&lt;/code&gt;) → &lt;strong&gt;Missing &lt;code&gt;:phone&lt;/code&gt; and &lt;code&gt;:bio&lt;/code&gt;&lt;/strong&gt; ✗&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Someone added the fields six months ago. Updated three files. Forgot the fourth.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Controller (the silent killer)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;user_params&lt;/span&gt;
  &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ← Forgot :phone, :bio&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;%# View (looks fine) %&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt; &lt;span class="ss"&gt;:phone&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_area&lt;/span&gt; &lt;span class="ss"&gt;:bio&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Service (validates fields that never arrive)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UpdateUserService&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:bio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;length: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;maximum: &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The dispersed architecture:&lt;/strong&gt; View defines fields. Controller filters params. Model validates data. Service enforces business rules. &lt;strong&gt;Four files. Four places for drift.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The paradox:&lt;/strong&gt; I extracted business logic to make it testable. But the test passed because it used a hash, not controller params. Production failed because params filtering happened &lt;em&gt;before&lt;/em&gt; the service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Service objects don't solve params drift. They just move where you forget to update things.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I patched it. Happened again next month with a different field. And again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why was I settling for this?&lt;/strong&gt; Because everyone else was. We'd normalized the sanity tax.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Unified Contrast: Form as a Solid Thing
&lt;/h3&gt;

&lt;p&gt;The structure of submitted parameters mirrors the structure of the form. They aren't separate concerns - they're different states of the same contract. In the Handler+Form approach, this entire bug class disappears:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UpdateUserForm&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionForm&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="ss"&gt;type: :string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="ss"&gt;:phone&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="ss"&gt;type: :string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="ss"&gt;:bio&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="ss"&gt;type: :string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;length: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;maximum: &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;One file. One source of truth.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The form defines its fields (UI)&lt;/li&gt;
&lt;li&gt;The form defines validation (contract)&lt;/li&gt;
&lt;li&gt;The form auto-generates the params schema (security)&lt;/li&gt;
&lt;li&gt;The handler uses the form (business logic)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When you add a field, you add it in &lt;em&gt;one place&lt;/em&gt;.&lt;/strong&gt; The params filtering, the validation, and the UI are no longer three separate responsibilities that can drift—they're a unified declaration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The test change:&lt;/strong&gt; Instead of mocking four layers and hoping they align, you test the form as a cohesive unit. If the form test passes, the integration works. No archaeology required.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Lifecycle: How the Solid Form Actually Works
&lt;/h3&gt;

&lt;p&gt;Here's the mechanical flow that eliminates the drift:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Handler defines the form template&lt;/strong&gt; (class definition)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UsersHandler&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;SteelWheel&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;form_class&lt;/span&gt;
    &lt;span class="no"&gt;UpdateUserForm&lt;/span&gt;  &lt;span class="c1"&gt;# The template&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: GET request&lt;/strong&gt; — Create empty form instance&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="vi"&gt;@form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;UsersHandler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;form_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;object: &lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Form renders itself with current user data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: User populates form&lt;/strong&gt; — Browser submits params&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: POST request&lt;/strong&gt; — Form creates params instance from params template (which is reflection of form structure) and user's inputs&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="vi"&gt;@params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;UsersHandler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;form_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;params_definition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Auto-generated schema validates input&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5: Validation feedback&lt;/strong&gt; — Create form with errors&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="vi"&gt;@form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_form&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;object: &lt;/span&gt;&lt;span class="vi"&gt;@params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Form instance now contains validation errors&lt;/span&gt;
&lt;span class="c1"&gt;# Renders itself with inline error messages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The key insight:&lt;/strong&gt; The form is both the UI definition &lt;em&gt;and&lt;/em&gt; the params schema generator. When you add a field to the form, you simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define its rendering&lt;/li&gt;
&lt;li&gt;Define its validation&lt;/li&gt;
&lt;li&gt;Auto-generate its params filter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;No drift. No archaeology. One solid thing.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Reformation: SteelWheel as Validation Machine
&lt;/h2&gt;

&lt;p&gt;Here's what changed when I stopped treating service objects as "a &lt;code&gt;call&lt;/code&gt; method in a void" and started using &lt;strong&gt;structured handlers&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/handlers/products/create_handler.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Products::CreateHandler&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationHandler&lt;/span&gt;
  &lt;span class="c1"&gt;# 1. URL validation (400 Bad Request if wrong)&lt;/span&gt;
  &lt;span class="n"&gt;url_params&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:format&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# 2. Form binding (generates params validation automatically)&lt;/span&gt;
  &lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="no"&gt;Products&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ModelForm&lt;/span&gt;

  &lt;span class="c1"&gt;# 3. Dependency validation (404 if missing)&lt;/span&gt;
  &lt;span class="n"&gt;finder&lt;/span&gt; &lt;span class="ss"&gt;:category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="n"&gt;form_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;category_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# 4. Form params validation (422 if invalid)&lt;/span&gt;
  &lt;span class="c1"&gt;# form_params automatically validated by form definition&lt;/span&gt;

  &lt;span class="c1"&gt;# 5. Business logic (only runs if all validation passes)&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_validation_success&lt;/span&gt;
    &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create?&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;form_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;add_to_stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;notify_team&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_to_stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;PointOfSale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="no"&gt;PosProductStock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;pos: &lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;product: &lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;on_hand: &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;notify_team&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;ProductMailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;deliver_later&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Controller becomes declarative traffic cop&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;handler: :create&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="vi"&gt;@product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;product&lt;/span&gt;
    &lt;span class="vi"&gt;@form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;form&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ss"&gt;:create&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;product&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="vi"&gt;@product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;product&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="vi"&gt;@form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;form&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What's different?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not just "business logic extracted." The entire &lt;strong&gt;validation hierarchy is explicit&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;URL params&lt;/strong&gt; (400): Wrong format, invalid action&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies&lt;/strong&gt; (404): Resource doesn't exist&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Form params&lt;/strong&gt; (422): Invalid data format&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business logic&lt;/strong&gt;: Only runs if 1-3 pass&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Service objects collapse all of this into &lt;code&gt;if result.success?&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can't tell if it failed because of invalid params, missing dependencies, or business logic. One bucket. Manual error handling everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handlers separate concerns by HTTP semantics.&lt;/strong&gt; Different failure modes get different responses.&lt;/p&gt;




&lt;h2&gt;
  
  
  Leveling Up: The Validation Hierarchy I Was Missing
&lt;/h2&gt;

&lt;p&gt;Let me show you why this matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With service objects:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CreateProductService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success?&lt;/span&gt;
  &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;product&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
  &lt;span class="c1"&gt;# result.error could be:&lt;/span&gt;
  &lt;span class="c1"&gt;# - "Unauthorized"&lt;/span&gt;
  &lt;span class="c1"&gt;# - "Invalid params"&lt;/span&gt;
  &lt;span class="c1"&gt;# - "Category not found"&lt;/span&gt;
  &lt;span class="c1"&gt;# - "Product already exists"&lt;/span&gt;
  &lt;span class="c1"&gt;# - Model validation errors&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="c1"&gt;# All failures look the same. How do I respond?&lt;/span&gt;
  &lt;span class="c1"&gt;# Manual conditionals everywhere.&lt;/span&gt;

  &lt;span class="n"&gt;flash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;  &lt;span class="c1"&gt;# ← String? Hash? Object?&lt;/span&gt;
  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;With handlers:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ss"&gt;:create&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="c1"&gt;# url_params invalid? → 400 (automatic, before block runs)&lt;/span&gt;
  &lt;span class="c1"&gt;# category not found? → 404 (automatic, finder fails)&lt;/span&gt;
  &lt;span class="c1"&gt;# form_params invalid? → 422 (automatic, form validation fails)&lt;/span&gt;

  &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;product&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;# ← Only business logic success&lt;/span&gt;
  &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;# ← Form errors automatically in @form&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The mechanics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;400&lt;/strong&gt;: Client sent garbage → reject before processing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;404&lt;/strong&gt;: Resource doesn't exist → can't proceed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;422&lt;/strong&gt;: Data format wrong → validation errors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;200&lt;/strong&gt;: Everything valid, business logic executed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Service objects treat all failures as equal.&lt;/strong&gt; You manually sort them out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handlers treat failures by HTTP semantics.&lt;/strong&gt; The framework sorts them out.&lt;/p&gt;

&lt;p&gt;This isn't just cleaner code. It's &lt;strong&gt;correct failure modes mapped to HTTP semantics&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I spent years writing manual conditionals to distinguish these cases. The hierarchy was always there—I just wasn't making it explicit.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Symmetry Revelation: One Handler, Multiple Actions
&lt;/h2&gt;

&lt;p&gt;Here's a paradox I lived with for years:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;new&lt;/code&gt; and &lt;code&gt;create&lt;/code&gt; are two sides of the same coin.&lt;/strong&gt; Same domain concept. Same data format. Same setup.&lt;/p&gt;

&lt;p&gt;Yet I treated them as &lt;strong&gt;separate silos&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Traditional approach&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;
  &lt;span class="vi"&gt;@product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
  &lt;span class="vi"&gt;@category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:category_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="n"&gt;authorize&lt;/span&gt; &lt;span class="vi"&gt;@product&lt;/span&gt;
  &lt;span class="n"&gt;setup_defaults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
  &lt;span class="vi"&gt;@product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="vi"&gt;@category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:category_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# ← Duplicated&lt;/span&gt;
  &lt;span class="n"&gt;authorize&lt;/span&gt; &lt;span class="vi"&gt;@product&lt;/span&gt;  &lt;span class="c1"&gt;# ← Duplicated&lt;/span&gt;
  &lt;span class="n"&gt;setup_defaults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valid?&lt;/span&gt;  &lt;span class="c1"&gt;# ← Duplicated&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;
    &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="vi"&gt;@product&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# With service objects&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateProductService&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;
    &lt;span class="vi"&gt;@current_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;
    &lt;span class="vi"&gt;@category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:category_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# ← Still duplicated&lt;/span&gt;
    &lt;span class="n"&gt;authorize!&lt;/span&gt;  &lt;span class="c1"&gt;# ← Still duplicated&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# But NEW action still does all the setup separately&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;
  &lt;span class="vi"&gt;@product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
  &lt;span class="vi"&gt;@category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:category_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# ← STILL DUPLICATED&lt;/span&gt;
  &lt;span class="n"&gt;authorize&lt;/span&gt; &lt;span class="vi"&gt;@product&lt;/span&gt;  &lt;span class="c1"&gt;# ← STILL DUPLICATED&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Service objects don't fix this.&lt;/strong&gt; They only handle &lt;code&gt;create&lt;/code&gt;. &lt;code&gt;new&lt;/code&gt; still duplicates setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; Because we think of them as separate actions. But they're not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;new&lt;/code&gt; (GET): Display the contract&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;create&lt;/code&gt; (POST): Fulfill the contract&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Same contract. Same setup. Same handler.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Products::CreateHandler&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationHandler&lt;/span&gt;
  &lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="no"&gt;Products&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ModelForm&lt;/span&gt;

  &lt;span class="n"&gt;finder&lt;/span&gt; &lt;span class="ss"&gt;:category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="n"&gt;url_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;category_id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;form_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;category_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;verify&lt;/span&gt; &lt;span class="n"&gt;memoize&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;product&lt;/span&gt;
    &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;form_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_validation_success&lt;/span&gt;
    &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create?&lt;/span&gt;  &lt;span class="c1"&gt;# ← Only execute on POST&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save!&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Controller&lt;/span&gt;
&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;handler: :create&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="vi"&gt;@product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;product&lt;/span&gt;  &lt;span class="c1"&gt;# ← Same product setup as create&lt;/span&gt;
  &lt;span class="vi"&gt;@form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;form&lt;/span&gt;        &lt;span class="c1"&gt;# ← Same form&lt;/span&gt;
  &lt;span class="vi"&gt;@category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;category&lt;/span&gt;  &lt;span class="c1"&gt;# ← Same category finding&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ss"&gt;:create&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;product&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;One handler. Two actions. Zero duplication.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Category finding happens once. Authorization happens once. Product setup happens once.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;new&lt;/code&gt; displays the form. &lt;code&gt;create&lt;/code&gt; executes the logic. But they &lt;strong&gt;share the exact same soul and data contract&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This applies to &lt;code&gt;show&lt;/code&gt;/&lt;code&gt;edit&lt;/code&gt;/&lt;code&gt;update&lt;/code&gt;/&lt;code&gt;destroy&lt;/code&gt; too—one &lt;code&gt;UpdateHandler&lt;/code&gt; for all four.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I spent years treating these as separate because controllers made me think in actions.&lt;/strong&gt; Handlers made me think in &lt;strong&gt;domain operations&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Declarative Traffic Cop
&lt;/h2&gt;

&lt;p&gt;The controller becomes a &lt;strong&gt;declarative traffic cop&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before: Imperative coordination&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
  &lt;span class="vi"&gt;@product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ← Manual setup&lt;/span&gt;
  &lt;span class="vi"&gt;@category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:category_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# ← Manual dependency&lt;/span&gt;

  &lt;span class="n"&gt;authorize&lt;/span&gt; &lt;span class="vi"&gt;@product&lt;/span&gt;  &lt;span class="c1"&gt;# ← Manual authorization&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalid?&lt;/span&gt;  &lt;span class="c1"&gt;# ← Manual validation&lt;/span&gt;
    &lt;span class="vi"&gt;@errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;  &lt;span class="c1"&gt;# ← Manual persistence&lt;/span&gt;
    &lt;span class="n"&gt;add_to_stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ← Manual side effects&lt;/span&gt;
    &lt;span class="n"&gt;notify_team&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="vi"&gt;@product&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# After: Declarative delegation&lt;/span&gt;
&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ss"&gt;:create&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;product&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The controller doesn't coordinate.&lt;/strong&gt; It delegates to a handler that has an explicit validation hierarchy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The handler doesn't just execute business logic.&lt;/strong&gt; It defines the entire contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What URL params are valid&lt;/li&gt;
&lt;li&gt;What dependencies are required&lt;/li&gt;
&lt;li&gt;What form data is expected&lt;/li&gt;
&lt;li&gt;What business logic runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The form generates params validation.&lt;/strong&gt; Change the form, params update automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No drift. One source of truth. Impossible to forget.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Service objects got me halfway: "extract business logic." Handlers finished the job: "make the contract explicit and automatic."&lt;/p&gt;




&lt;h2&gt;
  
  
  Completing the Abandoned Task: The Infrastructure We Forgot
&lt;/h2&gt;

&lt;p&gt;Let's examine what the community actually did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2012-2014:&lt;/strong&gt; "Fat controllers are bad"&lt;br&gt;
&lt;strong&gt;2015-2017:&lt;/strong&gt; "Service objects are the answer"&lt;br&gt;
&lt;strong&gt;2018-2020:&lt;/strong&gt; "Wait, this still doesn't solve params drift / validation scatter / duplication"&lt;br&gt;
&lt;strong&gt;2021-2026:&lt;/strong&gt; &lt;em&gt;...crickets...&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We identified the problem. We started the solution. &lt;strong&gt;Then we stopped before building the infrastructure.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Service objects are POROs. "Plain Old Ruby Objects." By definition: no framework support.&lt;/p&gt;

&lt;p&gt;Which means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No standard interface (everyone invents their own)&lt;/li&gt;
&lt;li&gt;No automatic params generation (manual drift)&lt;/li&gt;
&lt;li&gt;No validation hierarchy (one &lt;code&gt;if&lt;/code&gt; statement for everything)&lt;/li&gt;
&lt;li&gt;No form integration (still writing ERB)&lt;/li&gt;
&lt;li&gt;No action grouping (still duplicating &lt;code&gt;new&lt;/code&gt;/&lt;code&gt;create&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;No HTTP-semantic error handling (strings everywhere)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;We called this "simplicity."&lt;/strong&gt; It's not simplicity. It's &lt;strong&gt;incompleteness&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The "Rails Way™" didn't evolve because addressing these requires admitting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Forms should generate their own params (tight coupling is good here)&lt;/li&gt;
&lt;li&gt;Validation has a hierarchy (URL → Dependencies → Form → Business)&lt;/li&gt;
&lt;li&gt;Related actions share a contract (&lt;code&gt;new&lt;/code&gt;/&lt;code&gt;create&lt;/code&gt; are one operation)&lt;/li&gt;
&lt;li&gt;Controllers should be declarative, not imperative&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;This challenges core Rails assumptions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So we lived with params drift. We duplicated setup. We manually sorted failure modes. We called it "pragmatic."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But it's not pragmatic when your app grows past 50 controllers and you're debugging params drift every month.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Is it time to admit traditional Rails patterns fail when the domain gets messy? And finally finish the architectural work we started?&lt;/p&gt;




&lt;h2&gt;
  
  
  The Half-Finished House Analogy
&lt;/h2&gt;

&lt;p&gt;Refactoring with handlers is like moving into a house where the previous owners (the community) put up beautiful framing for "Thin Controllers" but left before installing the plumbing or the roof.&lt;/p&gt;

&lt;p&gt;I spent years getting wet every time it rained (params drift, validation inconsistency, duplication bugs), calling it "minimalism," until I realized: &lt;strong&gt;properly applied architecture isn't over-engineering. It's finishing the house.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The framing (service objects):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extract business logic ✓&lt;/li&gt;
&lt;li&gt;Make it testable ✓&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The plumbing (handlers):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explicit data contract (form + params)&lt;/li&gt;
&lt;li&gt;Structured validation hierarchy (400/404/422/200)&lt;/li&gt;
&lt;li&gt;Action grouping (one handler, multiple actions)&lt;/li&gt;
&lt;li&gt;Automatic params generation (no drift)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The roof (complete infrastructure):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Declarative controllers (traffic cops)&lt;/li&gt;
&lt;li&gt;Form-driven development (contract-first)&lt;/li&gt;
&lt;li&gt;HTTP-semantic error handling&lt;/li&gt;
&lt;li&gt;Zero duplication by design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Service objects got us the framing. Handlers install the plumbing and roof.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can live in a house with just framing.&lt;/strong&gt; I did, for years. But you'll get wet when it rains. And eventually you'll wonder: why am I tolerating this?&lt;/p&gt;




&lt;h2&gt;
  
  
  The Paradox We Ignored
&lt;/h2&gt;

&lt;p&gt;Here's the final paradox:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We extracted business logic to "simplify."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But we left the contract implicit, validation scattered, params manual, and actions duplicated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The extraction didn't simplify. It just moved complexity around.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Handlers complete the simplification:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contract explicit (form)&lt;/li&gt;
&lt;li&gt;Validation structured (hierarchy)&lt;/li&gt;
&lt;li&gt;Params automatic (generated)&lt;/li&gt;
&lt;li&gt;Actions grouped (shared context)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;True simplicity isn't "less code."&lt;/strong&gt; It's &lt;strong&gt;explicit constraints that prevent entire classes of bugs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I spent six years learning this. Maybe you can learn it faster.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/andriy-baran/steel_wheel" rel="noopener noreferrer"&gt;SteelWheel&lt;/a&gt; - Handler framework&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/andriy-baran/action_form" rel="noopener noreferrer"&gt;ActionForm&lt;/a&gt; - Form DSL&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/andriy-baran/easy_params" rel="noopener noreferrer"&gt;EasyParams&lt;/a&gt; - Type-safe parameters&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/andriy-baran/sips" rel="noopener noreferrer"&gt;Real App&lt;/a&gt; - Production example&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;If you're still living under the tarp&lt;/strong&gt;, ask yourself:&lt;/p&gt;

&lt;p&gt;Is params drift acceptable? Is validation scatter acceptable? Is &lt;code&gt;new&lt;/code&gt;/&lt;code&gt;create&lt;/code&gt; duplication acceptable?&lt;/p&gt;

&lt;p&gt;Or is it time to finish installing the plumbing?&lt;/p&gt;

&lt;p&gt;The framing has been up for a decade. Let's finish the house.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>architecture</category>
      <category>refactoring</category>
    </item>
  </channel>
</rss>
