<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://roberthopman.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://roberthopman.com/" rel="alternate" type="text/html" /><updated>2026-03-15T11:39:55+00:00</updated><id>https://roberthopman.com/feed.xml</id><title type="html">My blog</title><subtitle>Technical blog by Robert Hopman with practical guides on syntax, patterns, upgrades, and software development best practices for developers</subtitle><author><name>Robert Hopman</name><email>robert@bonaroo.nl</email></author><entry><title type="html">A History of Quality in Software Engineering</title><link href="https://roberthopman.com/history-of-quality-in-software-engineering/" rel="alternate" type="text/html" title="A History of Quality in Software Engineering" /><published>2026-02-03T00:00:00+00:00</published><updated>2026-02-03T00:00:00+00:00</updated><id>https://roberthopman.com/history-of-quality-in-software-engineering</id><content type="html" xml:base="https://roberthopman.com/history-of-quality-in-software-engineering/"><![CDATA[<p>Software quality practices have evolved over six decades. What began as a response
to the “software crisis” of the 1960s has grown into collaborative specification
techniques that bridge the gap between business and technical teams.</p>

<h2 id="contents">Contents</h2>

<ol>
  <li><a href="#evolution-of-quality-approaches">Evolution of Quality Approaches</a></li>
  <li><a href="#the-software-crisis-and-birth-of-software-engineering-1968">The Software Crisis (1968)</a></li>
  <li><a href="#structured-programming-1968">Structured Programming (1968)</a></li>
  <li><a href="#fagan-inspections-1976">Fagan Inspections (1976)</a></li>
  <li><a href="#cleanroom-software-engineering-1980s">Cleanroom Software Engineering (1980s)</a></li>
  <li><a href="#personal-software-process-1990s">Personal Software Process (1990s)</a></li>
  <li><a href="#user-stories-and-acceptance-criteria">User Stories (1990s, XP origin)</a></li>
  <li><a href="#extreme-programming-1996-1999">Extreme Programming (1996-1999)</a></li>
  <li><a href="#uml-and-design-communication-1997">UML and Design Communication (1997)</a></li>
  <li><a href="#test-driven-development">Test-Driven Development (~2000)</a></li>
  <li><a href="#given-when-then-format">Given-When-Then Format (early 2000s)</a></li>
  <li><a href="#the-birth-of-bdd-2006">Behavior-Driven Development (2006)</a></li>
  <li><a href="#specification-by-example-2010s">Specification by Example (2010s)</a></li>
  <li><a href="#the-c4-model-2011">The C4 Model (2011)</a></li>
  <li><a href="#example-mapping">Example Mapping (mid-2010s)</a></li>
  <li><a href="#tdd-vs-bdd-choosing-an-approach">TDD vs BDD</a></li>
  <li><a href="#common-pitfalls">Common Pitfalls</a></li>
  <li><a href="#key-figures">Key Figures</a></li>
  <li><a href="#further-reading">Further Reading</a></li>
  <li><a href="#example-story-customer-books-a-cleaning-appointment">Example Story: Customer Books a Cleaning Appointment</a></li>
</ol>

<h2 id="evolution-of-quality-approaches">Evolution of Quality Approaches</h2>

<p><strong>1968-1990s: Quality through PROCESS</strong></p>

<p>The early focus was on disciplined processes to catch defects: <a href="#fagan-inspections-1976">Fagan Inspections</a> (formal peer review), <a href="#cleanroom-software-engineering-1980s">Cleanroom</a> (defect prevention), and <a href="#personal-software-process-1990s">PSP</a> (individual measurement).</p>

<p><strong>1994-1997: Quality through DESIGN COMMUNICATION</strong></p>

<p>Teams needed shared visual languages. <a href="#uml-and-design-communication-1997">UML</a> unified competing notations into a standard for modeling software systems.</p>

<p><strong>1996-2001: Quality through PRACTICE</strong></p>

<p><a href="#extreme-programming-1996-1999">Extreme Programming</a> shifted focus to lightweight practices: <a href="#test-driven-development">TDD</a>, pair programming, and continuous integration.</p>

<p><strong>2001: The Agile Manifesto</strong></p>

<p>Seventeen practitioners valued “working software over comprehensive documentation,” creating tension with UML-heavy approaches. Source: <a href="https://agilemanifesto.org/">Agile Manifesto</a></p>

<p><strong>2006-2011: Quality through LIGHTWEIGHT COMMUNICATION</strong></p>

<p>Bridging business and technical teams: <a href="#the-birth-of-bdd-2006">BDD</a> (natural language specs), <a href="#the-c4-model-2011">C4 Model</a> (just enough diagrams), and <a href="#specification-by-example-2010s">Specification by Example</a> (collaborative examples).</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PROCESS --------&gt; DESIGN --------&gt; PRACTICE --------&gt; LIGHTWEIGHT
1968-1990s        1994-1997        1996-2001          2006-2011
    |                 |                |                  |
    v                 v                v                  v
Inspections         UML            XP &amp; TDD           BDD &amp; C4
Cleanroom        Sequence         Pair prog.            SbE
PSP              diagrams            CI             Example Map
</code></pre></div></div>

<hr />

<h2 id="the-software-crisis-and-birth-of-software-engineering-1968">The Software Crisis and Birth of Software Engineering (1968)</h2>

<p>The term “software crisis” was coined at the first NATO Software Engineering
Conference in Garmisch, Germany in 1968. The conference, attended by over fifty
experts from eleven countries including Edsger Dijkstra, Tony Hoare, and Niklaus
Wirth, confronted a growing problem: software projects were consistently over
budget, overdue, and unreliable.</p>

<p>As Dijkstra later observed in his 1972 Turing Award lecture:</p>

<blockquote>
  <p>“The major cause of the software crisis is that the machines have become
several orders of magnitude more powerful… as long as there were no
machines, programming was no problem at all; when we had a few weak
computers, programming became a mild problem, and now we have gigantic
computers, programming has become an equally gigantic problem.”</p>
</blockquote>

<p>The conference deliberately adopted the provocative term “software engineering”
to suggest that software development needed the rigor of traditional engineering
disciplines. This event marked the beginning of systematic approaches to
software quality.</p>

<hr />

<h2 id="structured-programming-1968">Structured Programming (1968)</h2>

<p>That same year, Dijkstra published his famous letter “Go To Statement Considered
Harmful” in Communications of the ACM, marking the beginning of structured
programming.</p>

<p><strong>Definition:</strong> A programming paradigm using block-based control flow instead of
arbitrary jumps via goto statements.</p>

<h3 id="the-three-control-structures">The Three Control Structures</h3>

<p>Dijkstra showed that any program can be written using just three constructs:</p>

<p><strong>Sequence</strong> - Statements execute one after another in order:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>read_input()
process_data()
write_output()
</code></pre></div></div>

<p><strong>Selection</strong> - Choose different paths based on a condition:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if user_is_authenticated:
    show_dashboard()
else:
    show_login_form()
</code></pre></div></div>

<p><strong>Iteration</strong> - Repeat statements while a condition holds:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>while items_remaining:
    process_next_item()
</code></pre></div></div>

<p><strong>Core principle:</strong> These three control structures are sufficient to express any
computable function (the structured program theorem).</p>

<p><strong>Key insight:</strong> Dijkstra observed that the quality of a programmer’s code was
inversely proportional to the number of gotos used. Code without gotos can more
easily be proven correct.</p>

<p>By the end of the 20th century, nearly all programming languages had adopted
structured programming constructs. Languages that originally lacked them
(FORTRAN, COBOL, BASIC) added support.</p>

<hr />

<h2 id="fagan-inspections-1976">Fagan Inspections (1976)</h2>

<p>Michael Fagan at IBM developed formal software inspections as a systematic
method for finding defects in documents, code, and specifications.</p>

<p><strong>Definition:</strong> A formal peer review process with predefined roles, entry/exit
criteria, and structured meetings focused solely on defect detection.</p>

<h3 id="inspection-roles">Inspection Roles</h3>

<ul>
  <li><strong>Moderator:</strong> Leads the inspection and ensures the process is followed</li>
  <li><strong>Reader:</strong> Presents the material being inspected to the team</li>
  <li><strong>Author:</strong> Created the work product and answers questions about it</li>
  <li><strong>Scribe:</strong> Records all defects found during the meeting</li>
</ul>

<h3 id="the-inspection-process">The Inspection Process</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+----------+    +----------+    +----------+    +----------+    +----------+    +----------+
| Planning |---&gt;| Overview |---&gt;|  Prep    |---&gt;|Inspection|---&gt;| Rework   |---&gt;| Follow-up|
+----------+    +----------+    +----------+    +----------+    +----------+    +----------+
     |              |               |               |               |               |
     v              v               v               v               v               v
  Select         Author          Individual      Team finds      Author         Verify
  material,      presents        review by       defects         fixes          fixes
  assign roles   context         inspectors      (NOT solutions) defects
</code></pre></div></div>

<ol>
  <li><strong>Planning:</strong> Select material to inspect and assign roles</li>
  <li><strong>Overview:</strong> Author presents context to the team</li>
  <li><strong>Preparation:</strong> Each inspector reviews the material individually</li>
  <li><strong>Inspection meeting:</strong> Team identifies defects (not solutions)</li>
  <li><strong>Rework:</strong> Author fixes the defects found</li>
  <li><strong>Follow-up:</strong> Verify that fixes are correct</li>
</ol>

<p><strong>Results:</strong> IBM reported that inspections located 82% of all errors. The
company doubled lines of code shipped while reducing defects per thousand lines
by two-thirds. Studies showed 80-90% of defects found with up to 25% resource
savings.</p>

<p><strong>Key principle:</strong> The inspection meeting finds defects only–solutions come
later during rework.</p>

<hr />

<h2 id="cleanroom-software-engineering-1980s">Cleanroom Software Engineering (1980s)</h2>

<p>Harlan Mills at IBM developed Cleanroom as a theory-based process for producing
software with certifiable reliability levels.</p>

<p><strong>Definition:</strong> A software development process emphasizing defect prevention
over defect removal, using formal methods and statistical quality control.</p>

<h3 id="cleanroom-process">Cleanroom Process</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+---------------+         +---------------+         +---------------+
| SPECIFICATION |  ---&gt;   | DEVELOPMENT   |  ---&gt;   | CERTIFICATION |
+---------------+         +---------------+         +---------------+

                DEFECT PREVENTION &gt; DEFECT REMOVAL

                     Traditional              Cleanroom
                     ----------               ---------
                     Code -&gt; Debug -&gt; Test    Verify -&gt; Certify
                     Find &amp; fix defects       Prevent defects
</code></pre></div></div>

<p><strong>Three phases:</strong></p>

<ol>
  <li><strong>Specification:</strong> Requirements analysis, function specification, usage
modeling</li>
  <li><strong>Development:</strong> Design with correctness verification–developers prove
their code is correct through formal reasoning, not debugging</li>
  <li><strong>Certification:</strong> Independent statistical testing based on expected usage
patterns</li>
</ol>

<p><strong>Key principle:</strong> Developers verify correctness through formal reasoning, not
debugging. A separate certification team performs all testing.</p>

<p><strong>Results:</strong> IBM’s COBOL/SF tool (85,000 lines of code) showed a ten-fold
reduction in defects during testing and five-fold improvement in developer
productivity. Only seven errors in three years of production use.</p>

<p>The term “Cleanroom” borrowed from semiconductor manufacturing reflects the
focus on preventing contamination (defects) rather than filtering it out later.</p>

<hr />

<h2 id="personal-software-process-1990s">Personal Software Process (1990s)</h2>

<p>Watts Humphrey at the Software Engineering Institute created PSP to apply
process improvement principles to individual developers.</p>

<p><strong>Definition:</strong> A structured self-improvement framework where engineers measure
and analyze their own performance to reduce defects and improve predictability.</p>

<h3 id="what-developers-track">What Developers Track</h3>

<ul>
  <li><strong>Time:</strong> Hours spent on each activity (design, code, test, etc.)</li>
  <li><strong>Defects:</strong> When injected, when found, and what type</li>
  <li><strong>Size:</strong> Lines of code predicted versus actual</li>
</ul>

<blockquote>
  <p>“You cannot improve what you do not measure”</p>
</blockquote>

<h3 id="process-levels-progressive-adoption">Process Levels (Progressive Adoption)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PSP0   --&gt;   PSP1   --&gt;   PSP2   --&gt;   PSP2.1
  |           |           |             |
  v           v           v             v
Baseline    Size &amp;      Code &amp;       Design
measure-    time        design       templates,
ment        estimation  reviews      verification
</code></pre></div></div>

<ul>
  <li><strong>PSP0:</strong> Establish baseline measurements (time, defects, size)</li>
  <li><strong>PSP1:</strong> Add size and time estimation based on historical data</li>
  <li><strong>PSP2:</strong> Add code reviews and design reviews</li>
  <li><strong>PSP2.1:</strong> Add design templates and formal verification</li>
</ul>

<p><strong>Key principle:</strong> By tracking personal data, engineers identify their own
defect patterns and improve systematically.</p>

<p>Humphrey, known as “the father of software quality,” also created the Capability
Maturity Model (CMM) for organizational process improvement.</p>

<hr />

<h2 id="user-stories-and-acceptance-criteria">User Stories and Acceptance Criteria</h2>

<p>User stories originated in Extreme Programming in the late 1990s as a
lightweight alternative to formal requirements documents. Dan North later
established a template that connects behavior to business value:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>As a [role]
I want [feature]
So that [benefit]
</code></pre></div></div>

<p><strong>Example:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>As a homeowner
I want to schedule recurring cleaning appointments
So that I don't have to remember to book each time
</code></pre></div></div>

<p>This format keeps the focus on who needs what and why, rather than jumping
straight to implementation details. Stories serve as placeholders for
conversations, not detailed specifications.</p>

<hr />

<h2 id="extreme-programming-1996-1999">Extreme Programming (1996-1999)</h2>

<p>Kent Beck developed Extreme Programming while leading the Chrysler Comprehensive
Compensation System (C3) payroll project, starting in March 1996. He refined the
methodology with Ward Cunningham and Ron Jeffries, publishing <em>Extreme
Programming Explained</em> in October 1999.</p>

<p><strong>Definition:</strong> A software development methodology that improves quality and
responsiveness to changing requirements through short development cycles,
continuous feedback, and close customer collaboration.</p>

<h3 id="xp-values">XP Values</h3>

<ul>
  <li><strong>Communication:</strong> Talk constantly with each other and with the customer</li>
  <li><strong>Simplicity:</strong> Build only what is needed now, no speculative features</li>
  <li><strong>Feedback:</strong> Test always, release often, review code continuously</li>
  <li><strong>Courage:</strong> Refactor aggressively, admit mistakes, discard failing approaches</li>
  <li><strong>Respect:</strong> Value every team member’s contribution (added in 2nd edition)</li>
</ul>

<h3 id="the-original-12-practices">The Original 12 Practices</h3>

<p><strong>Planning practices:</strong></p>

<ul>
  <li><u>Planning Game</u>: Business and development collaborate each iteration to
select and prioritize work based on value and cost estimates.</li>
  <li><u>Small Releases</u>: Release to production frequently (weeks, not months) so
each release delivers concrete business value and enables fast feedback.</li>
  <li><u>Metaphor</u>: Use a shared story or analogy that everyone understands to
guide development and name system components consistently.</li>
  <li><u>Customer Tests</u>: Customers write acceptance tests that define when a
story is complete, providing clear and testable requirements.</li>
</ul>

<p><strong>Development practices:</strong></p>

<ul>
  <li><u>Simple Design</u>: Build the simplest thing that could possibly work, then
refactor as understanding grows–no speculative generality.</li>
  <li><u>Pair Programming</u>: Two programmers work together at one workstation,
continuously reviewing each other’s work and sharing knowledge.</li>
  <li><u>Test-Driven Development</u>: Write a failing test before writing the code
that makes it pass, ensuring comprehensive test coverage.</li>
  <li><u>Refactoring</u>: Continuously improve code structure without changing
behavior, keeping the design clean as requirements evolve.</li>
  <li><u>Continuous Integration</u>: Integrate code into the shared repository
multiple times per day with automated builds and tests.</li>
</ul>

<p><strong>Team practices:</strong></p>

<ul>
  <li><u>On-site Customer</u>: A real customer sits with the team full-time to
answer questions and provide immediate feedback on decisions.</li>
  <li><u>Collective Ownership</u>: Anyone can modify any code, requiring coding
standards and comprehensive tests to work safely.</li>
  <li><u>Coding Standards</u>: The team agrees on coding conventions so all code
looks familiar and anyone can work on any part.</li>
  <li><u>40-Hour Week</u>: Sustainable pace prevents burnout and maintains quality,
since tired programmers make more mistakes.</li>
</ul>

<h3 id="pair-programming">Pair Programming</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+--------------------------------------------------+
|                   WORKSTATION                    |
|  +--------------------------------------------+  |
|  |                                            |  |
|  |              shared screen                 |  |
|  |                                            |  |
|  +--------------------------------------------+  |
+--------------------------------------------------+
          |                         |
          v                         v
    +-----------+             +-----------+
    |  DRIVER   |             | NAVIGATOR |
    +-----------+             +-----------+
    | Writes    |             | Reviews   |
    | code,     |  &lt;------&gt;   | continuously,
    | thinks    |   rotate    | thinks    |
    | tactical  |   roles     | strategic |
    +-----------+             +-----------+
</code></pre></div></div>

<p>Two programmers work together at one workstation. The <u>driver</u> writes code
and thinks tactically about the current line. The <u>navigator</u> reviews
continuously and thinks strategically about the overall approach. Pairs rotate
roles frequently, and partners switch often so knowledge spreads across the
team.</p>

<p>Studies showed ~15% more time investment but higher quality and faster knowledge
transfer.</p>

<h3 id="key-practices-explained">Key Practices Explained</h3>

<p><u>On-site Customer</u>: A real customer sits with the development team
full-time to answer questions, clarify requirements, and provide immediate
feedback. This was radical–previous methodologies treated requirements as
documents thrown over a wall.</p>

<p><u>Planning Game</u>: Business and development collaborate to maximize value.
Business writes User Stories on index cards describing desired features.
Development estimates effort. Business prioritizes based on value and cost.
Planning happens each iteration.</p>

<p><u>Collective Code Ownership</u>: No individual owns any code. Anyone can modify
any part of the system. This requires coding standards and comprehensive tests
to work safely.</p>

