I've found that a pretty simple technique along the lines of what's shared in the article makes complex Rails apps much more maintainable. Most of this applies to any MVC style app/framework.

I follow these rules of thumb:

1. Controllers should only handle converting HTTP to ruby calls. That includes logic that is specific to the request flow, like parsing params or authenticating cookies, but nothing else.

2. Models should only handle read/write on their own table. You can use associations, but no referencing another class name inside of a model. No after_* callbacks (and try not to use callbacks at all).

3. What Rails calls "views" should be though of as simple html templates with loops and simple if/else, but no complex logic.

4. Don't use Rails Helper Methods. Just don't.

All by itself this works for toy apps, but now you've got holes where complex presentational and procedural logic has no place to go. So you plug those gaps with two kinds of domain objects: view objects (for complex reads) and action objects (for complex writes).

View Objects (more often called Presenters in Rails land to avoid the conflict with the templates, which rails calls "views"):

These are used to wrap up any kind of complex, multi-model view. So for example, when you have something like an "account settings" page, you probably need to fetch the user and some associated models, maybe billing info, etc. You can make a simple object that takes in URL params in its constructor, efficiently queries whatever is needed to present this page, then freezes. Now you can put whatever data and logic is needed for the template here, and it's very easy to unit test the queries and the individual bits of logic to ensure they're correct.

Action Objects (sometimes called Mutations, Commands, Interactions, Services, or Procedures):

These are used to wrap up any kind of mutative procedure. They should take in a set of inputs, and when called, perform some kind of action (for example, running through all the steps of user registration). These should be written functionally, and should be idempotent whenever possible. Again, wrapping the code up this way makes it very easy to unit test, and to stub in external dependencies when relevant.

These patterns make it really easy to follow what's going on in your app - easy to add new behavior and easy to walk through complex business processes step by step since everything happens in one control flow. And of course you can compose these objects together for the most complex flows. The simple and stable interfaces help keep your program easy to reason about and allow you to work on individual parts in isolation with confidence.

I've used those patterns over the last ten years or so with great success, and more recently have been helping my team at Atlassian gradually convert what was a somewhat messy older Rails app. Happy to answer questions if anyone has any :)

— Edit —

Just to add, there’s one more big benefit, which is that if you code this way it becomes trivial to replicate any of the behavior in your app from the Rails console. Of course this is the same reason it’s easy to test when you build this way, and writing tests is more important than poking around in the console. But when I’ve worked with people who aren’t as in love with testing as I am, I’ve found that they get more excited when I show them how this puts all your apps behavior into an interface that’s very easy to drive from the console. :)