---
title: Operation: From State Hero to Zero
description: The surgical breakdown of a 1.6k LOC Ruby monolith into focused modules. Or: how I performed open-heart surgery on a dying codebase and lived to tell the tale.
date: 2025-06-19
tags: ruby, refactoring, architecture, maintainer-experience, code-surgery
author: Abdelkader Boudih
---

It was 3 AM when I stared down the beast.
The `state_machines` gem lay before me like a patient in critical condition—1,647 lines of tangled Ruby sprawled across a single file. Every method was a tumor, every global state an infection spreading through the codebase. The heart monitor was flat-lining, but somehow, this thing was still breathing.
By dawn, I’d either save it or put it out of its misery.

## Code Triage: Assessing the Damage

Here’s what 15 years of “just add it here” gets you: a monolithic nightmare that makes ancient Egyptian mummies look well-organized. The `machine.rb` file wasn’t code—it was digital archaeology. If I didn’t know Ruby, I would have thought it was a GGUF file, an LLM model file, but this one was sentient and judging me… in Ruby.
But here’s the plot twist: of those 1,647 lines, 1,477 were comments. Yes, 90% documentation. The actual code? A mere 125 lines drowning in an ocean of explanations, examples, and apologies to future maintainers. It wasn’t a code monster—it was a documentation graveyard with a few lines of logic trying to breathe underneath.

```
# The patient: machine.rb (1,647 LOC)
class Machine
  # 47 instance variables (why not 50?)
  # 23 class variables (global state soup)
  # 156 methods (some doing 12 different things)
  # Comments from 2008 explaining Ruby 1.8 quirks
  # Methods named things like `really_really_final_state`
end
```

Looking at this code was like performing an autopsy while the patient was still talking. Every line had dependencies reaching into three other sections. Touch one method, break five others. It was a house of cards built during an earthquake.
The diagnosis was clear: **Acute Monolithic Syndrome with Legacy Complications.**

## Mission Planning: The Extraction Strategy

You don’t just start hacking away at a 1.6k LOC beast. That’s not surgery—that’s a chainsaw massacre. I needed a plan, phases, and the surgical precision of someone who’s done this dance before.
**Operation Objectives:**

- Extract focused modules without killing the patient

- Maintain backward compatibility because I don’t have the emotional energy to open PRs across thousands of dependent repositories, pleading with maintainers to forgive my past architectural mistakes and begging them to accept that I’ve finally realized some DSL methods desperately need better names

- Prepare for aggressive modern refactoring once legacy support drops

- Transform chaos into comprehensible architecture

**Rules of Engagement:**

- One module extraction per deployment

- Tests must pass at every step

- No behavioral changes (yet)

- Document everything for the war crimes tribunal

## Phase 1: Utilities Extraction 🔧

*Codename: “Operation Listerine”* - because you need mouthwash before talking
First rule of surgery: start with the parts that won’t kill the patient. Utility methods were the low-hanging fruit—helper functions that had somehow gotten lost in the monolith like spare change in couch cushions.

```
# Before: buried in the 1.6k LOC monster
class Machine
  def pluralize(word)
    # Inflection logic mixed with state management
  end

  def sibling_machines
    # Complex lookup buried in monolith
  end

  def owner_class_has_method?(scope, method)
    # Reflection logic tangled with everything else
  end

  # 1,500 more lines of documentation...
end

# After: clean extraction to StateMachines::Machine::Utilities
module StateMachines::Machine::Utilities
  def pluralize(word)
    word = word.to_s
    if word.respond_to?(:pluralize)
      word.pluralize
    else
      "#{word}s"
    end
  end

  def sibling_machines
    owner_class.state_machines.each_with_object([]) do |(name, machine), machines|
      machines &#x3C;&#x3C; (owner_class.state_machine(name) {}) if machine.attribute == attribute &#x26;&#x26; machine != self
    end
  end

  # Clean, focused, testable
end
```

But then came the plot twist.
I was feeling cocky, drunk on the success of clean extraction. “You know what?” I thought, “Let’s make this beautiful. Let’s use Ruby Refinements to extend String with our pluralize method. Clean, scoped, no global pollution.”
I spent an hour crafting the perfect refinement:

```
module StateMachines::Machine::Utilities
  refine String do
    def pluralize
      # Beautiful, scoped string extension
    end
  end

  using StateMachines::Machine::Utilities

  def pluralize(word)
    word.to_s.pluralize
  end
end
```

It was elegant. It was modern. It was everything Ruby promised about clean code.
Then I ran the JRuby tests.
I run my CI on a cluster of three PlayStation 4 Pro consoles running Ubuntu headless—they process builds like proper servers, except with way better cooling. One of them pings my self-hosted chat: “Exotic Build failed.” Of course it’s either JRuby or TruffleRuby acting up. I keep those weird Ruby implementations quarantined on gaming hardware for a reason—if they leak memory or crash, at least they’re contained with Resident Evil and the Orbis kernel.
**“Exotic Build failed.”**
JRuby doesn’t support Refinements. Never has. Probably never will. [**@headius**](https://github.com/headius) and [**@enebo**](https://github.com/enebo) would probably ban me for life if I dared raise it as a bug. The JVM isn’t built for that kind of elegance. JRuby would have to become what Elixir did to Fortran—I mean, Erlang. My beautiful, modern solution was DOA on one-third of our supported platforms.

**Update**: JRuby does support Refinements now (thanks Charles and Thomas for the correction!), but in retrospective, people expect that `pluralize` will get monkey patched by ActiveSupport and get supercharged with the inflector, so we get `man => men` instead of `man => mans`. The simple fallback was the right choice anyway.

That moment was like performing surgery with a laser scalpel, only to discover the patient is allergic to light. Back to the stone-age tools: simple, boring, universally compatible methods.

```
def pluralize(word)
  word = word.to_s
  if word.respond_to?(:pluralize)
    word.pluralize
  else
    "#{word}s"
  end
end
```

But wait, it gets worse.
Curiosity got the better of me. I searched GitHub for Refinements usage patterns across popular Ruby repositories. Surely someone, somewhere, was using this elegant feature for production code?
The results were… telling.
Out of thousands of Ruby repositories, I found maybe a dozen actually using Refinements in meaningful ways. Most were either experiments, educational examples, or abandoned attempts. The pattern I’d spent an hour perfecting? Practically non-existent in the wild.
Then I found the answer in a buried Rails issue thread: “Refinements break method visibility, complicate debugging, and create invisible dependencies. They’re elegant in theory, painful in practice.”
Ruby gave us this beautiful feature, then the entire ecosystem collectively decided to pretend it doesn’t exist. It’s like being handed a sports car that only works on Sundays, during leap years, when the moon is full.
Legacy support strikes again. Even when you think you’re being clever.
**Emotional Status:** *Humbled, but wiser*
That first clean extraction felt like finding a perfect fossil in a pile of rubble. 84 lines moved to their own module, with clear responsibilities and zero surprises. The monolith shrank by 5%, but more importantly, I proved it could be done without everything exploding—as long as I remembered which Ruby features actually exist across all implementations.

## Phase 2: Parsing &#x26; Validation Surgery 🔍

*Codename: “Deep Cuts”*
Now for the scary stuff. Parsing and validation logic was embedded so deep in the monolith that extracting it felt like removing someone’s spine while they’re still walking.
The parsing methods had tentacles reaching into state management, event handling, and configuration. Everything was coupled to everything else in a beautiful display of how not to write software.

```
# The horror: parsing mixed with state logic
def parse_state_definition(name, options = {})
  # Validate the name (validation concern)
  # Parse the options (parsing concern)
  # Update internal state (state management concern)
  # Trigger callbacks (event handling concern)
  # Log everything (logging concern)
  # Sacrifice a goat to the Ruby gods (legacy concern)
end
```

Breaking this apart required the patience of a bomb disposal expert and the precision of a neurosurgeon. Each extracted method needed careful testing to ensure it still played nice with the remaining monolith. Especially since it needed to be tested across CRuby, JRuby, and TruffleRuby—because apparently one Ruby implementation wasn’t enough suffering.
Oh, and we also had to test all the integrations. Because it’s all fun and vibe coding until you release version 0.30.0 and discover that `state_machines-active_model` and `state_machines-activerecord` are completely broken, because the main gem doesn’t have tests that assert the Transformer assembly stuff actually works. Nothing like learning your architectural surgery killed half the ecosystem after the fact.
And here’s the thing: the gem works. So why refactor it? It’s not about fixing bugs. It’s about sending a message. It’s about emitting a signal that clean architecture matters, that maintainable code is worth fighting for, even when the old mess technically functions.
**Result:** `parsing.rb` (77 LOC) and `validation.rb` (38 LOC) emerged as separate modules. The monolith was down to 1,532 lines and starting to look like it might survive.
**Emotional Status:** *Sweating but hopeful*

## Phase 3: The Protected Layer Excavation 🏗️

*Codename: “Architecture Archaeology”*
This is where it got interesting. The protected methods were like the building’s foundation—mess with them wrong, and the whole structure collapses. But they were also the key to understanding how this thing actually worked.
Helper generators, action hooks, and scoping methods were all tangled together like headphone cables in a pocket. Each one seemed to depend on the others in ways that violated every principle of good design.

```
# Before: a beautiful mess of circular dependencies
protected

def generate_state_helpers
  generate_state_readers
  generate_state_writers
  generate_state_queries
  hook_into_action_methods # Wait, why is this here?
  scope_helper_methods     # And this?!
end
```

The breakthrough came when I realized these weren’t actually dependencies—they were just victims of poor organization. Like finding out your “messy” room just needs better storage solutions.
**Extraction Results:**

- `helper_generators.rb` (124 LOC)

- `action_hooks.rb` (78 LOC)

- `scoping.rb` (91 LOC)

**Emotional Status:** *Starting to see the light*

## Phase 4: Public API Liberation 🚀

*Codename: “The Final Countdown”*
The public methods were the grand finale—the actual interface that thousands of developers depend on. These had to be perfect because any mistake here would break real applications in production.
But here’s the thing about well-designed public APIs: they’re usually clean already. The chaos lives in the implementation, not the interface. So extracting these modules felt like polishing already beautiful gems.

```
# Clean public interfaces emerged naturally
module StateMachines::Configuration
  def state(name, options = {})
    # Clean, focused state configuration
  end
end

module StateMachines::Events
  def event(name, &#x26;block)
    # Clear event handling
  end
end

module StateMachines::Callbacks
  def before_transition(&#x26;block)
    # Obvious callback management
  end
end
```

Each module now had a single, clear responsibility. No more hunting through 1,600 lines to find where state transitions were defined. No more wondering if adding a callback would break event handling.
**Final Extraction Results:**

- `configuration.rb` (156 LOC)

- `state_methods.rb` (134 LOC)

- `event_methods.rb` (189 LOC)

- `callbacks.rb` (145 LOC)

- `rendering.rb` (98 LOC)

- `integration.rb` (112 LOC)

## Plot Twist: The IDE’s Final Judgment

Just when I thought the surgery was complete, my IDE started screaming. Two more files glowed red with warnings, like malevolent eyes staring back at me from the darkness:

- `macro_methods.rb` (522 lines) - DSL definition methods

- `test_helper.rb` (568 lines) - Test utilities

**MacroMethods**: The true horror show. This file looked like the 10 Commandments’ boilerplate—522 lines of what appeared to be harmless DSL definitions. But here’s the kicker: 514 of those lines were comments. The actual code? Seven lines. SEVEN. I had been terrorized by a 98.5% documentation file with seven lines of actual metaprogramming. It wasn’t a zip bomb—it was a documentation novel with a haiku of code at the end.
**TestHelper**: The ironic twist. I’d created this 568-line monster just days earlier, a safety net for developers who still believed in the old myth: “it worked on the maintainer’s machine.” This file contained helpers from the entire state_machines ecosystem—ActiveRecord integrations, audit trails, every adapter ever written. A digital ark of compatibility that I’d built to prevent other developers from harming themselves.
The good news? `test_helper.rb` gets a stay of execution. It’s not loaded at runtime, so it can’t hurt production. It’s just there, waiting patiently like a first-aid kit, ready to help developers who venture into the ecosystem without proper protection.
`macro_methods.rb`, however? That metaprogramming abomination needs to be dismantled with the same surgical precision. But that’s a battle for another day—and probably another blog post.

## The Resurrection: Before &#x26; After

**Before: The Monolith**

```
machine.rb (1,647 LOC)
├── Everything
├── Mixed together
├── In one giant file
└── Good luck finding anything
```

**After: The Constellation**

```
state_machines/
├── utilities.rb (84 LOC)      # Helper methods
├── parsing.rb (77 LOC)        # Private parsing
├── validation.rb (38 LOC)     # Private validation
├── helper_generators.rb (124) # Protected helpers
├── action_hooks.rb (78 LOC)   # Protected hooks
├── scoping.rb (91 LOC)        # Protected scoping
├── configuration.rb (156 LOC) # Public config
├── state_methods.rb (134 LOC) # Public state ops
├── event_methods.rb (189 LOC) # Public events
├── callbacks.rb (145 LOC)     # Public callbacks
├── rendering.rb (98 LOC)      # Public rendering
├── integration.rb (112 LOC)   # Public integrations
└── machine.rb (117 LOC)       # Core coordination
```

The 1,647-line documentation monster became 13 focused modules totaling 1,447 lines. But the real win wasn’t the line count—it was the **comprehensibility**. Each module does one thing well, instead of one file doing everything poorly.

## What Legacy Support Cost Me

Here’s the brutal truth: I could have done this refactoring three years ago. The code was begging for it. The architecture wanted to be clean. But every time I tried, I hit the same wall:
“What if this breaks Ruby 2.7?”
Legacy support didn’t just constrain my features—it constrained my **thinking**. I couldn’t use modern module composition because old Ruby didn’t support it. I couldn’t clean up method signatures because someone might be passing positional arguments. I couldn’t even extract simple utilities without worrying about backward compatibility.
The moment I dropped legacy support, the refactoring became **trivial**. Not easy—trivial. The code wanted to be organized. It just needed permission to use modern patterns.
When I had to test a matrix of 6 Ruby versions, plus JRuby, plus TruffleRuby, by the time one build finished, I had lost the wish to do more open source. Legacy support doesn’t just kill your code—it kills your soul.

## The Freedom of Modernity

Now, with Ruby 3.2+ as the baseline, I can finally write code the way it should be written:

```
# Modern, clean module composition
module StateMachines::Configuration
  def state(name:, initial: false, **options)
    # Keyword arguments! No more parsing hashes!
    # Pattern matching! No more conditional soup!
    # Proper error handling! No more mysterious failures!
  end
end
```

Each module can use:

- **Keyword arguments** for clear, self-documenting APIs

- **Pattern matching** for complex logic branches

- **Proper module composition** for clean dependencies

- **Modern Ruby features** that make code safer and faster

The architecture isn’t just cleaner—it’s **more expressive**. The code says what it means, instead of hiding behind compatibility hacks.

## Lessons from the Operating Table

**1. Start with utilities.** They’re usually safe to extract and give you confidence for the harder stuff.
**2. Test everything, twice.** When you’re performing surgery on a living system, paranoia is your friend.
**3. Legacy support isn’t just technical debt—it’s architectural paralysis.** You can’t build for the future while carrying the past.
**4. Monoliths don’t happen overnight.** They’re the result of a thousand small compromises. Break them apart the same way: one careful extraction at a time.
**5. The code wants to be clean.** Most architectural problems are organizational problems in disguise.

## The Aftermath

The patient survived. More than survived—it thrived.
The new modular architecture isn’t just easier to maintain; it’s easier to **understand**. New contributors can focus on one module instead of deciphering a 1,600-line documentation maze. Features can be added without touching unrelated code. Tests run faster because they can target specific concerns.
But the real victory? **I’m not embarrassed of my own code anymore.**
No more apologizing for the gem’s complexity. No more steering people toward “simpler” alternatives. No more deprecation warnings flooding their terminals.
The gem is finally free to be what it always wanted to be: a clean, modern, focused tool that does state machines well.
And all it took was the courage to stop dragging corpses through my codebase.

## The Unintended Consequence

Of course, there’s one tiny side effect I didn’t anticipate: now that there are more modules, they’re contained… but there are also more opportunities to monkey patch them. In a team of five developers, each team member can now add their own random, untested hack to different modules without conflicts.
Before, everyone had to fight over the same 1,647-line file. Now? Perfect distributed chaos. Each developer gets their own playground to break things in isolation.
I may have just weaponized my architecture. 🙃

## The Final Push

By 2:44 AM, my eyes were closing, but the refactoring was complete. Time for the ultimate test: opening a pull request with the only reviewer available at this ungodly hour—GitHub Copilot, my relentless digital companion who never sleeps and critiques everything from code to life choices.
This is the same AI that once spotted an unfinished recipe in my markdown file and helpfully suggested: “[nitpick] Add salt and pepper to avoid potentially bland food.” Like, dude, this is a Gist—why are you reviewing my cooking?
Unlike Claude, who needs a proper invitation like it’s cinema night, Copilot just shows up uninvited with suggestions. And yes, he keeps adding that robot emoji to every comment like it’s his signature move.
I hit “Create Pull Request” and immediately got two notifications from my tireless reviewer:

🤖 Consider handling the case when [**@helper_modules**](https://github.com/helper_modules)[scope] is not found in ancestors to avoid potential nil slicing errors.

🤖 [nitpick] Consider refactoring the compound condition for clarity; using positive conditions may improve readability.

Classic Copilot. Even at 3 AM, finding edge cases and nitpicking my boolean logic.
I stared at the screen for a moment, then typed my response: “Will see that tomorrow, good night.”
Some battles are worth fighting. Others can wait until you’ve had coffee.
But as I was about to close my laptop, one more notification caught my eye. Not from Copilot this time—from GitHub Issues. A comment on Issue #39, an 8-year-old feature request that had been gathering digital dust since 2016.
“I take this is dead in the water as of now, would love to have this capability.”
Dead in the water? Bro, we’re in outer space. The Open Source Ship isn’t going to build itself.
I bookmarked the issue and finally shut my laptop. Tomorrow’s battle would be different. Tomorrow, I’d arm the state guards.

*Next time: “The Macro Methods Massacre” - Part 3 of the refactoring series, where we dismantle the 98.5% documentation monster that terrorized our codebase.*   
Share Mission Log
     [  Twitter ](https://twitter.com/intent/tweet?text=Operation%3A%20From%20State%20Hero%20to%20Zero&url=https%3A%2F%2Fwww.seuros.com%2Fblog%2Foperation-from-state-hero-to-zero%2F&via=seuross)  [  LinkedIn ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.seuros.com%2Fblog%2Foperation-from-state-hero-to-zero%2F)  [  Reddit ](https://reddit.com/submit?url=https%3A%2F%2Fwww.seuros.com%2Fblog%2Foperation-from-state-hero-to-zero%2F&title=Operation%3A%20From%20State%20Hero%20to%20Zero)  [  HN ](https://news.ycombinator.com/submitlink?u=https%3A%2F%2Fwww.seuros.com%2Fblog%2Foperation-from-state-hero-to-zero%2F&t=Operation%3A%20From%20State%20Hero%20to%20Zero)     
✓ Mission coordinates copied to clipboard!
   🔗
Interstellar Communications
No transmissions detected yet.
Be the first to establish contact!
• Link to this post from your site• Share your thoughts via webmention• Join the IndieWeb conversation
## Related Posts

### Software Is Not a Nursing Home: Breaking Free from Legacy Support
Legacy support isn't just technical debt—it's innovation debt. Here's why I finally bumped my Ruby gem to require version 3.2.0 and why you should stop dragging corpses through your codebase.rubyopensourcemaintainer-experience Jun 19, 2025