<p><u>Continuous Integration</u>: Developers integrate code into a shared
repository multiple times per day, with automated builds and tests. This catches
integration problems immediately rather than in a painful “integration phase.”</p>

<p><u>Small Releases</u>: Release to production frequently–weeks, not months. Each
release delivers concrete business value. Short cycles mean faster feedback and
easier course correction.</p>

<p><strong>Key insight:</strong> XP takes practices that work and turns them to “extreme”
levels–if testing is good, test constantly; if code review helps, review
continuously via pairing; if integration is painful, integrate continuously.</p>

<p>The C3 project was cancelled in February 2000 after Daimler-Benz acquired
Chrysler, but XP had already spread. Beck was among the seventeen signatories
of the Agile Manifesto in 2001.</p>

<hr />

<h2 id="uml-and-design-communication-1997">UML and Design Communication (1997)</h2>

<p>While process-focused approaches evolved, another stream addressed quality
through visual design communication. The Unified Modeling Language emerged
from the “method wars” of the early 1990s, when competing object-oriented
notations made it difficult for teams to share designs.</p>

<p><strong>Definition:</strong> A standardized visual modeling language for specifying,
constructing, and documenting software system artifacts.</p>

<h3 id="the-three-amigos">The Three Amigos</h3>

<p>In 1994-1996, three leading methodologists unified their competing approaches:</p>

<ul>
  <li><strong>Grady Booch</strong> brought the Booch Method, strong in design and construction</li>
  <li><strong>James Rumbaugh</strong> brought OMT (Object Modeling Technique), strong in analysis
and data systems</li>
  <li><strong>Ivar Jacobson</strong> brought OOSE, strong in use cases and requirements capture</li>
</ul>

<p>They joined at Rational Software and released UML 1.1 in 1997, which was adopted
by the Object Management Group (OMG) on November 14, 1997.</p>

<h3 id="uml-diagram-types">UML Diagram Types</h3>

<p>UML defines 14 diagram types in two categories:</p>

<p><strong>Structural diagrams</strong> (static system view):</p>
<ul>
  <li>Class, Object, Component, Deployment, Package, Composite Structure, Profile</li>
</ul>

<p><strong>Behavioral diagrams</strong> (dynamic system view):</p>
<ul>
  <li>Use Case, Activity, State Machine, Sequence, Communication, Interaction
Overview, Timing</li>
</ul>

<h3 id="sequence-diagrams">Sequence Diagrams</h3>

<p>Of all UML diagrams, sequence diagrams proved most valuable and survived the
agile backlash against heavy documentation. They show how objects interact
over time:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+--------+          +--------+          +--------+
| Client |          | Server |          |Database|
+---+----+          +---+----+          +---+----+
    |                   |                   |
    | 1. request()      |                   |
    |------------------&gt;|                   |
    |                   | 2. query()        |
    |                   |------------------&gt;|
    |                   |                   |
    |                   | 3. results        |
    |                   |&lt;------------------|
    |                   |                   |
    | 4. response       |                   |
    |&lt;------------------|                   |
    |                   |                   |
</code></pre></div></div>

<p>As Martin Fowler noted: “The primary value of drawing diagrams is communication.
Because the purpose is communication, it’s essential to strip away some
information so as to clarify other information.”</p>

<h3 id="uml-and-agile-the-tension">UML and Agile: The Tension</h3>

<p>UML emerged from the “big design up front” tradition–create detailed models
before coding. The Agile Manifesto (2001) explicitly valued “working software
over comprehensive documentation.”</p>

<p>Many agile teams abandoned UML entirely. Others adopted “agile modeling”–using
diagrams for communication without treating them as formal deliverables.</p>

<hr />

<h2 id="test-driven-development">Test-Driven Development</h2>

<p>TDD emerged as one of XP’s core technical practices but became influential in
its own right.</p>

<p><strong>Definition:</strong> A development technique where you write a failing test before
writing the code that makes it pass, followed by refactoring.</p>

<h3 id="the-red-green-refactor-cycle">The Red-Green-Refactor Cycle</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>          +-------+
          | WRITE |
          | TEST  |
          +---+---+
              |
              v
        +-----------+
        |   RED     |  &lt;-- Test fails
        |  (fail)   |
        +-----+-----+
              |
              | write minimal code
              v
        +-----------+
        |  GREEN    |  &lt;-- Test passes
        |  (pass)   |
        +-----+-----+
              |
              | improve structure
              v
        +-----------+
        | REFACTOR  |  &lt;-- Keep tests green
        |           |
        +-----+-----+
              |
              +---------&gt; repeat
</code></pre></div></div>

<ol>
  <li><u>Red</u>: Write a test for the next bit of functionality–it should fail</li>
  <li><u>Green</u>: Write the minimal code needed to make the test pass</li>
  <li><u>Refactor</u>: Improve the code structure while keeping all tests green</li>
</ol>

<p>As Martin Fowler notes, the most common mistake is neglecting the third step–
skipping refactoring leads to messy code accumulation.</p>

<p><strong>Two primary benefits:</strong></p>

<ul>
  <li>Self-testing code: implementation only occurs in response to test requirements</li>
  <li>Better design: thinking about interfaces before implementation separates
concerns naturally</li>
</ul>

<p><strong>Theoretical foundation:</strong> J.B. Rainsberger grounds TDD in queuing theory–when
process B requires reworking process A, efficiency improves by performing part
of B before A begins. This eliminates wasteful rework cycles.</p>

<hr />

<h2 id="given-when-then-format">Given-When-Then Format</h2>

<p>The Given-When-Then syntax became the dominant format for expressing
specifications. According to Gojko Adzic’s 2020 survey, this format accounts
for 71% of usage versus less than 10% for table-based formats.</p>

<h3 id="structure">Structure</h3>

<ul>
  <li><strong>Given</strong> - The context or setup (preconditions)</li>
  <li><strong>When</strong> - The action or event that triggers the behavior</li>
  <li><strong>Then</strong> - The expected outcome</li>
</ul>

<p><strong>Example:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Given a registered user with valid credentials
When they submit the login form
Then they should see their dashboard
</code></pre></div></div>

<p>The format won adoption due to a good balance between expressiveness and
developer productivity. Its simplicity enabled broader tooling support and
better IDE integration.</p>

<hr />

<h2 id="the-birth-of-bdd-2006">The Birth of BDD (2006)</h2>

<p>Dan North introduced Behavior-Driven Development as an evolution of TDD that
addresses a common problem: developers struggling with where to start testing,
what to test, and how much to test at once.</p>

<h3 id="tdd-vs-bdd-focus">TDD vs BDD Focus</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>TDD Question:                    BDD Question:
"How do we test this code?"      "What behavior should this system exhibit?"

        CODE                              BEHAVIOR
          |                                   |
          v                                   v
    Implementation                      User outcomes
    details                             and value
</code></pre></div></div>

<p>BDD shifts focus from implementation details to behavior and user outcomes. The
key insight was applying the same queuing theory principle at the analysis
level–implementing features reveals insights about other features.</p>

<p>This practice involves business and technical people writing examples together
to establish shared understanding.</p>

<hr />

<h2 id="specification-by-example-2010s">Specification by Example (2010s)</h2>

<p>Gojko Adzic’s Specification by Example (SbE) formalized the collaborative
approach where teams use concrete examples to define acceptance criteria, guide
development, and create executable tests.</p>

<p><strong>Key findings from his 2020 survey after 10 years of SbE adoption:</strong></p>

<ul>
  <li>Teams using examples as acceptance criteria: 22% rated software as “Great”
(vs 8% without)</li>
  <li>47% of teams now define acceptance criteria collaboratively with business</li>
  <li>One-third don’t automate examples–yet automation correlates with 2x quality
ratings</li>
</ul>

<hr />

<h2 id="the-c4-model-2011">The C4 Model (2011)</h2>

<p>Simon Brown developed the C4 model between 2006-2011 as a response to teams
abandoning architecture diagrams entirely. While agile practitioners rejected
heavyweight UML, they still needed ways to communicate system structure.</p>

<p><strong>Definition:</strong> A hierarchical approach to visualizing software architecture at
four levels of abstraction: Context, Containers, Components, and Code.</p>

<h3 id="the-four-levels">The Four Levels</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Level 1: CONTEXT                    Level 2: CONTAINERS
"What is the system?"               "What's inside the system?"
+-------------------------+         +-------------------------+
|                         |         |  +-----+  +-----+       |
|   [Users]---&gt;[System]   |         |  | Web |  | API |       |
|              /    \     |         |  | App |  |     |       |
|             v      v    |         |  +--+--+  +--+--+       |
|      [External] [Mail]  |         |     |        |         |
|       System   Service  |         |     v        v         |
|                         |         |  +-------------+       |
+-------------------------+         |  |  Database   |       |
For: Everyone                       |  +-------------+       |
(business + technical)              +-------------------------+
                                    For: Technical people

Level 3: COMPONENTS                 Level 4: CODE
"What's inside a container?"        "How is component implemented?"
+-------------------------+         +-------------------------+
|  API Container          |         |                         |
|  +--------+ +--------+  |         |   Class diagrams        |
|  |  Auth  | | Order  |  |         |   (usually auto-        |
|  |Handler | |Service |  |         |    generated from       |
|  +--------+ +--------+  |         |    source code)         |
|       \      /          |         |                         |
|        v    v           |         |   Rarely used--too      |
|    +----------+         |         |   detailed for most     |
|    |Repository|         |         |   purposes              |
|    +----------+         |         |                         |
+-------------------------+         +-------------------------+
For: Developers/architects          For: Detailed design
</code></pre></div></div>

<ul>
  <li><u>Level 1 - Context</u>: Shows the system in its environment with users and
external systems. Suitable for everyone including non-technical stakeholders.</li>
  <li><u>Level 2 - Containers</u>: Zooms into the system to show applications,
databases, and services. For technical audiences.</li>
  <li><u>Level 3 - Components</u>: Zooms into a container to show internal
components. For developers and architects.</li>
  <li><u>Level 4 - Code</u>: Class-level diagrams, usually auto-generated. Rarely
used as it’s too detailed for most purposes.</li>
</ul>

<p><strong>Key principle:</strong> Good diagrams are about communication, not compliance with a
notation standard. Use whatever helps the team understand the system.</p>

<p>Brown has taught the C4 model to over 10,000 people in ~40 countries, reflecting
demand for lightweight architecture visualization that agile teams will use.</p>

<hr />

<h2 id="example-mapping">Example Mapping</h2>

<p>Example Mapping emerged as a structured conversation technique for exploring
user stories before development begins. Teams use colored cards to capture
different elements of a story.</p>

<h3 id="card-types">Card Types</h3>

<ul>
  <li><strong>Yellow card:</strong> The user story being discussed</li>
  <li><strong>Blue cards:</strong> Rules or acceptance criteria that govern the story</li>
  <li><strong>Green cards:</strong> Concrete examples that illustrate each rule</li>
  <li><strong>Red cards:</strong> Questions that need answers before development can proceed</li>
</ul>

<h3 id="example-cleaning-scheduling-service">Example: Cleaning Scheduling Service</h3>

<p><strong>Story (Yellow):</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>As a homeowner
I want to reschedule a cleaning appointment
So that I can adjust to changes in my calendar
</code></pre></div></div>

<p><strong>Rule 1 (Blue):</strong> Rescheduling must be done at least 24 hours in advance</p>

<ul>
  <li>Example (Green): Appointment on Friday 2pm, customer reschedules Wednesday
at 3pm -&gt; Allowed</li>
  <li>Example (Green): Appointment on Friday 2pm, customer reschedules Thursday
at 4pm -&gt; Denied with message “Less than 24 hours notice”</li>
</ul>

<p><strong>Rule 2 (Blue):</strong> Customers can only reschedule to available time slots</p>

<ul>
  <li>Example (Green): Customer selects Saturday 10am which shows as available -&gt;
Appointment moved to Saturday 10am</li>
  <li>Example (Green): Customer selects Saturday 10am which is fully booked -&gt;
Slot not shown in available options</li>
</ul>

<p><strong>Rule 3 (Blue):</strong> Rescheduling is free for the first change, then costs $15</p>

<ul>
  <li>Example (Green): First reschedule of appointment -&gt; No fee charged</li>
  <li>Example (Green): Second reschedule of same appointment -&gt; $15 fee shown
before confirmation</li>
</ul>

<p><strong>Questions (Red):</strong></p>

<ul>
  <li>What happens if the cleaner cancels? Does that reset the customer’s free
reschedule?</li>
  <li>Can customers reschedule to a different cleaner?</li>
  <li>Should we send a confirmation email after rescheduling?</li>
</ul>

<p>This approach surfaces ambiguity and missing requirements early, when changes
are cheapest. If a story has too many red cards, it needs more discovery. If
it has too many blue cards, it might need to be split into smaller stories.</p>

<hr />

<h2 id="tdd-vs-bdd-choosing-an-approach">TDD vs BDD: Choosing an Approach</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Aspect          TDD                       BDD
------          ---                       ---
Focus           Implementation details    User behavior
Language        Programming language      Natural language
Audience        Developers                Stakeholders + developers
Scope           Unit level                System interactions
Primary use     Code quality/design       Requirements communication
</code></pre></div></div>

<p>Both methodologies write tests before implementing code and create automated
test suites.</p>

<p><strong>Use TDD when:</strong> prioritizing code quality and design, working in continuous
integration environments, or needing rapid developer feedback.</p>

<p><strong>Use BDD when:</strong> requiring non-technical stakeholder involvement, building
user-centric applications, or dealing with complex business logic needing clear
communication.</p>

<hr />

<h2 id="common-pitfalls">Common Pitfalls</h2>

<h3 id="tdd-pitfalls">TDD Pitfalls</h3>

<ul>
  <li>Over-testing minor functions</li>
  <li>Skipping refactoring (the third step)</li>
  <li>Writing code before tests</li>
</ul>

<h3 id="bdd-pitfalls">BDD Pitfalls</h3>

<ul>
  <li>Creating overly vague or excessively detailed scenarios</li>
  <li>Excluding stakeholders from specification conversations</li>
  <li>Failing to automate the examples</li>
</ul>

<hr />

<h2 id="key-figures">Key Figures</h2>

<p>See <a href="#further-reading">Further Reading</a> for links to their work.</p>

<h3 id="pre-xp-era">Pre-XP Era</h3>

<ul>
  <li><strong>Edsger Dijkstra:</strong> Structured programming, “Go To Statement Considered
Harmful” (1968)</li>
  <li><strong>Michael Fagan:</strong> Formal software inspections at IBM (1976)</li>
  <li><strong>Harlan Mills:</strong> Cleanroom software engineering at IBM (1980s)</li>
  <li><strong>Watts Humphrey:</strong> Personal Software Process, CMM, “father of software
quality” (1990s)</li>
</ul>

<h3 id="design-communication-uml-era">Design Communication (UML Era)</h3>

<ul>
  <li><strong>Grady Booch:</strong> Booch Method, co-creator of UML, one of the “Three Amigos”</li>
  <li><strong>James Rumbaugh:</strong> Object Modeling Technique (OMT), co-creator of UML</li>
  <li><strong>Ivar Jacobson:</strong> Use cases, OOSE method, co-creator of UML</li>
</ul>

<h3 id="xp-and-agile-era">XP and Agile Era</h3>

<ul>
  <li><strong>Kent Beck:</strong> Created XP and TDD at Chrysler C3 project (1996). Author of
<em>Extreme Programming Explained</em> and <em>Test-Driven Development</em>. Agile
Manifesto signatory</li>
  <li><strong>Ward Cunningham:</strong> Co-developed XP practices with Beck. Created the wiki,
CRC cards, and FIT testing framework</li>
  <li><strong>Ron Jeffries:</strong> XP coach on C3 project, helped codify and spread XP
practices. Agile Manifesto signatory</li>
  <li><strong>Martin Fowler:</strong> Wrote extensively on refactoring, TDD, patterns, and UML.
Agile Manifesto signatory</li>
  <li><strong>Dan North:</strong> Introduced BDD in 2006 as evolution of TDD</li>
  <li><strong>Gojko Adzic:</strong> Authored <em>Specification by Example</em> and conducted industry
surveys on adoption</li>
  <li><strong>J.B. Rainsberger:</strong> Connected TDD to queuing theory and its productivity
benefits</li>
  <li><strong>Simon Brown:</strong> Created C4 model (2006-2011), author of <em>Software
Architecture for Developers</em>, founder of Structurizr</li>
</ul>

<hr />

<h2 id="further-reading">Further Reading</h2>

<h3 id="historical-foundations">Historical Foundations</h3>

<ul>
  <li><a href="http://homepages.cs.ncl.ac.uk/brian.randell/NATO/nato1968.PDF">NATO 1968 Conference Report</a></li>
  <li><a href="https://homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.pdf">Go To Statement Considered Harmful</a> - Dijkstra</li>
  <li><a href="https://en.wikipedia.org/wiki/Fagan_inspection">Fagan Inspection</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Cleanroom_software_engineering">Cleanroom Software Engineering</a></li>
  <li><a href="https://resources.sei.cmu.edu/library/asset-view.cfm?assetid=5283">Personal Software Process</a> - SEI</li>
</ul>

<h3 id="design-communication">Design Communication</h3>

<ul>
  <li><a href="https://en.wikipedia.org/wiki/Unified_Modeling_Language">Unified Modeling Language</a> - Wikipedia</li>
  <li><a href="https://www.uml-diagrams.org/">UML Diagrams</a> - Reference guide</li>
  <li><a href="https://c4model.com/">C4 Model</a> - Simon Brown’s official site</li>
  <li><a href="https://leanpub.com/visualising-software-architecture">Software Architecture for Developers</a> - Simon Brown</li>
  <li><a href="https://en.wikipedia.org/wiki/Sequence_diagram">Sequence Diagrams</a> - Wikipedia</li>
</ul>

<h3 id="extreme-programming">Extreme Programming</h3>

<ul>
  <li><a href="https://ronjeffries.com/xprog/what-is-extreme-programming/">What is Extreme Programming?</a> - Ron Jeffries</li>
  <li><a href="https://en.wikipedia.org/wiki/Extreme_programming">Extreme Programming</a> - Wikipedia</li>
  <li><a href="https://extremeprogrammingalliance.com/about-extreme-programming-xp/">About Extreme Programming</a> - XP Alliance</li>
</ul>

<h3 id="tdd-and-bdd">TDD and BDD</h3>

