By Adam Hawkins, IOD Expert

Earlier this year, I decided to build an SaaS MVP. My goal was simple: achieve MVP quickly. I considered using Rails instead of my standard approach. I wagered that Rails’ focus on rapid development outweighed the negative architectural trade-offs. Plus, I figured it would be worth it to experiment and give Rails another try after years away. I eschewed my concerns and set out to build the MVP.



I worked begrudgingly around systemic bugs and other oddities before completing the MVP. Whenever I face gnarly bugs like this, I always ask myself, “I can work around these issues, but what about others?” or “Why even use this?” Velocity was my reason for using Rails, but at what cost?

Distance from software engineering—and even greater distance from Rails—helped me see things differently, much like returning to your hometown after years of living somewhere else. Rails hasn’t changed. I have.

Like Riding a Bike

I expected Rails to work given my MVP is only a small API. No JavaScript. Not even HTML. Emails? Nope. Third party libraries? Excon. All covered by a handful of integration tests. It’s just Rails: a mish-mash of strong ideas and poor implementations, but good intentions. This recipe skews bugs toward features instead of failures, but there’s always a work-around. My experience taught me to expect bugs in common development work.

During my first integration test, I stumble upon a bug in ActionDispatch. The MVP receives GitHub webhooks. The webhook body innocuously sends an action key in the POST body. That breaks Rails’ routing since action determines which controller method handles the HTTP request. The test fails when test request returns “Not Found.” I change action to _action, and the response code changes from “Not Found” to “OK.” I implement the work-around after I peel my hand off of my face. Doubt creeps in—what else could go wrong? Regrettably, the answer is “more.”

The first production interaction with GitHub fails because of request forgery protection. Forgery protection guards against malicious PUT/POST/DELETE requests from unknown applications. The error is misconfiguration (for lack of a better term). Relevant controllers must skip the forgery protection filter. I edit the relevant controllers, run the tests to check for typos (since testing behavior is too strange in this case), and deploy. Now, it works.

IOD IS A CONTENT CREATION AND RESEARCH COMPANY WORKING WITH SOME OF THE TOP NAMES IN IT.

Our philosophy is experts are not writers and writers are not experts, so we pair tech experts with experienced editors to produce high-quality, deeply technical content.

The author of this post is one of our top experts.

You can be too! JOIN US.

Time to refactor. I dislike referencing a private framework method, and so does Rails. It includes the skip_forgery_protection method for the same result, with added encapsulation. I edit the relevant controllers and run the tests to check for typos. No typos, just failing tests. Requests now return “Not Found.” What in tarnation is going on? Pressing the undo key avoids the rabbit hole. More doubt creeps in. If this is broken, what else could go wrong? Again, to my great displeasure, the answer is “more.”

Tests fail randomly. It appears that tests are not using as-written code on disk, but rather code from the previous run. Spring’s flaky code reloading is the issue. I restart Spring and tests pass. There are two choices: remove Spring and increase the boot time, or keep Spring and reload to fix test flakes. I gleefully purge Spring and maintain a deterministic code-test feedback loop.

The next step requires a background job. This is my first exposure to ActiveJob. I write the job class and enqueue the job via event listeners. The test passes, but occasionally spits out an error message coming from the job. Rails executes jobs asynchronously in test mode and prints errors. Luckily, switching to the test queue adapter collects jobs and executes them on command. The MVP lumbers across the goal line.

Growing Up and Away

I grew up as a professional software engineer building web applications. First with Rails, then with other tools. I learned a lot that has made me an objectively better engineer. Eventually, I grew apart from Rails and time replaced my frustration with disinterest.

Now, I can pinpoint my reasons for avoiding the framework. Rails is too complex (even if you understand it) and has confounding defaults. Spring attempts to improve boot speeds caused by the framework (and the dependency graph) code size. It doesn’t work and creates indeterminism in common activities. Metaprogramming and complex layering create bugs in benign locations.

It’s all too much for me. Trade-offs that used to improve my velocity now inhibit them. Moreover, uncovering them exposes that my technical assumptions are wildly different than Rails’. Mine have changed and Rails’ have not. However, it’s not purely technical—now, it’s about business.

I appreciate my newfound technological disinterest in business decisions. Tech choices like languages, frameworks, and databases don’t build successful businesses. Engineers (myself included) can and will debate technical merits ad infinitum, but it’s ultimately fruitless. I’m glad that I was able to separate personal opinions from business goals and focus on the prime directive: shipping.

Sticking with Rails keeps me shipping. I’ll stick with Rails until the benefits become outweighed by the costs of switching—but I’ll do so dispassionately.