Absolutely agreed - better to make these things explicit somewhere rather than having your business logic implicitly handled by something directly coupled to your persistence layer (with the exception of data integrity tasks as mentioned by danenania).
As a sort of half-way house between using lifecycle callbacks and sticking everything in the controller, I've just moved the main app I work on to a pub-sub event system, which is working out quite nicely. Controllers or mutator methods no longer handle post-action tasks, they just announce what they did, and any interested listeners can act upon the event announced. This way we can have a listener that handles (for example) all post-action emailing, and we can test the logic for that in isolation. Controller logic is cleaned up too, so testing them becomes easier. On the flip side we have to write more comprehensive integration tests to make sure our listeners are all hooked up correctly, but it seems like a pretty good trade-off so far.
Enjoyed the OP nonetheless, though - I didn't know about ActiveModel#previous_changes, that's pretty nifty to have.
I'll post something about it soon - while the changes were wide-ranging for us (because these post-action tasks were initially scattered throughout our app), the extra infrastructure required was really pretty minimal. Most of the actual time spent in this refactoring was writing new tests for code that we'd previously been unable to practically test.
In the meantime, the pub/sub setup we've got is pretty similar to the one presented a little way down the following article:
Our listeners sit completely separate from the controllers, though, and we're not using them for anything like handling controller response logic (I'm not sure how I feel about that, although the idea is certainly interesting).
As a sort of half-way house between using lifecycle callbacks and sticking everything in the controller, I've just moved the main app I work on to a pub-sub event system, which is working out quite nicely. Controllers or mutator methods no longer handle post-action tasks, they just announce what they did, and any interested listeners can act upon the event announced. This way we can have a listener that handles (for example) all post-action emailing, and we can test the logic for that in isolation. Controller logic is cleaned up too, so testing them becomes easier. On the flip side we have to write more comprehensive integration tests to make sure our listeners are all hooked up correctly, but it seems like a pretty good trade-off so far.
Enjoyed the OP nonetheless, though - I didn't know about ActiveModel#previous_changes, that's pretty nifty to have.