<ul>
  <li><a href="https://martinfowler.com/bliki/TestDrivenDevelopment.html">Test-Driven Development</a> - Martin Fowler</li>
  <li><a href="https://dannorth.net/introducing-bdd">Introducing BDD</a> - Dan North</li>
  <li><a href="https://dannorth.net/whats-in-a-story/">What’s in a Story?</a> - Dan North</li>
  <li><a href="https://dannorth.net/bdd-is-like-tdd-if/">BDD is like TDD if…</a> - Dan North</li>
  <li><a href="https://blog.jbrains.ca/permalink/how-test-driven-development-works-and-more">How Test-Driven Development Works</a> - J.B. Rainsberger</li>
  <li><a href="https://gojko.net/2020/03/17/sbe-10-years.html">SbE: 10 Years Later</a> - Gojko Adzic</li>
  <li><a href="https://refine.dev/blog/tdd-vs-bdd/">TDD vs BDD</a> - Refine</li>
  <li><a href="https://examplemapping.com/">Example Mapping</a></li>
</ul>

<hr />

<h2 id="example-story-customer-books-a-cleaning-appointment">Example Story: Customer Books a Cleaning Appointment</h2>

<p>This example combines <a href="#user-stories-and-acceptance-criteria">User Stories</a>,
<a href="#given-when-then-format">Given-When-Then</a>, and
<a href="#the-birth-of-bdd-2006">BDD</a> techniques into a complete specification.</p>

<p><strong>Story:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>As a Customer
I want to book a cleaning appointment online
So that I can schedule cleaning without calling
</code></pre></div></div>

<p><strong>Scenario 1: Available slot selected</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Given the customer has selected "Tuesday 10am"
 And Tuesday 10am is available
 And the customer has a valid payment method
When the customer confirms the booking
Then the system should create an appointment for Tuesday 10am
 And the system should charge a $25 deposit
 And the customer should receive a confirmation email
</code></pre></div></div>

<p><strong>Scenario 2: Slot is no longer available</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Given the customer has selected "Tuesday 10am"
 And Tuesday 10am was booked by another customer moments ago
When the customer confirms the booking
Then the system should not create an appointment
 And the system should say "This slot is no longer available"
 And the system should show the next 3 available slots
</code></pre></div></div>

<p><strong>Scenario 3: Payment method is declined</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Given the customer has selected "Tuesday 10am"
 And Tuesday 10am is available
 And the customer's card is declined
When the customer confirms the booking
Then the system should not create an appointment
 And the slot should remain available for others
 And the system should say "Payment could not be processed"
</code></pre></div></div>

<p><strong>Scenario 4: Customer is blocked</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Given the customer has 3 previous no-shows
When the customer attempts to book any slot
Then the system should not show available slots
 And the system should say "Please contact support to reactivate booking"
</code></pre></div></div>

<p>Each scenario tests a different constraint: availability (race condition),
payment processing, and account standing. The happy path comes first, followed
by edge cases that each isolate a single failure mode with verifiable outcomes.</p>]]></content><author><name>Robert Hopman</name><email>robert@bonaroo.nl</email></author><category term="quality" /><category term="tdd" /><category term="bdd" /><category term="xp" /><category term="testing" /><category term="agile" /><summary type="html"><![CDATA[From the 1968 software crisis to BDD: tracing the evolution of quality practices through structured programming, inspections, TDD, and specification by example.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://roberthopman.com/assets/images/default_technical_blog.png" /><media:content medium="image" url="https://roberthopman.com/assets/images/default_technical_blog.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Rule-Based OOP: Git, Methods, Classes</title><link href="https://roberthopman.com/oop-rules-for-git-methods-and-classes/" rel="alternate" type="text/html" title="Rule-Based OOP: Git, Methods, Classes" /><published>2026-01-10T00:00:00+00:00</published><updated>2026-01-10T00:00:00+00:00</updated><id>https://roberthopman.com/oop-rules-for-git-methods-and-classes</id><content type="html" xml:base="https://roberthopman.com/oop-rules-for-git-methods-and-classes/"><![CDATA[<p>This is a rule-based checklist for software codebases that follow object-oriented programming principles. Keep it simple and enforceable.</p>

<p>Git</p>

<ul>
  <li>Small and frequent commits using <a href="https://www.conventionalcommits.org/en/v1.0.0/">Conventional Commits</a></li>
</ul>

<p>Line of code:</p>

<ul>
  <li>Limit lines to 80 characters</li>
</ul>

<p>Method</p>

<ul>
  <li>Small methods: 5 lines of code max</li>
  <li>Limit method arguments</li>
  <li>Limit data types of method arguments</li>
  <li>Explicit return of data type</li>
  <li>Return same data type</li>
  <li>Explicit error handling</li>
  <li>Error messages are easy to understand</li>
</ul>

<p>Class</p>

<ul>
  <li>Limit amount of public methods</li>
  <li>Add methods in private methods</li>
  <li>Maximum class size is 100 lines of code</li>
</ul>]]></content><author><name>Robert Hopman</name><email>robert@bonaroo.nl</email></author><category term="oop" /><category term="rules" /><category term="git" /><category term="design" /><summary type="html"><![CDATA[A rule-based checklist for software codebases that follow object-oriented programming principles: commit hygiene, method constraints, and class boundaries.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://roberthopman.com/assets/images/default_technical_blog.png" /><media:content medium="image" url="https://roberthopman.com/assets/images/default_technical_blog.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Moving from GitLab to GitHub</title><link href="https://roberthopman.com/migrate-gitlab-to-github/" rel="alternate" type="text/html" title="Moving from GitLab to GitHub" /><published>2026-01-09T00:00:00+00:00</published><updated>2026-01-09T00:00:00+00:00</updated><id>https://roberthopman.com/migrate-gitlab-to-github</id><content type="html" xml:base="https://roberthopman.com/migrate-gitlab-to-github/"><![CDATA[<p>I’ve migrated from gitlab to github in 2021 which had some issues: <a href="https://github.com/piceaTech/node-gitlab-2-github/issues/99">https://github.com/piceaTech/node-gitlab-2-github/issues/99</a></p>

<p>Most important was maintaining the history, so that included:</p>

<ul>
  <li>user mapping</li>
  <li>commit mapping</li>
  <li>project mapping</li>
  <li>attachments</li>
  <li>issue information</li>
  <li>descriptions, labels, milestones, merge requests</li>
</ul>

<p>Tool used: <a href="https://github.com/piceaTech/node-gitlab-2-github">https://github.com/piceaTech/node-gitlab-2-github</a></p>

<p>Have fun.</p>]]></content><author><name>Robert Hopman</name><email>robert@bonaroo.nl</email></author><category term="git" /><category term="migration" /><category term="gitlab" /><category term="github" /><summary type="html"><![CDATA[Notes on migrating from GitLab to GitHub while maintaining history, including user mapping, commits, issues, and more.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://roberthopman.com/assets/images/default_technical_blog.png" /><media:content medium="image" url="https://roberthopman.com/assets/images/default_technical_blog.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Rails application boilerplates</title><link href="https://roberthopman.com/rails-application-boilerplates/" rel="alternate" type="text/html" title="Rails application boilerplates" /><published>2026-01-04T00:00:00+00:00</published><updated>2026-01-04T00:00:00+00:00</updated><id>https://roberthopman.com/rails-application-boilerplates</id><content type="html" xml:base="https://roberthopman.com/rails-application-boilerplates/"><![CDATA[<h2 id="open-source">Open source</h2>

<ul>
  <li>Feb 2026 — <a href="https://github.com/shakacode/react_on_rails">shakacode/react_on_rails</a></li>
  <li>Feb 2026 — <a href="https://github.com/alec-c4/kickstart">alec-c4/kickstart</a></li>
  <li>Feb 2026 — <a href="https://github.com/rootstrap/rails_api_base">rootstrap/rails_api_base</a></li>
  <li>Feb 2026 — <a href="https://github.com/bullet-train-co/bullet_train">bullet-train-co/bullet_train</a></li>
  <li>Feb 2026 — <a href="https://github.com/yshmarov/moneygun">yshmarov/moneygun</a></li>
  <li>Jan 2026 — <a href="https://github.com/ralixjs/rails-ralix-tailwind">ralixjs/rails-ralix-tailwind</a></li>
  <li>Jan 2026 — <a href="https://github.com/lewagon/rails-templates">lewagon/rails-templates</a></li>
  <li>Jan 2026 — <a href="https://github.com/mattbrictson/nextgen">mattbrictson/nextgen</a></li>
  <li>Jan 2026 — <a href="https://github.com/ackama/rails-template">ackama/rails-template</a></li>
  <li>Jan 2026 — <a href="https://github.com/newstler/template/tree/feature/upgrade-philosophy">newstler/template</a></li>
  <li>Jan 2026 — <a href="https://github.com/tarunvelli/rails-tabler-starter">tarunvelli/rails-tabler-starter</a></li>
  <li>Sep 2025 — <a href="https://github.com/yatish27/shore">yatish27/shore</a></li>
  <li>May 2025 — <a href="https://github.com/ryanckulp/speedrail">ryanckulp/speedrail</a></li>
  <li>Feb 2025 — <a href="https://github.com/excid3/gorails-app-template">excid3/gorails-app-template</a></li>
  <li>Feb 2024 — <a href="https://github.com/pch/rails-boilerplate">pch/rails-boilerplate</a></li>
  <li>Oct 2023 — <a href="https://github.com/hanwenzhang123/react-on-rails-boilerplate">hanwenzhang123/react-on-rails-boilerplate</a></li>
  <li>Sep 2023 — <a href="https://github.com/vinhmai570/rails_boilerplate">vinhmai570/rails_boilerplate</a></li>
  <li><a href="https://github.com/excid3/jumpstart">excid3/jumpstart</a></li>
  <li>Apr 2020 — <a href="https://github.com/zarinn3pal/rails6_boilerplate">zarinn3pal/rails6_boilerplate</a></li>
  <li>Jan 2019 — <a href="https://github.com/mdegis/rails_boilerplate">mdegis/rails_boilerplate</a></li>
</ul>

<h2 id="paid">Paid</h2>

<ul>
  <li><a href="https://jumpstartrails.com/">Jumpstart Rails</a></li>
  <li><a href="https://businessclasskit.com/">Business Class Kit</a></li>
  <li><a href="https://lightningrails.com/">Lightning Rails</a></li>
</ul>

<h2 id="common-features">Common features</h2>

<p>After analyzing multiple Rails boilerplates, these are the minimal features they all share:</p>

<table>
  <thead>
    <tr>
      <th>Feature</th>
      <th>Common Choice</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Authentication</td>
      <td>Devise</td>
      <td>Often with OmniAuth for social logins</td>
    </tr>
    <tr>
      <td>Authorization</td>
      <td>Pundit</td>
      <td>Role-based access control</td>
    </tr>
    <tr>
      <td>Testing</td>
      <td>RSpec</td>
      <td>With FactoryBot, system specs</td>
    </tr>
    <tr>
      <td>Code Quality</td>
      <td>RuboCop</td>
      <td>Plus ERB linting, Brakeman</td>
    </tr>
    <tr>
      <td>Background Jobs</td>
      <td>Sidekiq/Solid Queue</td>
      <td>Async processing</td>
    </tr>
    <tr>
      <td>CSS Framework</td>
      <td>Tailwind CSS</td>
      <td>Some use Bootstrap</td>
    </tr>
    <tr>
      <td>Security Scanning</td>
      <td>Brakeman</td>
      <td>Static analysis</td>
    </tr>
    <tr>
      <td>Deployment</td>
      <td>Multiple</td>
      <td>Render, Heroku, Fly.io, Kamal</td>
    </tr>
  </tbody>
</table>

<p>Most boilerplates also include:</p>
<ul>
  <li>Multi-tenancy (teams/organizations)</li>
  <li>Stripe payment integration</li>
  <li>GitHub Actions CI/CD</li>
  <li>Modern frontend (Hotwire/Turbo)</li>
</ul>]]></content><author><name>Robert Hopman</name><email>robert@bonaroo.nl</email></author><category term="ruby" /><category term="rails" /><summary type="html"><![CDATA[Open source]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://roberthopman.com/assets/images/default_technical_blog.png" /><media:content medium="image" url="https://roberthopman.com/assets/images/default_technical_blog.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Jupyter Notebooks with Ruby</title><link href="https://roberthopman.com/jupyter-notebooks-with-ruby/" rel="alternate" type="text/html" title="Jupyter Notebooks with Ruby" /><published>2025-12-30T00:00:00+00:00</published><updated>2025-12-30T00:00:00+00:00</updated><id>https://roberthopman.com/jupyter-notebooks-with-ruby</id><content type="html" xml:base="https://roberthopman.com/jupyter-notebooks-with-ruby/"><![CDATA[<p>This post documents how to set up Jupyter notebooks with Ruby support and embed them in blog posts.</p>

<h2 id="pre-conditions">Pre-conditions</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem <span class="nb">install </span>iruby
iruby register <span class="nt">--force</span>
brew <span class="nb">install </span>jupyter
</code></pre></div></div>

<p>Later, you can check the installed versions of IRuby with:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for </span>version <span class="k">in</span> <span class="si">$(</span>rbenv versions <span class="nt">--bare</span><span class="si">)</span><span class="p">;</span> <span class="k">do </span><span class="nb">echo</span> <span class="s2">"=== Ruby </span><span class="nv">$version</span><span class="s2"> ==="</span> <span class="o">&amp;&amp;</span> <span class="nv">RBENV_VERSION</span><span class="o">=</span><span class="nv">$version</span> gem list iruby 2&gt;/dev/null | <span class="nb">grep</span> <span class="nt">-E</span> <span class="s2">"^iruby"</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"iruby not installed"</span><span class="p">;</span> <span class="k">done</span>
</code></pre></div></div>

<p>Local usage:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>jupyter lab
</code></pre></div></div>

<p>Webbrowser usage: <a href="http://localhost:8888/lab">http://localhost:8888/lab</a></p>

<h2 id="workflow">Workflow</h2>

<ol>
  <li>
    <p>Create your notebook (<code class="language-plaintext highlighter-rouge">.ipynb</code> file) - either via Jupyter Lab or by writing the JSON directly</p>
  </li>
  <li>
    <p>Convert to HTML:</p>
  </li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/opt/homebrew/opt/jupyterlab/bin/jupyter nbconvert <span class="nt">--to</span> html notebooks/your-notebook.ipynb
</code></pre></div></div>

<ol>
  <li>
    <p>Extract the <code class="language-plaintext highlighter-rouge">&lt;main&gt;</code> content from the generated HTML</p>
  </li>
  <li>
    <p>Add the Jupyter CSS to your site’s stylesheet (see <code class="language-plaintext highlighter-rouge">assets/css/codeblock.css</code>)</p>
  </li>
  <li>
    <p>Embed the HTML directly in your blog post</p>
  </li>
</ol>

<h2 id="example">Example</h2>

<div class="jp-Notebook">
  <div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
    <div class="jp-Cell-inputWrapper">
      <div class="jp-InputArea jp-Cell-inputArea">
        <div class="jp-InputPrompt jp-InputArea-prompt"></div>
        <div class="jp-RenderedMarkdown">
          <h3>stdout</h3>
          <p><code>puts</code> writes to stdout:</p>
        </div>
      </div>
    </div>
  </div>
  <div class="jp-Cell jp-CodeCell jp-Notebook-cell">
    <div class="jp-Cell-inputWrapper">
      <div class="jp-InputArea jp-Cell-inputArea">
        <div class="jp-InputPrompt jp-InputArea-prompt">In [1]:</div>
        <div class="jp-InputArea-editor">
          <div class="highlight hl-ruby">
            <pre><span class="nb">puts</span> <span class="s2">"This is stdout"</span></pre>
          </div>
        </div>
      </div>
    </div>
    <div class="jp-Cell-outputWrapper">
      <div class="jp-OutputArea jp-Cell-outputArea">
        <div class="jp-OutputArea-child">
          <div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
          <div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
            <pre>This is stdout</pre>
          </div>
        </div>
      </div>
    </div>
  </div>
  <div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
    <div class="jp-Cell-inputWrapper">
      <div class="jp-InputArea jp-Cell-inputArea">
        <div class="jp-InputPrompt jp-InputArea-prompt"></div>
        <div class="jp-RenderedMarkdown">
          <h3>stderr</h3>
          <p>Two ways to write to stderr:</p>
        </div>
      </div>
    </div>
  </div>
  <div class="jp-Cell jp-CodeCell jp-Notebook-cell">
    <div class="jp-Cell-inputWrapper">
      <div class="jp-InputArea jp-Cell-inputArea">
        <div class="jp-InputPrompt jp-InputArea-prompt">In [2]:</div>
        <div class="jp-InputArea-editor">
          <div class="highlight hl-ruby">
            <pre><span class="nb">warn</span> <span class="s2">"Using warn"</span></pre>
          </div>
        </div>
      </div>
    </div>
    <div class="jp-Cell-outputWrapper">
      <div class="jp-OutputArea jp-Cell-outputArea">
        <div class="jp-OutputArea-child">
          <div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
          <div class="jp-RenderedText jp-OutputArea-output" data-mime-type="application/vnd.jupyter.stderr">
            <pre>Using warn</pre>
          </div>
        </div>
      </div>
    </div>
  </div>
  <div class="jp-Cell jp-CodeCell jp-Notebook-cell">
    <div class="jp-Cell-inputWrapper">
      <div class="jp-InputArea jp-Cell-inputArea">
        <div class="jp-InputPrompt jp-InputArea-prompt">In [3]:</div>
        <div class="jp-InputArea-editor">
          <div class="highlight hl-ruby">
            <pre><span class="vg">$stderr</span><span class="o">.</span><span class="n">puts</span> <span class="s2">"Using $stderr.puts"</span></pre>
          </div>
        </div>
      </div>
    </div>
    <div class="jp-Cell-outputWrapper">
      <div class="jp-OutputArea jp-Cell-outputArea">
        <div class="jp-OutputArea-child">
          <div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
          <div class="jp-RenderedText jp-OutputArea-output" data-mime-type="application/vnd.jupyter.stderr">
            <pre>Using $stderr.puts</pre>
          </div>
        </div>
      </div>
    </div>
  </div>
  <div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
    <div class="jp-Cell-inputWrapper">
      <div class="jp-InputArea jp-Cell-inputArea">
        <div class="jp-InputPrompt jp-InputArea-prompt"></div>
        <div class="jp-RenderedMarkdown">
          <h3>Return value</h3>
          <p>The last expression's value shows with Out label:</p>
        </div>
      </div>
    </div>
  </div>
  <div class="jp-Cell jp-CodeCell jp-Notebook-cell">
    <div class="jp-Cell-inputWrapper">
      <div class="jp-InputArea jp-Cell-inputArea">
        <div class="jp-InputPrompt jp-InputArea-prompt">In [4]:</div>
        <div class="jp-InputArea-editor">
          <div class="highlight hl-ruby">
            <pre><span class="s2">"This is a return value"</span></pre>
          </div>
        </div>
      </div>
    </div>
    <div class="jp-Cell-outputWrapper">
      <div class="jp-OutputArea jp-Cell-outputArea">
        <div class="jp-OutputArea-child">
          <div class="jp-OutputPrompt jp-OutputArea-prompt">Out[4]:</div>
          <div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
            <pre>"This is a return value"</pre>
          </div>
        </div>
      </div>
    </div>
  </div>
  <div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
    <div class="jp-Cell-inputWrapper">
      <div class="jp-InputArea jp-Cell-inputArea">
        <div class="jp-InputPrompt jp-InputArea-prompt"></div>
        <div class="jp-RenderedMarkdown">
          <h3>All three together</h3>
          <p>Output order: stdout, stderr, then return value:</p>
        </div>
      </div>
    </div>
  </div>
  <div class="jp-Cell jp-CodeCell jp-Notebook-cell">
    <div class="jp-Cell-inputWrapper">
      <div class="jp-InputArea jp-Cell-inputArea">
        <div class="jp-InputPrompt jp-InputArea-prompt">In [5]:</div>
        <div class="jp-InputArea-editor">
          <div class="highlight hl-ruby">
            <pre><span class="nb">puts</span> <span class="s2">"1. stdout via puts"</span>
<span class="nb">warn</span> <span class="s2">"2. stderr via warn"</span>
<span class="vg">$stderr</span><span class="o">.</span><span class="n">puts</span> <span class="s2">"3. stderr via $stderr.puts"</span>
<span class="s2">"4. return value"</span></pre>
          </div>
        </div>
      </div>
    </div>
    <div class="jp-Cell-outputWrapper">
      <div class="jp-OutputArea jp-Cell-outputArea">
        <div class="jp-OutputArea-child">
          <div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
          <div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
            <pre>1. stdout via puts</pre>
          </div>
        </div>
        <div class="jp-OutputArea-child">
          <div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
          <div class="jp-RenderedText jp-OutputArea-output" data-mime-type="application/vnd.jupyter.stderr">
            <pre>2. stderr via warn
3. stderr via $stderr.puts</pre>
          </div>
        </div>
        <div class="jp-OutputArea-child">
          <div class="jp-OutputPrompt jp-OutputArea-prompt">Out[5]:</div>
          <div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
            <pre>"4. return value"</pre>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>]]></content><author><name>Robert Hopman</name><email>robert@bonaroo.nl</email></author><category term="ruby" /><category term="jupyter" /><category term="tutorial" /><category term="workflow" /><summary type="html"><![CDATA[How to set up Jupyter notebooks with Ruby using IRuby, and embed them in blog posts.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://roberthopman.com/assets/images/default_technical_blog.png" /><media:content medium="image" url="https://roberthopman.com/assets/images/default_technical_blog.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Search in a repo</title><link href="https://roberthopman.com/search-in-directory/" rel="alternate" type="text/html" title="Search in a repo" /><published>2025-12-07T00:00:00+00:00</published><updated>2025-12-07T00:00:00+00:00</updated><id>https://roberthopman.com/search-in-directory</id><content type="html" xml:base="https://roberthopman.com/search-in-directory/"><![CDATA[<p>You can search for a word in a repo using several command line tools.</p>

<h3 id="grep">grep</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Searches in a single file only</span>
<span class="nb">grep</span> <span class="s2">"search_term"</span> filename

<span class="c"># Recursive search, search in the directory and all files in it and its subdirectories</span>
<span class="nb">grep</span> <span class="nt">-r</span> <span class="s2">"search_term"</span> directory

<span class="c"># Case insensitive</span>
<span class="nb">grep</span> <span class="nt">-ri</span> <span class="s2">"search_term"</span> directory

<span class="c"># Show line numbers</span>
<span class="nb">grep</span> <span class="nt">-rn</span> <span class="s2">"search_term"</span> directory
</code></pre></div></div>

<h3 id="git-grep-tracked-files-only">git grep (tracked files only)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git <span class="nb">grep</span> <span class="s2">"search_term"</span>
</code></pre></div></div>

<h3 id="find-by-filename">find (by filename)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Find files by name</span>
find <span class="nb">.</span> <span class="nt">-name</span> <span class="s2">"*.rb"</span>

<span class="c"># Find files containing text in name</span>
find <span class="nb">.</span> <span class="nt">-name</span> <span class="s2">"*controller*"</span>
</code></pre></div></div>

<h3 id="ripgrep-rg">ripgrep (rg)</h3>

<p>Ripgrep is a faster grep tool.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Search for a word</span>
rg <span class="s2">"search_term"</span>

<span class="c"># Case insensitive</span>
rg <span class="nt">-i</span> <span class="s2">"search_term"</span>

<span class="c"># Show line numbers</span>
rg <span class="nt">-n</span> <span class="s2">"search_term"</span>

<span class="c"># Show line numbers and context</span>
rg <span class="nt">-C</span> 3 <span class="s2">"search_term"</span>
</code></pre></div></div>

<h3 id="list-files">list files</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># list files</span>
<span class="nb">ls</span>

<span class="c"># long listing of files and directories</span>
ll

<span class="c"># show the alias for ll</span>
<span class="nb">type</span> <span class="nt">-a</span> ll   
<span class="o">=&gt;</span> ll is an <span class="nb">alias </span><span class="k">for </span><span class="nb">ls</span> <span class="nt">-lh</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># list all files and directories, typically an alias for ls -a</span>
la
<span class="c"># show the alias for la</span>
<span class="nb">type</span> <span class="nt">-a</span> la   
la is an <span class="nb">alias </span><span class="k">for </span><span class="nb">ls</span> <span class="nt">-lAh</span>
</code></pre></div></div>

<h3 id="bash--zsh">bash &amp; zsh</h3>

<p>Further reading on Bourne-Again SHell and Z Shell.</p>
<ul>
  <li><a href="https://en.wikipedia.org/wiki/Bourne-Again_Shell">Bourne-Again SHell</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Z_shell">Z Shell</a></li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># show the man page for ls</span>
man <span class="nb">ls</span>

<span class="c"># show the man page for man</span>
man man

<span class="c"># show the man page for bash</span>
man bash

<span class="c"># show the man page for zsh</span>
man zsh
</code></pre></div></div>

<p>That’s it for now.</p>]]></content><author><name>Robert Hopman</name><email>robert@bonaroo.nl</email></author><category term="terminal" /><category term="grep" /><category term="git grep" /><category term="ripgrep" /><category term="find" /><summary type="html"><![CDATA[Quick reference for searching files and content in a directory using grep, git grep, ripgrep and find.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://roberthopman.com/assets/images/default_technical_blog.png" /><media:content medium="image" url="https://roberthopman.com/assets/images/default_technical_blog.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Multi-Step Forms (Wizards) in Rails</title><link href="https://roberthopman.com/multi-step-forms-wizards-in-rails/" rel="alternate" type="text/html" title="Multi-Step Forms (Wizards) in Rails" /><published>2025-12-05T00:00:00+00:00</published><updated>2025-12-05T00:00:00+00:00</updated><id>https://roberthopman.com/multi-step-forms-wizards-in-rails</id><content type="html" xml:base="https://roberthopman.com/multi-step-forms-wizards-in-rails/"><![CDATA[<h2 id="what-is-a-multi-step-form-wizard-and-why-do-we-need-it">What is a multi-step form (wizard) and why do we need it?</h2>

<p>Starting with a summary of <a href="https://jonsully.net/blog/rails-wizards-part-one">Rails Wizards Part 1-5</a> by Jon Sullivan. This is a great introduction to multi-step forms in Rails. Then we’ll look at different approaches to implement them in Rails.</p>

<p>Many apps have multiple steps to complete a task:</p>
<ul>
  <li>Onboarding / signup</li>
  <li>Filling out tax forms</li>
  <li>Insurance claims</li>
  <li>Invoice generation</li>
  <li>Mortgage/loan applications</li>
</ul>

<p>Multi-step forms (wizards) make these flows manageable.</p>

<p><strong>Scope:</strong></p>
<ul>
  <li>Server side information management</li>
  <li>Focus on saving information to a single model or database table</li>
  <li>Each step fills out some subset of attributes on that model</li>
</ul>

<p><strong>Acceptance Criteria:</strong></p>
<ul>
  <li>DRY code for maintainability</li>
  <li>Each step usable via browser and API endpoint with correct feedback</li>
  <li>User experience considerations:
    <ul>
      <li>Can the user stop and resume later?</li>
      <li>Can the user work through two instances in different tabs?</li>
      <li>Can the user jump between steps?</li>
    </ul>
  </li>
</ul>

<h2 id="data-persistence-strategies">Data persistence strategies</h2>

<p>Three options:</p>
<ol>
  <li><strong>Session persistence</strong> - Generally avoid this</li>
  <li><strong>Database persistence</strong> - Use when wizard progress is part of your domain model</li>
  <li><strong>Cache persistence</strong> - Use when wizard progress is not part of your domain model</li>
</ol>

<h2 id="url-strategies">URL strategies</h2>

<p><strong>ID/Key-in-URL:</strong></p>

<p>Database:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">object</span><span class="o">/</span><span class="ss">:id</span><span class="o">/</span><span class="n">steps</span><span class="o">/</span><span class="ss">:step_name</span>
</code></pre></div></div>

<p>Cache:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">object</span><span class="o">/</span><span class="n">p1rsn93</span><span class="o">/</span><span class="n">steps</span><span class="o">/</span><span class="ss">:step_name</span>
</code></pre></div></div>

<p><strong>ID/Key-in-Session:</strong></p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">object</span><span class="o">/</span><span class="n">wizard</span><span class="o">/</span><span class="n">steps</span><span class="o">/</span><span class="ss">:step_name</span>
</code></pre></div></div>

<h2 id="model-validations">Model validations</h2>

<p>The goal is to validate each step against its subset of fields while maintaining capability to validate the whole object.</p>

<p>Example scaffold:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rails</span> <span class="n">g</span> <span class="n">scaffold</span> <span class="no">House</span> <span class="n">address</span> <span class="n">exterior_color</span> <span class="n">interior_color</span> <span class="n">current_family_last_name</span> <span class="n">rooms</span><span class="ss">:integer</span> <span class="n">square_feet</span><span class="ss">:integer</span>
</code></pre></div></div>

<p>The model uses an enum to define form steps and context-specific validators: <code class="language-plaintext highlighter-rouge">required_for_step?</code> is a helper method to determine if the current step is required based on the form step. <code class="language-plaintext highlighter-rouge">with_options</code> is a Rails helper method to group validations by context.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/models/house.rb</span>
<span class="k">class</span> <span class="nc">House</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">enum</span> <span class="ss">form_steps: </span><span class="p">{</span>
    <span class="ss">address_info: </span><span class="p">[</span><span class="ss">:address</span><span class="p">,</span> <span class="ss">:current_family_last_name</span><span class="p">],</span>
    <span class="ss">house_info: </span><span class="p">[</span><span class="ss">:interior_color</span><span class="p">,</span> <span class="ss">:exterior_color</span><span class="p">],</span>
    <span class="ss">house_stats: </span><span class="p">[</span><span class="ss">:rooms</span><span class="p">,</span> <span class="ss">:square_feet</span><span class="p">]</span>
  <span class="p">}</span>

  <span class="nb">attr_accessor</span> <span class="ss">:form_step</span>

  <span class="n">with_options</span> <span class="ss">if: </span><span class="o">-&gt;</span> <span class="p">{</span> <span class="n">required_for_step?</span><span class="p">(</span><span class="ss">:address_info</span><span class="p">)</span> <span class="p">}</span> <span class="k">do</span>
    <span class="n">validates</span> <span class="ss">:address</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">minimum: </span><span class="mi">10</span><span class="p">,</span> <span class="ss">maximum: </span><span class="mi">50</span> <span class="p">}</span>
    <span class="n">validates</span> <span class="ss">:current_family_last_name</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">minimum: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">maximum: </span><span class="mi">30</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="n">with_options</span> <span class="ss">if: </span><span class="o">-&gt;</span> <span class="p">{</span> <span class="n">required_for_step?</span><span class="p">(</span><span class="ss">:house_info</span><span class="p">)</span> <span class="p">}</span> <span class="k">do</span>
    <span class="n">validates</span> <span class="ss">:interior_color</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
    <span class="n">validates</span> <span class="ss">:exterior_color</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
  <span class="k">end</span>

  <span class="n">with_options</span> <span class="ss">if: </span><span class="o">-&gt;</span> <span class="p">{</span> <span class="n">required_for_step?</span><span class="p">(</span><span class="ss">:house_stats</span><span class="p">)</span> <span class="p">}</span> <span class="k">do</span>
    <span class="n">validates</span> <span class="ss">:rooms</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">numericality: </span><span class="p">{</span> <span class="ss">gt: </span><span class="mi">1</span> <span class="p">}</span>
    <span class="n">validates</span> <span class="ss">:square_feet</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">required_for_step?</span><span class="p">(</span><span class="n">step</span><span class="p">)</span>
    <span class="k">return</span> <span class="kp">true</span> <span class="k">if</span> <span class="n">form_step</span><span class="p">.</span><span class="nf">nil?</span>

    <span class="n">ordered_keys</span> <span class="o">=</span> <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">form_steps</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:to_sym</span><span class="p">)</span>
    <span class="n">ordered_keys</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="n">step</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="n">ordered_keys</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="n">form_step</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="routes-and-controllers">Routes and controllers</h2>

<p>Use a separate controller per wizard (e.g., <code class="language-plaintext highlighter-rouge">app/controllers/steps_controllers/house_steps_controller.rb</code>). The examples below use the <a href="https://github.com/zombocom/wicked">wicked gem</a>.</p>

<h3 id="1-database-persistence--in-url-routing">1. Database persistence + in-URL routing</h3>

<p>Routes:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">resources</span> <span class="ss">:houses</span> <span class="k">do</span>
  <span class="n">resources</span> <span class="ss">:steps</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:show</span><span class="p">,</span> <span class="ss">:update</span><span class="p">],</span> <span class="ss">controller: </span><span class="s1">'steps_controllers/house_steps'</span>
<span class="k">end</span>
<span class="c1"># URLs: /houses/1/steps/address_info</span>
</code></pre></div></div>

<p>Houses controller:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">HousesController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">new</span>
    <span class="vi">@house</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">new</span>
    <span class="vi">@house</span><span class="p">.</span><span class="nf">save!</span><span class="p">(</span><span class="ss">validate: </span><span class="kp">false</span><span class="p">)</span>
    <span class="n">redirect_to</span> <span class="n">house_step_path</span><span class="p">(</span><span class="vi">@house</span><span class="p">,</span> <span class="no">House</span><span class="p">.</span><span class="nf">form_steps</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">first</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Steps controller:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">StepsControllers</span>
  <span class="k">class</span> <span class="nc">HouseStepsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
    <span class="kp">include</span> <span class="no">Wicked</span><span class="o">::</span><span class="no">Wizard</span>

    <span class="n">steps</span> <span class="o">*</span><span class="no">House</span><span class="p">.</span><span class="nf">form_steps</span><span class="p">.</span><span class="nf">keys</span>

    <span class="k">def</span> <span class="nf">show</span>
      <span class="vi">@house</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:house_id</span><span class="p">])</span>
      <span class="n">render_wizard</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">update</span>
      <span class="vi">@house</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:house_id</span><span class="p">])</span>
      <span class="vi">@house</span><span class="p">.</span><span class="nf">assign_attributes</span><span class="p">(</span><span class="n">house_params</span><span class="p">)</span>
      <span class="n">render_wizard</span> <span class="vi">@house</span>
    <span class="k">end</span>

    <span class="kp">private</span>

    <span class="k">def</span> <span class="nf">house_params</span>
      <span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:house</span><span class="p">).</span><span class="nf">permit</span><span class="p">(</span><span class="no">House</span><span class="p">.</span><span class="nf">form_steps</span><span class="p">[</span><span class="n">step</span><span class="p">]).</span><span class="nf">merge</span><span class="p">(</span><span class="ss">form_step: </span><span class="n">step</span><span class="p">.</span><span class="nf">to_sym</span><span class="p">)</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">finish_wizard_path</span>
      <span class="n">house_path</span><span class="p">(</span><span class="vi">@house</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>View template:</p>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- app/views/steps_controllers/house_steps/address_info.html.erb --&gt;</span>

<span class="cp">&lt;%=</span> <span class="n">form_with</span> <span class="ss">model: </span><span class="vi">@house</span><span class="p">,</span> <span class="ss">url: </span><span class="n">wizard_path</span> <span class="k">do</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="cp">%&gt;</span>
  <span class="cp">&lt;%</span> <span class="k">if</span> <span class="n">f</span><span class="p">.</span><span class="nf">object</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">any?</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"error_messages"</span><span class="nt">&gt;</span>
      <span class="cp">&lt;%</span> <span class="n">f</span><span class="p">.</span><span class="nf">object</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">full_messages</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">error</span><span class="o">|</span> <span class="cp">%&gt;</span>
        <span class="nt">&lt;p&gt;</span><span class="cp">&lt;%=</span> <span class="n">error</span> <span class="cp">%&gt;</span><span class="nt">&lt;/p&gt;</span>
      <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
  <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>

  <span class="nt">&lt;fieldset&gt;</span>
    <span class="nt">&lt;legend&gt;</span>Address Info<span class="nt">&lt;/legend&gt;</span>

    <span class="nt">&lt;div&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:address</span> <span class="cp">%&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:address</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;/div&gt;</span>

    <span class="nt">&lt;div&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:current_family_last_name</span> <span class="cp">%&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:current_family_last_name</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;/div&gt;</span>

    <span class="nt">&lt;div&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">submit</span> <span class="s1">'Next Step'</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
  <span class="nt">&lt;/fieldset&gt;</span>
<span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">.permit(House.form_steps[step])</code> trick ensures users can’t inject attributes not meant for the current step.</p>

<h3 id="2-database-persistence--in-session-routing">2. Database persistence + in-session routing</h3>

<p>For prettier URLs:</p>

<p>Routes:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">resources</span> <span class="ss">:houses</span>
<span class="n">resources</span> <span class="ss">:build_house</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:show</span><span class="p">,</span> <span class="ss">:update</span><span class="p">],</span> <span class="ss">controller: </span><span class="s1">'steps_controllers/house_steps'</span>
<span class="c1"># URLs: /build_house/address_info</span>
</code></pre></div></div>

<p>Houses controller:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">HousesController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">new</span>
    <span class="k">unless</span> <span class="n">house_id</span> <span class="o">=</span> <span class="n">session</span><span class="p">[</span><span class="ss">:house_id</span><span class="p">]</span>
      <span class="vi">@house</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">new</span>
      <span class="vi">@house</span><span class="p">.</span><span class="nf">save!</span><span class="p">(</span><span class="ss">validate: </span><span class="kp">false</span><span class="p">)</span>
      <span class="n">session</span><span class="p">[</span><span class="ss">:house_id</span><span class="p">]</span> <span class="o">=</span> <span class="vi">@house</span><span class="p">.</span><span class="nf">id</span>
    <span class="k">end</span>
    <span class="n">redirect_to</span> <span class="n">build_house_path</span><span class="p">(</span><span class="no">House</span><span class="p">.</span><span class="nf">form_steps</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">first</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Steps controller:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">StepsControllers</span>
  <span class="k">class</span> <span class="nc">HouseStepsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
    <span class="kp">include</span> <span class="no">Wicked</span><span class="o">::</span><span class="no">Wizard</span>

    <span class="n">steps</span> <span class="o">*</span><span class="no">House</span><span class="p">.</span><span class="nf">form_steps</span><span class="p">.</span><span class="nf">keys</span>

    <span class="k">def</span> <span class="nf">show</span>
      <span class="vi">@house</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">session</span><span class="p">[</span><span class="ss">:house_id</span><span class="p">])</span>
      <span class="n">render_wizard</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">update</span>
      <span class="vi">@house</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">session</span><span class="p">[</span><span class="ss">:house_id</span><span class="p">])</span>
      <span class="vi">@house</span><span class="p">.</span><span class="nf">assign_attributes</span><span class="p">(</span><span class="n">house_params</span><span class="p">)</span>
      <span class="n">render_wizard</span> <span class="vi">@house</span>
    <span class="k">end</span>

    <span class="kp">private</span>

    <span class="k">def</span> <span class="nf">house_params</span>
      <span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:house</span><span class="p">).</span><span class="nf">permit</span><span class="p">(</span><span class="no">House</span><span class="p">.</span><span class="nf">form_steps</span><span class="p">[</span><span class="n">step</span><span class="p">]).</span><span class="nf">merge</span><span class="p">(</span><span class="ss">form_step: </span><span class="n">step</span><span class="p">.</span><span class="nf">to_sym</span><span class="p">)</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">finish_wizard_path</span>
      <span class="n">house_path</span><span class="p">(</span><span class="vi">@house</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="3-cache-persistence--in-url-routing">3. Cache persistence + in-URL routing</h3>

<p>Note: Enable Rails cache for local development with <code class="language-plaintext highlighter-rouge">rails dev:cache</code>.</p>

<p>Routes:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">resources</span> <span class="ss">:houses</span>
<span class="n">resources</span> <span class="ss">:build_house</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[]</span> <span class="k">do</span>
  <span class="n">resources</span> <span class="ss">:step</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:update</span><span class="p">,</span> <span class="ss">:show</span><span class="p">],</span> <span class="ss">controller: </span><span class="s1">'steps_controllers/house_steps'</span>
<span class="k">end</span>
<span class="c1"># URLs: /build_house/abc-xyz/steps/address_info</span>
</code></pre></div></div>

<p>Houses controller:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">HousesController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">new</span>
    <span class="n">house_builder_key</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">urlsafe_base64</span><span class="p">(</span><span class="mi">6</span><span class="p">)</span>
    <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="n">house_builder_key</span><span class="p">)</span> <span class="p">{</span> <span class="no">Hash</span><span class="p">.</span><span class="nf">new</span> <span class="p">}</span>
    <span class="n">redirect_to</span> <span class="n">build_house_step_path</span><span class="p">(</span><span class="n">house_builder_key</span><span class="p">,</span> <span class="no">House</span><span class="p">.</span><span class="nf">form_steps</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">first</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>For <code class="language-plaintext highlighter-rouge">Rails.cache.fetch(house_builder_key) { Hash.new }</code>: If the key exists in the cache, it returns the stored value. If the key doesn’t exist in the cache, it executes the block { Hash.new } which creates a new empty hash, stores it in the cache with the given key, and returns it. So fetch either returns the stored value or the new empty hash.</p>

<p>Steps controller:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">StepsControllers</span>
  <span class="k">class</span> <span class="nc">HouseStepsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
    <span class="kp">include</span> <span class="no">Wicked</span><span class="o">::</span><span class="no">Wizard</span>

    <span class="n">steps</span> <span class="o">*</span><span class="no">House</span><span class="p">.</span><span class="nf">form_steps</span><span class="p">.</span><span class="nf">keys</span>

    <span class="k">def</span> <span class="nf">show</span>
      <span class="c1"># If the key exists in the cache, it returns the stored value. If the key doesn't exist in the cache, it returns nil</span>
      <span class="n">house_attrs</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:build_house_id</span><span class="p">])</span>
      <span class="vi">@house</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">house_attrs</span><span class="p">)</span>
      <span class="n">render_wizard</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">update</span>
      <span class="c1"># It reads the cache, merges the house_params, and writes the result back to the cache</span>
      <span class="n">house_attrs</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:build_house_id</span><span class="p">]).</span><span class="nf">merge</span><span class="p">(</span><span class="n">house_params</span><span class="p">)</span>
      <span class="vi">@house</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">house_attrs</span><span class="p">)</span>

      <span class="k">if</span> <span class="vi">@house</span><span class="p">.</span><span class="nf">valid?</span>
        <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:build_house_id</span><span class="p">],</span> <span class="n">house_attrs</span><span class="p">)</span>
        <span class="n">redirect_to_next</span> <span class="n">next_step</span>
      <span class="k">else</span>
        <span class="n">render_wizard</span>
      <span class="k">end</span>
    <span class="k">end</span>

    <span class="kp">private</span>

    <span class="c1"># Only allow the params for specific attributes allowed in this step. </span>
    <span class="k">def</span> <span class="nf">house_params</span>
      <span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:house</span><span class="p">).</span><span class="nf">permit</span><span class="p">(</span><span class="no">House</span><span class="p">.</span><span class="nf">form_steps</span><span class="p">[</span><span class="n">step</span><span class="p">]).</span><span class="nf">merge</span><span class="p">(</span><span class="ss">form_step: </span><span class="n">step</span><span class="p">.</span><span class="nf">to_sym</span><span class="p">)</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">finish_wizard_path</span>
      <span class="n">house_attrs</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:build_house_id</span><span class="p">])</span>
      <span class="vi">@house</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">new</span> <span class="n">house_attrs</span>
      <span class="c1"># end of the wizard we save the house to the database</span>
      <span class="vi">@house</span><span class="p">.</span><span class="nf">save!</span>
      <span class="c1"># delete the cache</span>
      <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">delete</span> <span class="n">params</span><span class="p">[</span><span class="ss">:build_house_id</span><span class="p">]</span>
      <span class="c1"># redirect to the house path</span>
      <span class="n">house_path</span><span class="p">(</span><span class="vi">@house</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>In our cache-based wizard:</p>

<ul>
  <li>We’re creating a new House object in each step with House.new(house_attrs)</li>
  <li>This object is not persisted to the database until the very end of the wizard</li>
  <li>Therefore, form_with would default to POST, which is incorrect for our wizard flow</li>
</ul>

<p>For cache-based wizards, explicitly set the form method to PATCH since the object isn’t persisted until the very end of the wizard:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;%=</span> <span class="n">form_with</span> <span class="ss">model: </span><span class="vi">@house</span><span class="p">,</span> <span class="ss">url: </span><span class="n">wizard_path</span><span class="p">,</span> <span class="ss">method: :patch</span> <span class="k">do</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="cp">%&gt;</span>
  <span class="c">&lt;%# form fields... %&gt;</span>
<span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
</code></pre></div></div>

<h3 id="4-cache-persistence--in-session-routing">4. Cache persistence + in-session routing</h3>

<p>Routes:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">resources</span> <span class="ss">:houses</span>
<span class="n">resources</span> <span class="ss">:build_house</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:update</span><span class="p">,</span> <span class="ss">:show</span><span class="p">],</span> <span class="ss">controller: </span><span class="s1">'steps_controllers/house_steps'</span>
<span class="c1"># URLs: /build_house/address_info</span>
</code></pre></div></div>

<p>Houses controller:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">HousesController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">new</span>
    <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="n">session</span><span class="p">.</span><span class="nf">id</span><span class="p">)</span> <span class="p">{</span> <span class="no">Hash</span><span class="p">.</span><span class="nf">new</span> <span class="p">}</span>
    <span class="n">redirect_to</span> <span class="n">build_house_path</span><span class="p">(</span><span class="no">House</span><span class="p">.</span><span class="nf">form_steps</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">first</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Steps controller:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">StepsControllers</span>
  <span class="k">class</span> <span class="nc">HouseStepsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
    <span class="kp">include</span> <span class="no">Wicked</span><span class="o">::</span><span class="no">Wizard</span>

    <span class="n">steps</span> <span class="o">*</span><span class="no">House</span><span class="p">.</span><span class="nf">form_steps</span><span class="p">.</span><span class="nf">keys</span>

    <span class="k">def</span> <span class="nf">show</span>
      <span class="n">house_attrs</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="n">session</span><span class="p">.</span><span class="nf">id</span><span class="p">)</span>
      <span class="vi">@house</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">house_attrs</span><span class="p">)</span>
      <span class="n">render_wizard</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">update</span>
      <span class="n">house_attrs</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="n">session</span><span class="p">.</span><span class="nf">id</span><span class="p">).</span><span class="nf">merge</span><span class="p">(</span><span class="n">house_params</span><span class="p">)</span>
      <span class="vi">@house</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">house_attrs</span><span class="p">)</span>

      <span class="k">if</span> <span class="vi">@house</span><span class="p">.</span><span class="nf">valid?</span>
        <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">session</span><span class="p">.</span><span class="nf">id</span><span class="p">,</span> <span class="n">house_attrs</span><span class="p">)</span>
        <span class="n">redirect_to_next</span> <span class="n">next_step</span>
      <span class="k">else</span>
        <span class="n">render_wizard</span>
      <span class="k">end</span>
    <span class="k">end</span>

    <span class="kp">private</span>

    <span class="k">def</span> <span class="nf">house_params</span>
      <span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:house</span><span class="p">).</span><span class="nf">permit</span><span class="p">(</span><span class="no">House</span><span class="p">.</span><span class="nf">form_steps</span><span class="p">[</span><span class="n">step</span><span class="p">]).</span><span class="nf">merge</span><span class="p">(</span><span class="ss">form_step: </span><span class="n">step</span><span class="p">.</span><span class="nf">to_sym</span><span class="p">)</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">finish_wizard_path</span>
      <span class="n">house_attrs</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="n">session</span><span class="p">.</span><span class="nf">id</span><span class="p">)</span>
      <span class="vi">@house</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">house_attrs</span><span class="p">)</span>
      <span class="vi">@house</span><span class="p">.</span><span class="nf">save!</span>
      <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="n">session</span><span class="p">.</span><span class="nf">id</span><span class="p">)</span>
      <span class="n">house_path</span><span class="p">(</span><span class="vi">@house</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>For the full series with more details, visit <a href="https://jonsully.net/blog/rails-wizards-part-one">jonsully.net/blog/rails-wizards-part-one</a>.</p>

<h2 id="the-implementation-of-cache-persistence-without-the-wicked-gem">The implementation of cache persistence without the Wicked gem</h2>

<p>Then an implementation using cache persistence + in-URL routing without the Wicked gem. This approach gives you full control over the wizard flow.</p>

<p>Source: <a href="https://github.com/roberthopman/multi-step-form/commit/49a365c48c95a6d62e8b9228928eb81f4b90847b">multi-step-form commit 49a365c</a></p>

<h3 id="sequence-diagram">Sequence diagram</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌───────┐          ┌──────────┐          ┌───────┐          ┌──────┐
│Browser│          │Controller│          │ Cache │          │Model │
└───┬───┘          └────┬─────┘          └───┬───┘          └──┬───┘
    │                   │                    │                 │
    │ GET /houses/new   │                    │                 │
    │──────────────────&gt;│                    │                 │
    │                   │                    │                 │
    │                   │ generate cache_key │                 │
    │                   │ write({})          │                 │
    │                   │───────────────────&gt;│                 │
    │                   │                    │                 │
    │  redirect /build_house/:key/steps/address_info           │
    │&lt;──────────────────│                    │                 │
    │                   │                    │                 │
    │ GET /build_house/:key/steps/address_info                 │
    │──────────────────&gt;│                    │                 │
    │                   │                    │                 │
    │                   │ read(cache_key)    │                 │
    │                   │───────────────────&gt;│                 │
    │                   │   house_attrs      │                 │
    │                   │&lt;───────────────────│                 │
    │                   │                    │                 │
    │                   │ House.new(attrs)   │                 │
    │                   │─────────────────────────────────────&gt;│
    │                   │      @house        │                 │
    │                   │&lt;─────────────────────────────────────│
    │                   │                    │                 │
    │   render show     │                    │                 │
    │&lt;──────────────────│                    │                 │
    │                   │                    │                 │
    │ PATCH (form data) │                    │                 │
    │──────────────────&gt;│                    │                 │
    │                   │                    │                 │
    │                   │ read(cache_key)    │                 │
    │                   │───────────────────&gt;│                 │
    │                   │   house_attrs      │                 │
    │                   │&lt;───────────────────│                 │
    │                   │                    │                 │
    │                   │ merge(params)      │                 │
    │                   │ House.new(attrs)   │                 │
    │                   │─────────────────────────────────────&gt;│
    │                   │      @house        │                 │
    │                   │&lt;─────────────────────────────────────│
    │                   │                    │                 │
    │                   │ @house.valid?      │                 │
    │                   │─────────────────────────────────────&gt;│
    │                   │      true          │                 │
    │                   │&lt;─────────────────────────────────────│
    │                   │                    │                 │
    │                   │ write(key, attrs)  │                 │
    │                   │───────────────────&gt;│                 │
    │                   │                    │                 │
    │                   │ [if final_step?]   │                 │
    │                   │ @house.save        │                 │
    │                   │─────────────────────────────────────&gt;│
    │                   │                    │                 │
    │                   │ delete(cache_key)  │                 │
    │                   │───────────────────&gt;│                 │
    │                   │                    │                 │
    │  redirect         │                    │                 │
    │&lt;──────────────────│                    │                 │
</code></pre></div></div>

<h3 id="model">Model</h3>

<p>The model defines form steps and conditional validations:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/models/house.rb</span>
<span class="k">class</span> <span class="nc">House</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="no">FORM_STEPS</span> <span class="o">=</span> <span class="p">{</span>
    <span class="ss">address_info: </span><span class="p">[</span><span class="ss">:address</span><span class="p">,</span> <span class="ss">:current_family_last_name</span><span class="p">],</span>
    <span class="ss">house_info: </span><span class="p">[</span><span class="ss">:interior_color</span><span class="p">,</span> <span class="ss">:exterior_color</span><span class="p">],</span>
    <span class="ss">house_stats: </span><span class="p">[</span><span class="ss">:rooms</span><span class="p">,</span> <span class="ss">:square_feet</span><span class="p">]</span>
  <span class="p">}.</span><span class="nf">freeze</span>

  <span class="nb">attr_accessor</span> <span class="ss">:form_step</span>

  <span class="n">with_options</span> <span class="ss">if: </span><span class="o">-&gt;</span> <span class="p">{</span> <span class="n">required_for_step?</span><span class="p">(</span><span class="ss">:address_info</span><span class="p">)</span> <span class="p">}</span> <span class="k">do</span>
    <span class="n">validates</span> <span class="ss">:address</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">minimum: </span><span class="mi">5</span><span class="p">,</span> <span class="ss">maximum: </span><span class="mi">50</span> <span class="p">}</span>
    <span class="n">validates</span> <span class="ss">:current_family_last_name</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">minimum: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">maximum: </span><span class="mi">30</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="n">with_options</span> <span class="ss">if: </span><span class="o">-&gt;</span> <span class="p">{</span> <span class="n">required_for_step?</span><span class="p">(</span><span class="ss">:house_info</span><span class="p">)</span> <span class="p">}</span> <span class="k">do</span>
    <span class="n">validates</span> <span class="ss">:interior_color</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
    <span class="n">validates</span> <span class="ss">:exterior_color</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
  <span class="k">end</span>

  <span class="n">with_options</span> <span class="ss">if: </span><span class="o">-&gt;</span> <span class="p">{</span> <span class="n">required_for_step?</span><span class="p">(</span><span class="ss">:house_stats</span><span class="p">)</span> <span class="p">}</span> <span class="k">do</span>
    <span class="n">validates</span> <span class="ss">:rooms</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">numericality: </span><span class="p">{</span> <span class="ss">greater_than: </span><span class="mi">1</span> <span class="p">}</span>
    <span class="n">validates</span> <span class="ss">:square_feet</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">required_for_step?</span><span class="p">(</span><span class="n">step</span><span class="p">)</span>
    <span class="k">return</span> <span class="kp">true</span> <span class="k">if</span> <span class="n">form_step</span><span class="p">.</span><span class="nf">nil?</span>

    <span class="n">ordered_keys</span> <span class="o">=</span> <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="o">::</span><span class="no">FORM_STEPS</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:to_sym</span><span class="p">)</span>
    <span class="o">!!</span><span class="p">(</span><span class="n">ordered_keys</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="n">step</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="n">ordered_keys</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="n">form_step</span><span class="p">))</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="routes">Routes</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/routes.rb</span>
<span class="n">resources</span> <span class="ss">:houses</span>

<span class="n">resources</span> <span class="ss">:build_house</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[]</span> <span class="k">do</span>
  <span class="n">resources</span> <span class="ss">:steps</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:show</span><span class="p">,</span> <span class="ss">:update</span><span class="p">],</span> <span class="ss">controller: </span><span class="s2">"steps_controllers/house_steps"</span> <span class="k">do</span>
    <span class="n">member</span> <span class="k">do</span>
      <span class="n">get</span> <span class="ss">:back</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="houses-controller">Houses controller</h3>

<p>Initiates the wizard by generating a cache key:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/houses_controller.rb</span>
<span class="k">class</span> <span class="nc">HousesController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="n">before_action</span> <span class="ss">:set_house</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[show edit update destroy]</span>

  <span class="k">def</span> <span class="nf">index</span>
    <span class="vi">@houses</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">all</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">show</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">new</span>
    <span class="n">house_cache_key</span> <span class="o">=</span> <span class="s2">"house_form_</span><span class="si">#{</span><span class="n">session</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="s2">_</span><span class="si">#{</span><span class="no">Random</span><span class="p">.</span><span class="nf">urlsafe_base64</span><span class="p">(</span><span class="mi">6</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span>
    <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">house_cache_key</span><span class="p">,</span> <span class="p">{},</span> <span class="ss">expires_in: </span><span class="mi">1</span><span class="p">.</span><span class="nf">hour</span><span class="p">)</span>
    <span class="n">redirect_to</span> <span class="n">build_house_step_path</span><span class="p">(</span><span class="n">house_cache_key</span><span class="p">,</span> <span class="no">House</span><span class="o">::</span><span class="no">FORM_STEPS</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">first</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="c1"># edit, create, update, destroy...</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">set_house</span>
    <span class="vi">@house</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">.</span><span class="nf">expect</span><span class="p">(</span><span class="ss">:id</span><span class="p">))</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">house_params</span>
    <span class="n">params</span><span class="p">.</span><span class="nf">expect</span><span class="p">(</span><span class="ss">house: </span><span class="p">[</span><span class="ss">:address</span><span class="p">,</span> <span class="ss">:exterior_color</span><span class="p">,</span> <span class="ss">:interior_color</span><span class="p">,</span>
                          <span class="ss">:current_family_last_name</span><span class="p">,</span> <span class="ss">:rooms</span><span class="p">,</span> <span class="ss">:square_feet</span><span class="p">])</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="steps-controller">Steps controller</h3>

<p>Handles form navigation and validation:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/steps_controllers/house_steps_controller.rb</span>
<span class="k">module</span> <span class="nn">StepsControllers</span>
  <span class="k">class</span> <span class="nc">HouseStepsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
    <span class="n">before_action</span> <span class="ss">:set_house_cache_key</span>
    <span class="n">before_action</span> <span class="ss">:load_house_from_cache</span>

    <span class="no">STEPS</span> <span class="o">=</span> <span class="no">House</span><span class="o">::</span><span class="no">FORM_STEPS</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">freeze</span>

    <span class="k">def</span> <span class="nf">show</span>
      <span class="vi">@current_step</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">].</span><span class="nf">to_sym</span>
      <span class="vi">@step_index</span> <span class="o">=</span> <span class="no">STEPS</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="vi">@current_step</span><span class="p">)</span>
      <span class="vi">@total_steps</span> <span class="o">=</span> <span class="no">STEPS</span><span class="p">.</span><span class="nf">length</span>

      <span class="k">unless</span> <span class="no">STEPS</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="vi">@current_step</span><span class="p">)</span>
        <span class="n">redirect_to</span> <span class="n">build_house_step_path</span><span class="p">(</span><span class="vi">@house_cache_key</span><span class="p">,</span> <span class="no">STEPS</span><span class="p">.</span><span class="nf">first</span><span class="p">)</span>
      <span class="k">end</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">update</span>
      <span class="vi">@current_step</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">].</span><span class="nf">to_sym</span>
      <span class="vi">@step_index</span> <span class="o">=</span> <span class="no">STEPS</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="vi">@current_step</span><span class="p">)</span>
      <span class="vi">@total_steps</span> <span class="o">=</span> <span class="no">STEPS</span><span class="p">.</span><span class="nf">length</span>

      <span class="c1"># Load existing data and merge with new params</span>
      <span class="n">house_attrs</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="vi">@house_cache_key</span><span class="p">)</span> <span class="o">||</span> <span class="p">{}</span>
      <span class="n">house_attrs</span> <span class="o">=</span> <span class="n">house_attrs</span><span class="p">.</span><span class="nf">merge</span><span class="p">(</span><span class="n">house_params</span><span class="p">)</span>

      <span class="vi">@house</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">house_attrs</span><span class="p">)</span>
      <span class="vi">@house</span><span class="p">.</span><span class="nf">form_step</span> <span class="o">=</span> <span class="vi">@current_step</span>

      <span class="k">if</span> <span class="vi">@house</span><span class="p">.</span><span class="nf">valid?</span>
        <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="vi">@house_cache_key</span><span class="p">,</span> <span class="n">house_attrs</span><span class="p">,</span> <span class="ss">expires_in: </span><span class="mi">1</span><span class="p">.</span><span class="nf">hour</span><span class="p">)</span>

        <span class="k">if</span> <span class="n">final_step?</span>
          <span class="vi">@house</span><span class="p">.</span><span class="nf">form_step</span> <span class="o">=</span> <span class="kp">nil</span>
          <span class="k">if</span> <span class="vi">@house</span><span class="p">.</span><span class="nf">save</span>
            <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="vi">@house_cache_key</span><span class="p">)</span>
            <span class="n">redirect_to</span> <span class="n">house_path</span><span class="p">(</span><span class="vi">@house</span><span class="p">),</span> <span class="ss">notice: </span><span class="s2">"House was successfully created."</span>
          <span class="k">else</span>
            <span class="n">render</span> <span class="ss">:show</span><span class="p">,</span> <span class="ss">status: :unprocessable_entity</span>
          <span class="k">end</span>
        <span class="k">else</span>
          <span class="n">next_step</span> <span class="o">=</span> <span class="no">STEPS</span><span class="p">[</span><span class="no">STEPS</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="vi">@current_step</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">]</span>
          <span class="n">redirect_to</span> <span class="n">build_house_step_path</span><span class="p">(</span><span class="vi">@house_cache_key</span><span class="p">,</span> <span class="n">next_step</span><span class="p">)</span>
        <span class="k">end</span>
      <span class="k">else</span>
        <span class="n">render</span> <span class="ss">:show</span><span class="p">,</span> <span class="ss">status: :unprocessable_entity</span>
      <span class="k">end</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">back</span>
      <span class="vi">@current_step</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">].</span><span class="nf">to_sym</span>
      <span class="n">previous_step_index</span> <span class="o">=</span> <span class="no">STEPS</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="vi">@current_step</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span>

      <span class="k">if</span> <span class="n">previous_step_index</span> <span class="o">&gt;=</span> <span class="mi">0</span>
        <span class="n">previous_step</span> <span class="o">=</span> <span class="no">STEPS</span><span class="p">[</span><span class="n">previous_step_index</span><span class="p">]</span>
        <span class="n">redirect_to</span> <span class="n">build_house_step_path</span><span class="p">(</span><span class="vi">@house_cache_key</span><span class="p">,</span> <span class="n">previous_step</span><span class="p">)</span>
      <span class="k">else</span>
        <span class="n">redirect_to</span> <span class="n">houses_path</span>
      <span class="k">end</span>
    <span class="k">end</span>

    <span class="kp">private</span>

    <span class="k">def</span> <span class="nf">set_house_cache_key</span>
      <span class="vi">@house_cache_key</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:build_house_id</span><span class="p">]</span> <span class="o">||</span> <span class="n">generate_cache_key</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">generate_cache_key</span>
      <span class="s2">"house_form_</span><span class="si">#{</span><span class="n">session</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="s2">_</span><span class="si">#{</span><span class="no">Random</span><span class="p">.</span><span class="nf">urlsafe_base64</span><span class="p">(</span><span class="mi">6</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">load_house_from_cache</span>
      <span class="n">house_attrs</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="vi">@house_cache_key</span><span class="p">)</span> <span class="o">||</span> <span class="p">{}</span>
      <span class="vi">@house</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">house_attrs</span><span class="p">)</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">house_params</span>
      <span class="n">allowed_fields</span> <span class="o">=</span> <span class="no">House</span><span class="o">::</span><span class="no">FORM_STEPS</span><span class="p">[</span><span class="vi">@current_step</span><span class="p">]</span> <span class="o">||</span> <span class="p">[]</span>
      <span class="n">params</span><span class="p">.</span><span class="nf">expect</span><span class="p">(</span><span class="ss">house: </span><span class="p">[</span><span class="o">*</span><span class="n">allowed_fields</span><span class="p">]).</span><span class="nf">merge</span><span class="p">(</span><span class="ss">form_step: </span><span class="vi">@current_step</span><span class="p">)</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">final_step?</span>
      <span class="vi">@current_step</span> <span class="o">==</span> <span class="no">STEPS</span><span class="p">.</span><span class="nf">last</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="helper">Helper</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/helpers/houses_helper.rb</span>
<span class="k">module</span> <span class="nn">HousesHelper</span>
  <span class="k">def</span> <span class="nf">step_title</span><span class="p">(</span><span class="n">step</span><span class="p">)</span>
    <span class="k">case</span> <span class="n">step</span><span class="p">.</span><span class="nf">to_sym</span>
    <span class="k">when</span> <span class="ss">:address_info</span>
      <span class="s2">"Address Information"</span>
    <span class="k">when</span> <span class="ss">:house_info</span>
      <span class="s2">"House Information"</span>
    <span class="k">when</span> <span class="ss">:house_stats</span>
      <span class="s2">"House Statistics"</span>
    <span class="k">else</span>
      <span class="s2">"Unknown Step"</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">step_button_text</span><span class="p">(</span><span class="n">step</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">step</span><span class="p">.</span><span class="nf">to_sym</span> <span class="o">==</span> <span class="no">House</span><span class="o">::</span><span class="no">FORM_STEPS</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">last</span>
      <span class="s2">"Complete House"</span>
    <span class="k">else</span>
      <span class="s2">"Continue"</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">first_step?</span><span class="p">(</span><span class="n">current_step</span> <span class="o">=</span> <span class="kp">nil</span><span class="p">)</span>
    <span class="n">step</span> <span class="o">=</span> <span class="n">current_step</span> <span class="o">||</span> <span class="vi">@current_step</span>
    <span class="n">step</span> <span class="o">==</span> <span class="no">House</span><span class="o">::</span><span class="no">FORM_STEPS</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">first</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">last_step?</span><span class="p">(</span><span class="n">current_step</span> <span class="o">=</span> <span class="kp">nil</span><span class="p">)</span>
    <span class="n">step</span> <span class="o">=</span> <span class="n">current_step</span> <span class="o">||</span> <span class="vi">@current_step</span>
    <span class="n">step</span> <span class="o">==</span> <span class="no">House</span><span class="o">::</span><span class="no">FORM_STEPS</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">last</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="views">Views</h3>

<p>Main show template:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- app/views/steps_controllers/house_steps/show.html.erb --&gt;</span>
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"house-wizard-container"</span><span class="nt">&gt;</span>
  <span class="cp">&lt;%=</span> <span class="n">turbo_frame_tag</span> <span class="s2">"wizard_step"</span> <span class="k">do</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"progress-bar-container"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"progress-bar"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"progress-fill"</span> <span class="na">style=</span><span class="s">"width: </span><span class="cp">&lt;%=</span> <span class="p">((</span><span class="vi">@step_index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="mf">100.0</span> <span class="o">/</span> <span class="vi">@total_steps</span><span class="p">).</span><span class="nf">round</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="cp">%&gt;</span><span class="s">%"</span><span class="nt">&gt;&lt;/div&gt;</span>
      <span class="nt">&lt;/div&gt;</span>
      <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"progress-text"</span><span class="nt">&gt;</span>Step <span class="cp">&lt;%=</span> <span class="vi">@step_index</span> <span class="o">+</span> <span class="mi">1</span> <span class="cp">%&gt;</span> of <span class="cp">&lt;%=</span> <span class="vi">@total_steps</span> <span class="cp">%&gt;</span><span class="nt">&lt;/p&gt;</span>
    <span class="nt">&lt;/div&gt;</span>

    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"step-content"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;h2&gt;</span><span class="cp">&lt;%=</span> <span class="n">step_title</span><span class="p">(</span><span class="vi">@current_step</span><span class="p">)</span> <span class="cp">%&gt;</span><span class="nt">&lt;/h2&gt;</span>

      <span class="cp">&lt;%=</span> <span class="n">form_with</span> <span class="ss">model: </span><span class="vi">@house</span><span class="p">,</span>
            <span class="ss">url: </span><span class="n">build_house_step_path</span><span class="p">(</span><span class="vi">@house_cache_key</span><span class="p">,</span> <span class="vi">@current_step</span><span class="p">),</span>
            <span class="ss">method: :patch</span><span class="p">,</span>
            <span class="ss">data: </span><span class="p">{</span>
              <span class="ss">controller: </span><span class="s2">"multi-step-form"</span><span class="p">,</span>
              <span class="ss">turbo_frame: </span><span class="n">last_step?</span> <span class="p">?</span> <span class="s2">"_top"</span> <span class="p">:</span> <span class="s2">"wizard_step"</span>
            <span class="p">},</span>
            <span class="ss">class: </span><span class="s2">"wizard-form"</span> <span class="k">do</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="cp">%&gt;</span>

        <span class="cp">&lt;%</span> <span class="k">if</span> <span class="vi">@house</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">any?</span> <span class="cp">%&gt;</span>
          <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"error-messages"</span><span class="nt">&gt;</span>
            <span class="cp">&lt;%</span> <span class="vi">@house</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">full_messages</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">error</span><span class="o">|</span> <span class="cp">%&gt;</span>
              <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"error"</span><span class="nt">&gt;</span><span class="cp">&lt;%=</span> <span class="n">error</span> <span class="cp">%&gt;</span><span class="nt">&lt;/p&gt;</span>
            <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
          <span class="nt">&lt;/div&gt;</span>
        <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>

        <span class="cp">&lt;%=</span> <span class="n">render</span> <span class="s2">"steps_controllers/house_steps/</span><span class="si">#{</span><span class="vi">@current_step</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span> <span class="ss">form: </span><span class="n">f</span> <span class="cp">%&gt;</span>

        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"form-actions"</span><span class="nt">&gt;</span>
          <span class="cp">&lt;%</span> <span class="k">unless</span> <span class="n">first_step?</span> <span class="cp">%&gt;</span>
            <span class="cp">&lt;%=</span> <span class="n">link_to</span> <span class="s2">"Back"</span><span class="p">,</span>
                  <span class="n">back_build_house_step_path</span><span class="p">(</span><span class="vi">@house_cache_key</span><span class="p">,</span> <span class="vi">@current_step</span><span class="p">),</span>
                  <span class="ss">class: </span><span class="s2">"btn btn-secondary"</span> <span class="cp">%&gt;</span>
          <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>

          <span class="cp">&lt;%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">submit</span> <span class="n">step_button_text</span><span class="p">(</span><span class="vi">@current_step</span><span class="p">),</span> <span class="ss">class: </span><span class="s2">"btn btn-primary"</span> <span class="cp">%&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
      <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
  <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>

<p>Step partial:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- app/views/steps_controllers/house_steps/_address_info.html.erb --&gt;</span>
<span class="nt">&lt;fieldset&gt;</span>
  <span class="nt">&lt;legend&gt;</span>Address Information<span class="nt">&lt;/legend&gt;</span>

  <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"form-group"</span><span class="nt">&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:address</span> <span class="cp">%&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:address</span><span class="p">,</span> <span class="ss">placeholder: </span><span class="s2">"Enter your full address"</span> <span class="cp">%&gt;</span>
  <span class="nt">&lt;/div&gt;</span>

  <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"form-group"</span><span class="nt">&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:current_family_last_name</span><span class="p">,</span> <span class="s2">"Family Last Name"</span> <span class="cp">%&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:current_family_last_name</span> <span class="cp">%&gt;</span>
  <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/fieldset&gt;</span>
</code></pre></div></div>

<h3 id="observations">Observations</h3>

<p>This implementation works, but has some issues:</p>

<ol>
  <li><strong>Model pollution</strong>: The <code class="language-plaintext highlighter-rouge">House</code> model contains wizard-specific logic (<code class="language-plaintext highlighter-rouge">FORM_STEPS</code>, <code class="language-plaintext highlighter-rouge">form_step</code>, <code class="language-plaintext highlighter-rouge">required_for_step?</code>)</li>
  <li><strong>Controller complexity</strong>: Navigation logic (<code class="language-plaintext highlighter-rouge">next_step</code>, <code class="language-plaintext highlighter-rouge">previous_step</code>, <code class="language-plaintext highlighter-rouge">final_step?</code>) lives in the controller</li>
  <li><strong>Scattered concerns</strong>: Step definitions, validations, and navigation are spread across model and controller. This makes it difficult to test and maintain.</li>
</ol>

<hr />

<h2 id="improvement-extract-to-a-form-object">Improvement: extract to a form object</h2>

<p>Considering the issues with the Model + Controller approach, let’s extract the wizard logic to a Form Object. This is a more Rails-y way to handle multi-step forms.</p>

<p>Source: <a href="https://github.com/roberthopman/multi-step-form/commit/c0523d5117b8f93ea0f698d3e548b3b0b9ef8985">multi-step-form commit c0523d5</a></p>

<h3 id="what-changes">What changes?</h3>

<table>
  <thead>
    <tr>
      <th>Before (Model + Controller)</th>
      <th>After (Form Object)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">FORM_STEPS</code> in model</td>
      <td><code class="language-plaintext highlighter-rouge">FORM_STEPS</code> in form object</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">form_step</code> attr_accessor in model</td>
      <td><code class="language-plaintext highlighter-rouge">form_step</code> attribute in form object</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">required_for_step?</code> in model</td>
      <td><code class="language-plaintext highlighter-rouge">required_for_step?</code> in form object</td>
    </tr>
    <tr>
      <td>Step validations in model</td>
      <td>Step validations in form object</td>
    </tr>
    <tr>
      <td>Navigation methods in controller</td>
      <td>Navigation methods in form object</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">House.new(house_attrs)</code></td>
      <td><code class="language-plaintext highlighter-rouge">House::MultiStepHouseForm.from_cache(cache_data)</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">@house.valid?</code></td>
      <td><code class="language-plaintext highlighter-rouge">@form.valid?</code></td>
    </tr>
  </tbody>
</table>

<h3 id="model-now-clean">Model (now clean)</h3>

<p>The model now only contains data integrity validations:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/models/house.rb</span>
<span class="k">class</span> <span class="nc">House</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">validates</span> <span class="ss">:address</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">minimum: </span><span class="mi">10</span><span class="p">,</span> <span class="ss">maximum: </span><span class="mi">50</span> <span class="p">}</span>
  <span class="n">validates</span> <span class="ss">:current_family_last_name</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">minimum: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">maximum: </span><span class="mi">30</span> <span class="p">}</span>
  <span class="n">validates</span> <span class="ss">:interior_color</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
  <span class="n">validates</span> <span class="ss">:exterior_color</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
  <span class="n">validates</span> <span class="ss">:rooms</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">numericality: </span><span class="p">{</span> <span class="ss">greater_than: </span><span class="mi">1</span> <span class="p">}</span>
  <span class="n">validates</span> <span class="ss">:square_feet</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">numericality: </span><span class="p">{</span> <span class="ss">greater_than: </span><span class="mi">0</span> <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="base-form-class">Base form class</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/forms/application_form.rb</span>
<span class="k">class</span> <span class="nc">ApplicationForm</span>
  <span class="kp">include</span> <span class="no">ActiveModel</span><span class="o">::</span><span class="no">Model</span>
  <span class="kp">include</span> <span class="no">ActiveModel</span><span class="o">::</span><span class="no">Attributes</span>
  <span class="kp">include</span> <span class="no">ActiveModel</span><span class="o">::</span><span class="no">Validations</span>

  <span class="c1"># For form_with compatibility</span>
  <span class="k">def</span> <span class="nf">persisted?</span>
    <span class="kp">false</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">to_key</span>
    <span class="kp">nil</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">to_param</span>
    <span class="kp">nil</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">model_name</span>
    <span class="no">ActiveModel</span><span class="o">::</span><span class="no">Name</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">name</span><span class="p">.</span><span class="nf">demodulize</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/Form$/</span><span class="p">,</span> <span class="s2">""</span><span class="p">))</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="form-object">Form object</h3>

<p>The form object handles step definitions, validations, navigation, and persistence:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/forms/house/multi_step_house_form.rb</span>
<span class="k">class</span> <span class="nc">House::MultiStepHouseForm</span> <span class="o">&lt;</span> <span class="no">ApplicationForm</span>
  <span class="no">FORM_STEPS</span> <span class="o">=</span> <span class="p">{</span>
    <span class="ss">address_info: </span><span class="p">[</span><span class="ss">:address</span><span class="p">,</span> <span class="ss">:current_family_last_name</span><span class="p">],</span>
    <span class="ss">house_info: </span><span class="p">[</span><span class="ss">:interior_color</span><span class="p">,</span> <span class="ss">:exterior_color</span><span class="p">],</span>
    <span class="ss">house_stats: </span><span class="p">[</span><span class="ss">:rooms</span><span class="p">,</span> <span class="ss">:square_feet</span><span class="p">]</span>
  <span class="p">}.</span><span class="nf">freeze</span>

  <span class="c1"># Define all form attributes</span>
  <span class="n">attribute</span> <span class="ss">:address</span><span class="p">,</span> <span class="ss">:string</span>
  <span class="n">attribute</span> <span class="ss">:current_family_last_name</span><span class="p">,</span> <span class="ss">:string</span>
  <span class="n">attribute</span> <span class="ss">:interior_color</span><span class="p">,</span> <span class="ss">:string</span>
  <span class="n">attribute</span> <span class="ss">:exterior_color</span><span class="p">,</span> <span class="ss">:string</span>
  <span class="n">attribute</span> <span class="ss">:rooms</span><span class="p">,</span> <span class="ss">:integer</span>
  <span class="n">attribute</span> <span class="ss">:square_feet</span><span class="p">,</span> <span class="ss">:integer</span>

  <span class="c1"># Form state management</span>
  <span class="n">attribute</span> <span class="ss">:form_step</span><span class="p">,</span> <span class="ss">:string</span>
  <span class="n">attribute</span> <span class="ss">:house_id</span><span class="p">,</span> <span class="ss">:integer</span>

  <span class="c1"># Step-specific validations</span>
  <span class="n">with_options</span> <span class="ss">if: </span><span class="o">-&gt;</span> <span class="p">{</span> <span class="n">required_for_step?</span><span class="p">(</span><span class="ss">:address_info</span><span class="p">)</span> <span class="p">}</span> <span class="k">do</span>
    <span class="n">validates</span> <span class="ss">:address</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">minimum: </span><span class="mi">10</span><span class="p">,</span> <span class="ss">maximum: </span><span class="mi">50</span> <span class="p">}</span>
    <span class="n">validates</span> <span class="ss">:current_family_last_name</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">minimum: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">maximum: </span><span class="mi">30</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="n">with_options</span> <span class="ss">if: </span><span class="o">-&gt;</span> <span class="p">{</span> <span class="n">required_for_step?</span><span class="p">(</span><span class="ss">:house_info</span><span class="p">)</span> <span class="p">}</span> <span class="k">do</span>
    <span class="n">validates</span> <span class="ss">:interior_color</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
    <span class="n">validates</span> <span class="ss">:exterior_color</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
  <span class="k">end</span>

  <span class="n">with_options</span> <span class="ss">if: </span><span class="o">-&gt;</span> <span class="p">{</span> <span class="n">required_for_step?</span><span class="p">(</span><span class="ss">:house_stats</span><span class="p">)</span> <span class="p">}</span> <span class="k">do</span>
    <span class="n">validates</span> <span class="ss">:rooms</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">numericality: </span><span class="p">{</span> <span class="ss">greater_than: </span><span class="mi">1</span> <span class="p">}</span>
    <span class="n">validates</span> <span class="ss">:square_feet</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">steps</span>
    <span class="no">FORM_STEPS</span><span class="p">.</span><span class="nf">keys</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">first_step</span>
    <span class="n">steps</span><span class="p">.</span><span class="nf">first</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">last_step</span>
    <span class="n">steps</span><span class="p">.</span><span class="nf">last</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">attributes</span> <span class="o">=</span> <span class="p">{})</span>
    <span class="k">if</span> <span class="n">attributes</span><span class="p">[</span><span class="ss">:house_id</span><span class="p">].</span><span class="nf">present?</span>
      <span class="n">house</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">attributes</span><span class="p">[</span><span class="ss">:house_id</span><span class="p">])</span>
      <span class="n">house_attrs</span> <span class="o">=</span> <span class="n">house</span><span class="p">.</span><span class="nf">attributes</span><span class="p">.</span><span class="nf">symbolize_keys</span><span class="p">.</span><span class="nf">slice</span><span class="p">(</span><span class="o">*</span><span class="no">FORM_STEPS</span><span class="p">.</span><span class="nf">values</span><span class="p">.</span><span class="nf">flatten</span><span class="p">)</span>
      <span class="n">house_attrs</span><span class="p">[</span><span class="ss">:house_id</span><span class="p">]</span> <span class="o">=</span> <span class="n">house</span><span class="p">.</span><span class="nf">id</span>
      <span class="n">attributes</span> <span class="o">=</span> <span class="n">house_attrs</span><span class="p">.</span><span class="nf">merge</span><span class="p">(</span><span class="n">attributes</span><span class="p">)</span>
    <span class="k">end</span>

    <span class="k">super</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">form_step</span>
    <span class="vi">@form_step</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">to_sym</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">form_step</span><span class="o">=</span><span class="p">(</span><span class="n">step</span><span class="p">)</span>
    <span class="vi">@form_step</span> <span class="o">=</span> <span class="n">step</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">to_sym</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">current_step_fields</span>
    <span class="no">FORM_STEPS</span><span class="p">[</span><span class="n">form_step</span><span class="p">]</span> <span class="o">||</span> <span class="p">[]</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">required_for_step?</span><span class="p">(</span><span class="n">step</span><span class="p">)</span>
    <span class="k">return</span> <span class="kp">true</span> <span class="k">if</span> <span class="n">form_step</span><span class="p">.</span><span class="nf">nil?</span>

    <span class="n">ordered_keys</span> <span class="o">=</span> <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">steps</span>
    <span class="o">!!</span><span class="p">(</span><span class="n">ordered_keys</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="n">step</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="n">ordered_keys</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="n">form_step</span><span class="p">))</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">first_step?</span>
    <span class="n">form_step</span> <span class="o">==</span> <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">first_step</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">last_step?</span>
    <span class="n">form_step</span> <span class="o">==</span> <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">last_step</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">next_step</span>
    <span class="n">current_index</span> <span class="o">=</span> <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">steps</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="n">form_step</span><span class="p">)</span>
    <span class="k">return</span> <span class="kp">nil</span> <span class="k">if</span> <span class="n">current_index</span><span class="p">.</span><span class="nf">nil?</span> <span class="o">||</span> <span class="n">current_index</span> <span class="o">&gt;=</span> <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">steps</span><span class="p">.</span><span class="nf">length</span> <span class="o">-</span> <span class="mi">1</span>

    <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">steps</span><span class="p">[</span><span class="n">current_index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">]</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">previous_step</span>
    <span class="n">current_index</span> <span class="o">=</span> <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">steps</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="n">form_step</span><span class="p">)</span>
    <span class="k">return</span> <span class="kp">nil</span> <span class="k">if</span> <span class="n">current_index</span><span class="p">.</span><span class="nf">nil?</span> <span class="o">||</span> <span class="n">current_index</span> <span class="o">&lt;=</span> <span class="mi">0</span>

    <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">steps</span><span class="p">[</span><span class="n">current_index</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">step_index</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">steps</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="n">form_step</span><span class="p">)</span> <span class="o">||</span> <span class="mi">0</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">total_steps</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">steps</span><span class="p">.</span><span class="nf">length</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">progress_percentage</span>
    <span class="p">((</span><span class="n">step_index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="mf">100.0</span> <span class="o">/</span> <span class="n">total_steps</span><span class="p">).</span><span class="nf">round</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">to_cache</span>
    <span class="p">{</span>
      <span class="ss">address: </span><span class="n">address</span><span class="p">,</span>
      <span class="ss">current_family_last_name: </span><span class="n">current_family_last_name</span><span class="p">,</span>
      <span class="ss">interior_color: </span><span class="n">interior_color</span><span class="p">,</span>
      <span class="ss">exterior_color: </span><span class="n">exterior_color</span><span class="p">,</span>
      <span class="ss">rooms: </span><span class="n">rooms</span><span class="p">,</span>
      <span class="ss">square_feet: </span><span class="n">square_feet</span><span class="p">,</span>
      <span class="ss">form_step: </span><span class="n">form_step</span><span class="p">,</span>
      <span class="ss">house_id: </span><span class="n">house_id</span>
    <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">from_cache</span><span class="p">(</span><span class="n">cache_data</span><span class="p">)</span>
    <span class="n">new</span><span class="p">(</span><span class="n">cache_data</span> <span class="o">||</span> <span class="p">{})</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">save</span>
    <span class="k">return</span> <span class="kp">false</span> <span class="k">unless</span> <span class="n">valid?</span>

    <span class="n">house_attributes</span> <span class="o">=</span> <span class="p">{</span>
      <span class="ss">address: </span><span class="n">address</span><span class="p">,</span>
      <span class="ss">current_family_last_name: </span><span class="n">current_family_last_name</span><span class="p">,</span>
      <span class="ss">interior_color: </span><span class="n">interior_color</span><span class="p">,</span>
      <span class="ss">exterior_color: </span><span class="n">exterior_color</span><span class="p">,</span>
      <span class="ss">rooms: </span><span class="n">rooms</span><span class="p">,</span>
      <span class="ss">square_feet: </span><span class="n">square_feet</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="n">house_id</span><span class="p">.</span><span class="nf">present?</span>
      <span class="n">house</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">house_id</span><span class="p">)</span>
      <span class="n">house</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="n">house_attributes</span><span class="p">)</span>
    <span class="k">else</span>
      <span class="n">house</span> <span class="o">=</span> <span class="no">House</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="n">house_attributes</span><span class="p">)</span>
      <span class="nb">self</span><span class="p">.</span><span class="nf">house_id</span> <span class="o">=</span> <span class="n">house</span><span class="p">.</span><span class="nf">id</span> <span class="k">if</span> <span class="n">house</span><span class="p">.</span><span class="nf">persisted?</span>
    <span class="k">end</span>

    <span class="n">house</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">save!</span>
    <span class="n">house</span> <span class="o">=</span> <span class="n">save</span>
    <span class="k">raise</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">RecordInvalid</span><span class="p">,</span> <span class="n">house</span> <span class="k">unless</span> <span class="n">house</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">persisted?</span>

    <span class="n">house</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">model_name</span>
    <span class="no">ActiveModel</span><span class="o">::</span><span class="no">Name</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="s2">"House"</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="controller">Controller</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/steps_controllers/house_steps_controller.rb</span>
<span class="k">module</span> <span class="nn">StepsControllers</span>
  <span class="k">class</span> <span class="nc">HouseStepsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
    <span class="n">before_action</span> <span class="ss">:set_house_cache_key</span>
    <span class="n">before_action</span> <span class="ss">:load_form_from_cache</span>

    <span class="k">def</span> <span class="nf">show</span>
      <span class="vi">@current_step</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">].</span><span class="nf">to_sym</span>
      <span class="vi">@form</span><span class="p">.</span><span class="nf">form_step</span> <span class="o">=</span> <span class="vi">@current_step</span>

      <span class="k">unless</span> <span class="no">House</span><span class="o">::</span><span class="no">MultiStepHouseForm</span><span class="p">.</span><span class="nf">steps</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="vi">@current_step</span><span class="p">)</span>
        <span class="n">redirect_to</span> <span class="n">build_house_step_path</span><span class="p">(</span><span class="vi">@house_cache_key</span><span class="p">,</span> <span class="no">House</span><span class="o">::</span><span class="no">MultiStepHouseForm</span><span class="p">.</span><span class="nf">first_step</span><span class="p">)</span>
        <span class="kp">nil</span>
      <span class="k">end</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">update</span>
      <span class="vi">@current_step</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">].</span><span class="nf">to_sym</span>
      <span class="vi">@form</span><span class="p">.</span><span class="nf">form_step</span> <span class="o">=</span> <span class="vi">@current_step</span>
      <span class="vi">@form</span><span class="p">.</span><span class="nf">assign_attributes</span><span class="p">(</span><span class="n">form_params</span><span class="p">)</span>

      <span class="k">if</span> <span class="vi">@form</span><span class="p">.</span><span class="nf">valid?</span>
        <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="vi">@house_cache_key</span><span class="p">,</span> <span class="vi">@form</span><span class="p">.</span><span class="nf">to_cache</span><span class="p">,</span> <span class="ss">expires_in: </span><span class="mi">1</span><span class="p">.</span><span class="nf">hour</span><span class="p">)</span>

        <span class="k">if</span> <span class="vi">@form</span><span class="p">.</span><span class="nf">last_step?</span>
          <span class="vi">@form</span><span class="p">.</span><span class="nf">form_step</span> <span class="o">=</span> <span class="kp">nil</span>
          <span class="n">house</span> <span class="o">=</span> <span class="vi">@form</span><span class="p">.</span><span class="nf">save!</span>
          <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="vi">@house_cache_key</span><span class="p">)</span>
          <span class="n">redirect_to</span> <span class="n">house_path</span><span class="p">(</span><span class="n">house</span><span class="p">),</span> <span class="ss">notice: </span><span class="s2">"House was successfully created."</span>
        <span class="k">else</span>
          <span class="n">redirect_to</span> <span class="n">build_house_step_path</span><span class="p">(</span><span class="vi">@house_cache_key</span><span class="p">,</span> <span class="vi">@form</span><span class="p">.</span><span class="nf">next_step</span><span class="p">)</span>
        <span class="k">end</span>
      <span class="k">else</span>
        <span class="n">render</span> <span class="ss">:show</span><span class="p">,</span> <span class="ss">status: :unprocessable_entity</span>
      <span class="k">end</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">back</span>
      <span class="vi">@current_step</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">].</span><span class="nf">to_sym</span>
      <span class="vi">@form</span><span class="p">.</span><span class="nf">form_step</span> <span class="o">=</span> <span class="vi">@current_step</span>

      <span class="k">if</span> <span class="vi">@form</span><span class="p">.</span><span class="nf">previous_step</span>
        <span class="n">redirect_to</span> <span class="n">build_house_step_path</span><span class="p">(</span><span class="vi">@house_cache_key</span><span class="p">,</span> <span class="vi">@form</span><span class="p">.</span><span class="nf">previous_step</span><span class="p">)</span>
      <span class="k">else</span>
        <span class="n">redirect_to</span> <span class="n">houses_path</span>
      <span class="k">end</span>
    <span class="k">end</span>

    <span class="kp">private</span>

    <span class="k">def</span> <span class="nf">set_house_cache_key</span>
      <span class="vi">@house_cache_key</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:build_house_id</span><span class="p">]</span> <span class="o">||</span> <span class="n">generate_cache_key</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">generate_cache_key</span>
      <span class="s2">"house_form_</span><span class="si">#{</span><span class="n">session</span><span class="p">.</span><span class="nf">id</span><span class="si">}</span><span class="s2">_</span><span class="si">#{</span><span class="no">Random</span><span class="p">.</span><span class="nf">urlsafe_base64</span><span class="p">(</span><span class="mi">6</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">load_form_from_cache</span>
      <span class="n">cache_data</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="vi">@house_cache_key</span><span class="p">)</span> <span class="o">||</span> <span class="p">{}</span>
      <span class="vi">@form</span> <span class="o">=</span> <span class="no">House</span><span class="o">::</span><span class="no">MultiStepHouseForm</span><span class="p">.</span><span class="nf">from_cache</span><span class="p">(</span><span class="n">cache_data</span><span class="p">)</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">form_params</span>
      <span class="n">params</span><span class="p">.</span><span class="nf">expect</span><span class="p">(</span><span class="ss">house: </span><span class="p">[</span><span class="o">*</span><span class="vi">@form</span><span class="p">.</span><span class="nf">current_step_fields</span><span class="p">])</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="routes-1">Routes</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/routes.rb</span>
<span class="n">resources</span> <span class="ss">:houses</span>

<span class="n">resources</span> <span class="ss">:build_house</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[]</span> <span class="k">do</span>
  <span class="n">resources</span> <span class="ss">:steps</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:show</span><span class="p">,</span> <span class="ss">:update</span><span class="p">],</span> <span class="ss">controller: </span><span class="s2">"steps_controllers/house_steps"</span> <span class="k">do</span>
    <span class="n">member</span> <span class="k">do</span>
      <span class="n">get</span> <span class="ss">:back</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
<span class="c1"># URLs: /build_house/:cache_key/steps/:step_name</span>
</code></pre></div></div>

<h3 id="helper-1">Helper</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/helpers/houses_helper.rb</span>
<span class="k">module</span> <span class="nn">HousesHelper</span>
  <span class="k">def</span> <span class="nf">step_title</span><span class="p">(</span><span class="n">step</span><span class="p">)</span>
    <span class="k">case</span> <span class="n">step</span><span class="p">.</span><span class="nf">to_sym</span>
    <span class="k">when</span> <span class="ss">:address_info</span>
      <span class="s2">"Address Information"</span>
    <span class="k">when</span> <span class="ss">:house_info</span>
      <span class="s2">"House Information"</span>
    <span class="k">when</span> <span class="ss">:house_stats</span>
      <span class="s2">"House Statistics"</span>
    <span class="k">else</span>
      <span class="s2">"Unknown Step"</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">step_button_text</span><span class="p">(</span><span class="n">step</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">step</span><span class="p">.</span><span class="nf">to_sym</span> <span class="o">==</span> <span class="no">House</span><span class="o">::</span><span class="no">MultiStepHouseForm</span><span class="p">.</span><span class="nf">last_step</span>
      <span class="s2">"Complete House"</span>
    <span class="k">else</span>
      <span class="s2">"Continue"</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="views-1">Views</h3>

<p>Main show template:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- app/views/steps_controllers/house_steps/show.html.erb --&gt;</span>
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"house-wizard-container"</span><span class="nt">&gt;</span>
  <span class="cp">&lt;%=</span> <span class="n">turbo_frame_tag</span> <span class="s2">"wizard_step"</span> <span class="k">do</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"progress-bar-container"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"progress-bar"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"progress-fill"</span> <span class="na">style=</span><span class="s">"width: </span><span class="cp">&lt;%=</span> <span class="vi">@form</span><span class="p">.</span><span class="nf">progress_percentage</span> <span class="cp">%&gt;</span><span class="s">%"</span><span class="nt">&gt;&lt;/div&gt;</span>
      <span class="nt">&lt;/div&gt;</span>
      <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"progress-text"</span><span class="nt">&gt;</span>Step <span class="cp">&lt;%=</span> <span class="vi">@form</span><span class="p">.</span><span class="nf">step_index</span> <span class="o">+</span> <span class="mi">1</span> <span class="cp">%&gt;</span> of <span class="cp">&lt;%=</span> <span class="vi">@form</span><span class="p">.</span><span class="nf">total_steps</span> <span class="cp">%&gt;</span><span class="nt">&lt;/p&gt;</span>
    <span class="nt">&lt;/div&gt;</span>

    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"step-content"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;h2&gt;</span><span class="cp">&lt;%=</span> <span class="n">step_title</span><span class="p">(</span><span class="vi">@current_step</span><span class="p">)</span> <span class="cp">%&gt;</span><span class="nt">&lt;/h2&gt;</span>

      <span class="cp">&lt;%=</span> <span class="n">form_with</span> <span class="ss">model: </span><span class="vi">@form</span><span class="p">,</span>
            <span class="ss">url: </span><span class="n">build_house_step_path</span><span class="p">(</span><span class="vi">@house_cache_key</span><span class="p">,</span> <span class="vi">@current_step</span><span class="p">),</span>
            <span class="ss">method: :patch</span><span class="p">,</span>
            <span class="ss">data: </span><span class="p">{</span>
              <span class="ss">controller: </span><span class="s2">"multi-step-form"</span><span class="p">,</span>
              <span class="ss">turbo_frame: </span><span class="vi">@form</span><span class="p">.</span><span class="nf">last_step?</span> <span class="p">?</span> <span class="s2">"_top"</span> <span class="p">:</span> <span class="s2">"wizard_step"</span>
            <span class="p">},</span>
            <span class="ss">class: </span><span class="s2">"wizard-form"</span> <span class="k">do</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="cp">%&gt;</span>

        <span class="cp">&lt;%</span> <span class="k">if</span> <span class="vi">@form</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">any?</span> <span class="cp">%&gt;</span>
          <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"error-messages"</span><span class="nt">&gt;</span>
            <span class="cp">&lt;%</span> <span class="vi">@form</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">full_messages</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">error</span><span class="o">|</span> <span class="cp">%&gt;</span>
              <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"error"</span><span class="nt">&gt;</span><span class="cp">&lt;%=</span> <span class="n">error</span> <span class="cp">%&gt;</span><span class="nt">&lt;/p&gt;</span>
            <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
          <span class="nt">&lt;/div&gt;</span>
        <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>

        <span class="cp">&lt;%=</span> <span class="n">render</span> <span class="s2">"steps_controllers/house_steps/</span><span class="si">#{</span><span class="vi">@current_step</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span> <span class="ss">form: </span><span class="n">f</span> <span class="cp">%&gt;</span>

        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"form-actions"</span><span class="nt">&gt;</span>
          <span class="cp">&lt;%</span> <span class="k">unless</span> <span class="vi">@form</span><span class="p">.</span><span class="nf">first_step?</span> <span class="cp">%&gt;</span>
            <span class="cp">&lt;%=</span> <span class="n">link_to</span> <span class="s2">"Back"</span><span class="p">,</span>
                  <span class="n">back_build_house_step_path</span><span class="p">(</span><span class="vi">@house_cache_key</span><span class="p">,</span> <span class="vi">@current_step</span><span class="p">),</span>
                  <span class="ss">class: </span><span class="s2">"btn btn-secondary"</span> <span class="cp">%&gt;</span>
          <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>

          <span class="cp">&lt;%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">submit</span> <span class="n">step_button_text</span><span class="p">(</span><span class="vi">@current_step</span><span class="p">),</span> <span class="ss">class: </span><span class="s2">"btn btn-primary"</span> <span class="cp">%&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
      <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
  <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>

<p>Step partial example:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- app/views/steps_controllers/house_steps/_address_info.html.erb --&gt;</span>
<span class="nt">&lt;fieldset&gt;</span>
  <span class="nt">&lt;legend&gt;</span>Address Information<span class="nt">&lt;/legend&gt;</span>

  <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"form-group"</span><span class="nt">&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:address</span> <span class="cp">%&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:address</span><span class="p">,</span> <span class="ss">placeholder: </span><span class="s2">"Enter your full address"</span> <span class="cp">%&gt;</span>
  <span class="nt">&lt;/div&gt;</span>

  <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"form-group"</span><span class="nt">&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:current_family_last_name</span><span class="p">,</span> <span class="s2">"Family Last Name"</span> <span class="cp">%&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:current_family_last_name</span><span class="p">,</span> <span class="ss">placeholder: </span><span class="s2">"Enter family last name"</span> <span class="cp">%&gt;</span>
  <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/fieldset&gt;</span>
</code></pre></div></div>

<h3 id="summary">Summary</h3>

<table>
  <thead>
    <tr>
      <th>Approach</th>
      <th>Pros</th>
      <th>Cons</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Wicked gem</td>
      <td>Less boilerplate, battle-tested</td>
      <td>External dependency, less control</td>
    </tr>
    <tr>
      <td>Model + Controller</td>
      <td>No dependencies, straightforward</td>
      <td>Model pollution, scattered logic</td>
    </tr>
    <tr>
      <td>Form Object</td>
      <td>Clean model, centralized logic, testable</td>
      <td>More initial code to write</td>
    </tr>
  </tbody>
</table>

<h3 id="when-to-use-each">When to use each</h3>

<ul>
  <li><strong>Wicked gem</strong>: Quick prototypes, simple wizards, when you want conventions</li>
  <li><strong>Model + Controller</strong>: When the wizard is simple and the form object feels like overkill</li>
  <li><strong>Form Object</strong>: Long-running apps, complex validation rules, when you want clean separation of concerns</li>
</ul>]]></content><author><name>Robert Hopman</name><email>robert@bonaroo.nl</email></author><category term="ruby" /><category term="rails" /><category term="forms" /><category term="wizard" /><category term="wicked" /><summary type="html"><![CDATA[A summary of implementing multi-step forms in Rails covering data persistence, URL strategies, model validations, and controller patterns.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://roberthopman.com/assets/images/default_technical_blog.png" /><media:content medium="image" url="https://roberthopman.com/assets/images/default_technical_blog.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Adding ERB Lint Caching to Nextgen</title><link href="https://roberthopman.com/nextgen-erb-lint-caching/" rel="alternate" type="text/html" title="Adding ERB Lint Caching to Nextgen" /><published>2025-11-29T00:00:00+00:00</published><updated>2025-11-29T00:00:00+00:00</updated><id>https://roberthopman.com/nextgen-erb-lint-caching</id><content type="html" xml:base="https://roberthopman.com/nextgen-erb-lint-caching/"><![CDATA[<p>I recently submitted a <a href="https://github.com/mattbrictson/nextgen/pull/178">pull request</a> to Matt Brictson’s nextgen gem.</p>

<h2 id="why-bother">Why Bother</h2>

<p>Pre-commit hooks are becoming annoyingly slow when they check all erb files.</p>

<p>ERB Lint caching skips re-analysis of unchanged files. This helps with quicker pre-commit hooks, which will reduce time in local development. Just a flag that was available but not enabled by default.</p>

<h2 id="the-change">The Change</h2>

<p>The PR enables ERB Lint’s <code class="language-plaintext highlighter-rouge">--cache</code> flag across the project:</p>

<ul>
  <li>Adds <code class="language-plaintext highlighter-rouge">--cache</code> to the erb_lint rake task</li>
  <li>Updates overcommit hooks to use caching</li>
  <li>Adds <code class="language-plaintext highlighter-rouge">.erb_lint_cache</code> to gitignore</li>
</ul>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Before</span>
<span class="n">sh</span> <span class="s2">"bin/erb_lint --lint-all"</span>

<span class="c1"># After</span>
<span class="n">sh</span> <span class="s2">"bin/erb_lint --lint-all --cache"</span>
</code></pre></div></div>

<h2 id="some-numbers">Some Numbers</h2>

<p>Looking at the <a href="https://clickgems.clickhouse.com/dashboard/nextgen">download stats</a>, nextgen sees around 284 weekly downloads and has 29K total downloads. That means this small change might save a few seconds here and there for a decent number of projects.</p>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Weekly downloads</td>
      <td>~284</td>
    </tr>
    <tr>
      <td>Monthly downloads</td>
      <td>~4,500</td>
    </tr>
    <tr>
      <td>Total downloads</td>
      <td>29,000+</td>
    </tr>
  </tbody>
</table>

<h2 id="takeaway">Takeaway</h2>

<p>If you use a gem regularly and spot something that could be slightly better, consider opening a PR. It doesn’t have to be a big feature. Sometimes enabling a flag is enough.</p>

<p><a href="https://github.com/mattbrictson/nextgen/pull/178">View the PR on GitHub</a></p>]]></content><author><name>Robert Hopman</name></author><category term="ruby" /><category term="rails" /><category term="performance" /><category term="open-source" /><summary type="html"><![CDATA[A small contribution to nextgen that enables ERB Lint caching]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://roberthopman.com/assets/images/default_technical_blog.png" /><media:content medium="image" url="https://roberthopman.com/assets/images/default_technical_blog.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">General Production Checklist for Automating Applications</title><link href="https://roberthopman.com/general-production-checklist-for-automating-applications/" rel="alternate" type="text/html" title="General Production Checklist for Automating Applications" /><published>2025-07-06T00:00:00+00:00</published><updated>2025-07-06T00:00:00+00:00</updated><id>https://roberthopman.com/general-production-checklist-for-automating-applications</id><content type="html" xml:base="https://roberthopman.com/general-production-checklist-for-automating-applications/"><![CDATA[<p>This is a concept checklist to ensure that an application is production ready.</p>

<p><a href="https://gist.github.com/roberthopman/fdeeb94e74b7c6b7e67857916f241714">The gist at github.</a></p>

<h2 id="hosting">Hosting</h2>
<ul>
  <li>Hosting
    <ul>
      <li>Locations</li>
      <li>Server(s)
        <ul>
          <li>Physical</li>
          <li>Cloud</li>
        </ul>
      </li>
      <li>Access to servers
        <ul>
          <li>Ip address whitelist</li>
          <li>Authorization / Verification
            <ul>
              <li>Phone</li>
              <li>Email</li>
            </ul>
          </li>
          <li>Ssh
            <ul>
              <li>Authorized keys</li>
            </ul>
          </li>
          <li>Clean up old account access</li>
        </ul>
      </li>
      <li>Disk space
        <ul>
          <li>Used and free</li>
          <li>Inodes used and free</li>
          <li>Who can modify</li>
          <li>When to modify</li>
          <li>How to modify</li>
        </ul>
      </li>
      <li>File storage</li>
      <li>Backups
        <ul>
          <li>Is there an automated database backup?
            <ul>
              <li>How often?</li>
              <li>How long are backups kept?</li>
              <li>Where are backups stored?</li>
              <li>How to restore a backup?</li>
              <li>How to test a backup?</li>
            </ul>
          </li>
          <li>Is there an automated file backup?</li>
        </ul>
      </li>
      <li>CDN</li>
    </ul>
  </li>
</ul>

<h2 id="monitoring">Monitoring</h2>
<ul>
  <li>Bug/Error monitoring (Sentry)</li>
  <li>Infra/Hosting monitoring (AppSignal/Elastic/…)
    <ul>
      <li>Server</li>
      <li>Database</li>
      <li>Redis</li>
      <li>Background processing</li>
      <li>Email processing</li>
    </ul>
  </li>
  <li>Other monitoring in place
    <ul>
      <li>Custom made</li>
      <li>Emails</li>
      <li>3rd party tools
        <ul>
          <li>checkly, uptimerobot, pingdom, …</li>
          <li>Slack webhooks</li>
        </ul>
      </li>
      <li>Manual checks</li>
    </ul>
  </li>
</ul>

<h2 id="logging">Logging</h2>
<ul>
  <li>Rotation</li>
  <li>Collection</li>
  <li>Retention</li>
  <li>Size</li>
  <li>Types
    <ul>
      <li>Server</li>
      <li>Cron</li>
      <li>Error</li>
      <li>Other</li>
    </ul>
  </li>
</ul>

<h2 id="ssl">SSL</h2>
<ul>
  <li>Domains</li>
  <li>Renewal procedure
    <ul>
      <li>Access provision</li>
      <li>Automatic</li>
    </ul>
  </li>
</ul>

<h2 id="application-checklist">Application Checklist</h2>
<ul>
  <li>Documentation for technical team
    <ul>
      <li>Description of business processes</li>
      <li>Description of application</li>
      <li>Deploy processes and scripts
        <ul>
          <li>For acceptance, staging, test, production</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>API
    <ul>
      <li>Are API requests being made via a separate subdomain? Api.example.com</li>
    </ul>
  </li>
  <li>Configuration is stored in environment variables?
    <ul>
      <li>Check:
        <ul>
          <li>No Passwords in the database</li>
          <li>Env variables are configured</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="domain-names--dns">Domain names &amp; DNS</h2>
<ul>
  <li>List</li>
  <li>Access</li>
  <li>Payment</li>
</ul>

<h2 id="transactional-email">Transactional Email</h2>
<ul>
  <li>Payment processing</li>
</ul>

<h2 id="3rd-party-integrations-services-plugins-themes-assets-images-fonts">3rd party integrations, services, plugins, themes, assets, images, fonts</h2>
<ul>
  <li>List</li>
  <li>Access</li>
  <li>Payment</li>
</ul>

<h2 id="development-checklist">Development Checklist</h2>
<ul>
  <li>Version control</li>
  <li>Code reviews</li>
  <li>Test driven development
    <ul>
      <li>Tests all green?</li>
      <li>Acceptance / integration / unit / other</li>
      <li>Coverage</li>
    </ul>
  </li>
  <li>Continuous integration</li>
  <li>Deployment
    <ul>
      <li>Schedule monday - thursday</li>
    </ul>
  </li>
  <li>Programming language
    <ul>
      <li>version</li>
    </ul>
  </li>
  <li>Able to start project locally</li>
</ul>

<h2 id="coordination-checklist">Coordination Checklist</h2>
<ul>
  <li>Shared knowledge in directories, or files</li>
  <li>Work locations, times and schedules for real-time conversation</li>
  <li>Access to communication; Slack, Email, Telephone</li>
</ul>]]></content><author><name>Robert Hopman</name><email>robert@bonaroo.nl</email></author><category term="deployment" /><category term="automation" /><category term="monitoring" /><category term="devops" /><category term="checklist" /><category term="production" /><summary type="html"><![CDATA[Comprehensive production readiness checklist covering hosting, monitoring, logging, SSL, domain management, and deployment automation for applications.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://roberthopman.com/assets/images/default_technical_blog.png" /><media:content medium="image" url="https://roberthopman.com/assets/images/default_technical_blog.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">A bug report template</title><link href="https://roberthopman.com/bug-report-template/" rel="alternate" type="text/html" title="A bug report template" /><published>2024-07-07T00:00:00+00:00</published><updated>2024-07-07T00:00:00+00:00</updated><id>https://roberthopman.com/bug-report-template</id><content type="html" xml:base="https://roberthopman.com/bug-report-template/"><![CDATA[<p>A good bug report can save hours of debugging.</p>

<p>This can be used as (Github) template for issues.</p>

<p>Add file in repo_name/.github/ISSUE_TEMPLATE/bug_report.md</p>

<p><a href="https://gist.github.com/roberthopman/96154ffab30e89d2972cb875d09ecec8">The gist at github.</a></p>

<script src="https://gist.github.com/96154ffab30e89d2972cb875d09ecec8.js"> </script>]]></content><author><name>Robert Hopman</name><email>robert@bonaroo.nl</email></author><category term="github" /><category term="template" /><category term="debugging" /><category term="workflow" /><summary type="html"><![CDATA[A good bug report can save hours of debugging. Use this GitHub issue template to standardize bug reporting in your projects.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://roberthopman.com/assets/images/default_technical_blog.png" /><media:content medium="image" url="https://roberthopman.com/assets/images/default_technical_blog.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>